Skip to content

Commit

Permalink
esbuild plugin: add proper support for lume url rewrites #685
Browse files Browse the repository at this point in the history
  • Loading branch information
into-the-v0id committed Nov 1, 2024
1 parent f739c2a commit 0792ac6
Show file tree
Hide file tree
Showing 9 changed files with 844 additions and 92 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Go to the `v1` branch to see the changelog of Lume 1.
- `code_highlight` plugin: configuration type must be Partial [#679].
- Updated dependencies: `sass`, `terser`, `liquid`, `tailwindcss`, `std`, `preact`, `mdx`, `xml`, `satori`, `react` types, `unocss`, `magic-string`.
- esbuild plugin: Add support for `entryNames` option [#678].
- esbuild plugin: Add proper support for lume url rewrites (`basename` and `url`) [#685].

## [2.3.3] - 2024-10-07
### Added
Expand Down Expand Up @@ -575,6 +576,7 @@ Go to the `v1` branch to see the changelog of Lume 1.
[#680]: https://github.com/lumeland/lume/issues/680
[#681]: https://github.com/lumeland/lume/issues/681
[#683]: https://github.com/lumeland/lume/issues/683
[#685]: https://github.com/lumeland/lume/issues/685

[Unreleased]: https://github.com/lumeland/lume/compare/v2.3.3...HEAD
[2.3.3]: https://github.com/lumeland/lume/compare/v2.3.2...v2.3.3
Expand Down
12 changes: 12 additions & 0 deletions core/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ export function normalizePath(path: string, rootToRemove?: string) {
: absolute;
}

/**
* Convert the Windows paths (that use the separator "\")
* to Posix paths (with the separator "/").
*/
export function toUnixPath(path: string): string {
if (SEPARATOR === "/") {
return path;
}

return path.replaceAll(SEPARATOR, "/");
}

/** Check if the path is an URL */
export function isUrl(path: string): boolean {
return !!path.match(/^(https?|file):\/\//);
Expand Down
138 changes: 99 additions & 39 deletions plugins/esbuild.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {
getPathAndExtension,
isAbsolutePath,
isUrl,
normalizePath,
replaceExtension,
toUnixPath,
} from "../core/utils/path.ts";
import { merge } from "../core/utils/object.ts";
import { readDenoConfig } from "../core/utils/deno_config.ts";
Expand All @@ -15,7 +16,14 @@ import {
OutputFile,
stop,
} from "../deps/esbuild.ts";
import { dirname, extname, fromFileUrl, toFileUrl } from "../deps/path.ts";
import {
dirname,
extname,
fromFileUrl,
join,
relative,
toFileUrl,
} from "../deps/path.ts";
import { prepareAsset, saveAsset } from "./source_maps.ts";
import { Page } from "../core/file.ts";
import textLoader from "../core/loaders/text.ts";
Expand Down Expand Up @@ -127,14 +135,20 @@ export function esbuild(userOptions?: Options) {
pages: Page[],
): Promise<[OutputFile[], Metafile, boolean]> {
let enableAllSourceMaps = false;
const entryPoints: string[] = [];
const entryPoints: { in: string; out: string }[] = [];

pages.forEach((page) => {
const { content, filename, enableSourceMap } = prepareAsset(site, page);
if (enableSourceMap) {
enableAllSourceMaps = true;
}
entryPoints.push(filename);

let outUri = getPathAndExtension(page.data.url)[0];
if (outUri.startsWith("/")) {
outUri = outUri.slice(1);
}

entryPoints.push({ in: filename, out: outUri });
entryContent[toFileUrl(filename).href] = content;
});

Expand Down Expand Up @@ -261,8 +275,8 @@ export function esbuild(userOptions?: Options) {
}

// Replace .tsx and .jsx extensions with .js
const content = (!options.options.splitting && !options.options.bundle)
? resolveImports(outputFile.text, options.esm)
const content = !options.options.bundle
? resolveImports(outputFile, basePath, options.esm, metafile)
: outputFile.text;

// Get the associated source map
Expand All @@ -278,22 +292,8 @@ export function esbuild(userOptions?: Options) {
continue;
}

let outputRelativeEntryPoint = output.entryPoint;
if (outputRelativeEntryPoint.startsWith("deno:")) {
outputRelativeEntryPoint = outputRelativeEntryPoint.slice(
"deno:".length,
);
}
if (outputRelativeEntryPoint.startsWith(prefix)) {
outputRelativeEntryPoint = outputRelativeEntryPoint.slice(
prefix.length,
);
}
if (outputRelativeEntryPoint.startsWith("file://")) {
outputRelativeEntryPoint = fromFileUrl(outputRelativeEntryPoint);
}
outputRelativeEntryPoint = normalizePath(
outputRelativeEntryPoint,
const outputRelativeEntryPoint = relativePathFromUri(
output.entryPoint,
basePath,
);

Expand All @@ -309,10 +309,7 @@ export function esbuild(userOptions?: Options) {
}

// The page is an entry point
entryPoint.data.url = normalizedOutPath.replaceAll(
dirname(entryPoint.sourcePath) + "/",
dirname(entryPoint.data.url) + "/",
);
entryPoint.data.url = normalizedOutPath;
saveAsset(site, entryPoint, content, map?.text);
}
});
Expand Down Expand Up @@ -368,23 +365,86 @@ function buildJsxConfig(config?: DenoConfig): BuildOptions | undefined {
}
}

function relativePathFromUri(uri: string, basePath?: string): string {
if (uri.startsWith("deno:")) {
uri = uri.slice("deno:".length);
}

if (uri.startsWith("file://")) {
uri = fromFileUrl(uri);
}

return normalizePath(uri, basePath);
}

function resolveImports(
content: string,
outfile: OutputFile,
basePath: string,
esm: EsmOptions,
metafile: Metafile,
): string {
return content.replaceAll(
/(from\s*)["']([^"']+)["']/g,
(_, from, path) => {
if (path.startsWith(".") || path.startsWith("/")) {
const resolved = path.endsWith(".json")
? path
: replaceExtension(path, ".js");
return `${from}"${resolved}"`;
}
const resolved = import.meta.resolve(path);
return `${from}"${handleEsm(resolved, esm) || resolved}"`;
},
let source = outfile.text;

source = source.replaceAll(
/\bfrom\s*["']([^"']+)["']/g,
(_, path) =>
`from "${resolveImport(path, outfile.path, basePath, esm, metafile)}"`,
);

source = source.replaceAll(
/\bimport\s*["']([^"']+)["']/g,
(_, path) =>
`import "${resolveImport(path, outfile.path, basePath, esm, metafile)}"`,
);

source = source.replaceAll(
/\bimport\([\s\n]*["']([^"']+)["'](?=[\s\n]*[,)])/g,
(_, path) =>
`import("${resolveImport(path, outfile.path, basePath, esm, metafile)}"`,
);

return source;
}

function resolveImport(
importPath: string,
sourceFile: string,
basePath: string,
esm: EsmOptions,
metafile: Metafile,
): string {
if (importPath.startsWith(".") || importPath.startsWith("/")) {
const absoluteImportPath = join(dirname(sourceFile), importPath);
const sourcePathOfImport = normalizePath(absoluteImportPath, basePath);
const outputOfImport = Object.entries(metafile.outputs)
.find(([_, output]) => {
if (!output.entryPoint) {
return false;
}

const outputRelativeEntryPoint = relativePathFromUri(
output.entryPoint,
basePath,
);

return outputRelativeEntryPoint === sourcePathOfImport;
});

if (!outputOfImport) {
return importPath;
}

const outputPathOfImport = outputOfImport[0];
const relativeOutputPathOfImport = relative(
dirname(sourceFile),
basePath + "/" + outputPathOfImport,
);

return "./" + toUnixPath(relativeOutputPathOfImport);
}

const resolved = import.meta.resolve(importPath);
return handleEsm(resolved, esm) || resolved;
}

function handleEsm(path: string, options: EsmOptions): string | undefined {
Expand Down
614 changes: 582 additions & 32 deletions tests/__snapshots__/esbuild.test.ts.snap

Large diffs are not rendered by default.

84 changes: 73 additions & 11 deletions tests/__snapshots__/source_maps.test.ts.snap

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion tests/assets/esbuild/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
/// <reference lib="dom" />
import toUppercase from "./modules/to_uppercase.ts";
import toUppercase, { toLowercase } from "./modules/to_uppercase.ts";
import data from "./data.json";

// https://github.com/lumeland/lume/issues/442
import "https://esm.sh/v127/[email protected]/denonext/prop-types.development.mjs";

document.querySelectorAll("h1")?.forEach((h1) => {
h1.innerHTML = toUppercase(h1.innerHTML + data.foo);

toLowercase(h1.innerHTML)
.then(lower => {
h1.innerHTML = lower;
});
});
6 changes: 6 additions & 0 deletions tests/assets/esbuild/modules/to_uppercase.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export default function toUppercase(text: string) {
return text.toUpperCase();
}

export async function toLowercase(text: string) {
const { toLowercase } = await import("../other/to_lowercase.ts");

return toLowercase(text);
}
3 changes: 3 additions & 0 deletions tests/assets/esbuild/other/to_lowercase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function toLowercase(text: string) {
return text.toLowerCase();
}
70 changes: 61 additions & 9 deletions tests/esbuild.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Deno.test(
src: "esbuild",
});

site.data("basename", "toLower", "/other/to_lowercase.ts");

// Test ignore with a function filter
site.ignore((path) => path === "/modules" || path.startsWith("/modules/"));
site.use(esbuild());
Expand All @@ -29,6 +31,8 @@ Deno.test(
src: "esbuild",
});

site.data("basename", "toLower", "/other/to_lowercase.ts");

// Test ignore with a function filter
site.ignore((path) => path === "/modules" || path.startsWith("/modules/"));
site.use(esbuild({
Expand All @@ -41,19 +45,35 @@ Deno.test(
await build(site);

// Normalize chunk name
let chunkIndex = 0;
const chunkMap: { [chunk: string]: string } = {};
for (const page of site.pages) {
const url = page.data.url;

if (url.match(/chunk-[\w]{8}\.js/)) {
page.data.url = url.replace(/chunk-[\w]{8}\.js/, "chunk.js");
page.data.basename = page.data.basename.replace(
/chunk-[\w]{8}/,
"chunk",
);
} else {
const content = page.content as string;
page.content = content.replace(/chunk-[\w]{8}\.js/, "chunk.js");
const match = url.match(/chunk-[\w]{8}\.js/);
if (!match) {
continue;
}

page.data.url = url.replace(
/chunk-[\w]{8}\.js/,
`chunk-${chunkIndex}.js`,
);
page.data.basename = page.data.basename.replace(
/chunk-[\w]{8}/,
`chunk-${chunkIndex}`,
);
chunkMap[match[0]] = `chunk-${chunkIndex}.js`;
chunkIndex += 1;
}
for (const page of site.pages) {
let content = page.content as string;
for (
const [originalChunkName, newChunkName] of Object.entries(chunkMap)
) {
content = content.replace(originalChunkName, newChunkName);
}
page.content = content;
}

await assertSiteSnapshot(t, site);
Expand All @@ -69,6 +89,8 @@ Deno.test(
src: "esbuild_jsx",
});

site.data("basename", "toLower", "/other/to_lowercase.ts");

site.use(jsx({
pageSubExtension: ".page",
}));
Expand All @@ -91,6 +113,8 @@ Deno.test(
src: "esbuild",
});

site.data("basename", "toLower", "/other/to_lowercase.ts");

site.use(esbuild({
options: {
outExtension: { ".js": ".min.js" },
Expand All @@ -111,6 +135,8 @@ Deno.test(
src: "esbuild",
});

site.data("basename", "toLower", "/other/to_lowercase.ts");

site.use(esbuild({
options: {
entryNames: "js/[name].hash",
Expand All @@ -131,6 +157,8 @@ Deno.test(
src: "esbuild",
});

site.data("basename", "toLower", "/other/to_lowercase.ts");

site.use(esbuild({
options: {
entryNames: "one/[dir]/two/[name]/hash",
Expand All @@ -141,3 +169,27 @@ Deno.test(
await assertSiteSnapshot(t, site);
},
);

// Disable sanitizeOps & sanitizeResources because esbuild doesn't close them
Deno.test(
"esbuild plugin without bundle",
{ sanitizeOps: false, sanitizeResources: false },
async (t) => {
const site = getSite({
src: "esbuild",
});

site.data("basename", "toLower", "/other/to_lowercase.ts");

site.use(esbuild({
options: {
bundle: false,
entryNames: "[dir]/[name].hash",
outExtension: { ".js": ".min.js" },
},
}));

await build(site);
await assertSiteSnapshot(t, site);
},
);

0 comments on commit 0792ac6

Please sign in to comment.