Skip to content

Commit d4bb414

Browse files
committed
esbuild plugin: add proper support for lume url rewrites lumeland#685
1 parent f739c2a commit d4bb414

7 files changed

Lines changed: 734 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Go to the `v1` branch to see the changelog of Lume 1.
2424
- `code_highlight` plugin: configuration type must be Partial [#679].
2525
- Updated dependencies: `sass`, `terser`, `liquid`, `tailwindcss`, `std`, `preact`, `mdx`, `xml`, `satori`, `react` types, `unocss`, `magic-string`.
2626
- esbuild plugin: Add support for `entryNames` option [#678].
27+
- esbuild plugin: Add proper support for lume url rewrites (`basename` and `url`) [#685].
2728

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

579581
[Unreleased]: https://github.com/lumeland/lume/compare/v2.3.3...HEAD
580582
[2.3.3]: https://github.com/lumeland/lume/compare/v2.3.2...v2.3.3

plugins/esbuild.ts

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
getPathAndExtension,
23
isAbsolutePath,
34
isUrl,
45
normalizePath,
@@ -15,7 +16,14 @@ import {
1516
OutputFile,
1617
stop,
1718
} from "../deps/esbuild.ts";
18-
import { dirname, extname, fromFileUrl, toFileUrl } from "../deps/path.ts";
19+
import {
20+
dirname,
21+
extname,
22+
fromFileUrl,
23+
join,
24+
relative,
25+
toFileUrl,
26+
} from "../deps/path.ts";
1927
import { prepareAsset, saveAsset } from "./source_maps.ts";
2028
import { Page } from "../core/file.ts";
2129
import textLoader from "../core/loaders/text.ts";
@@ -127,14 +135,20 @@ export function esbuild(userOptions?: Options) {
127135
pages: Page[],
128136
): Promise<[OutputFile[], Metafile, boolean]> {
129137
let enableAllSourceMaps = false;
130-
const entryPoints: string[] = [];
138+
const entryPoints: { in: string; out: string }[] = [];
131139

132140
pages.forEach((page) => {
133141
const { content, filename, enableSourceMap } = prepareAsset(site, page);
134142
if (enableSourceMap) {
135143
enableAllSourceMaps = true;
136144
}
137-
entryPoints.push(filename);
145+
146+
let outUri = getPathAndExtension(page.data.url)[0];
147+
if (outUri.startsWith("/")) {
148+
outUri = outUri.slice(1);
149+
}
150+
151+
entryPoints.push({ in: filename, out: outUri });
138152
entryContent[toFileUrl(filename).href] = content;
139153
});
140154

@@ -261,8 +275,8 @@ export function esbuild(userOptions?: Options) {
261275
}
262276

263277
// Replace .tsx and .jsx extensions with .js
264-
const content = (!options.options.splitting && !options.options.bundle)
265-
? resolveImports(outputFile.text, options.esm)
278+
const content = !options.options.bundle
279+
? resolveImports(outputFile, basePath, options.esm, allPages)
266280
: outputFile.text;
267281

268282
// Get the associated source map
@@ -309,10 +323,7 @@ export function esbuild(userOptions?: Options) {
309323
}
310324

311325
// The page is an entry point
312-
entryPoint.data.url = normalizedOutPath.replaceAll(
313-
dirname(entryPoint.sourcePath) + "/",
314-
dirname(entryPoint.data.url) + "/",
315-
);
326+
entryPoint.data.url = normalizedOutPath;
316327
saveAsset(site, entryPoint, content, map?.text);
317328
}
318329
});
@@ -369,22 +380,63 @@ function buildJsxConfig(config?: DenoConfig): BuildOptions | undefined {
369380
}
370381

371382
function resolveImports(
372-
content: string,
383+
outfile: OutputFile,
384+
basePath: string,
373385
esm: EsmOptions,
386+
pages: Page[],
374387
): string {
375-
return content.replaceAll(
376-
/(from\s*)["']([^"']+)["']/g,
377-
(_, from, path) => {
378-
if (path.startsWith(".") || path.startsWith("/")) {
379-
const resolved = path.endsWith(".json")
380-
? path
381-
: replaceExtension(path, ".js");
382-
return `${from}"${resolved}"`;
383-
}
384-
const resolved = import.meta.resolve(path);
385-
return `${from}"${handleEsm(resolved, esm) || resolved}"`;
386-
},
388+
let source = outfile.text;
389+
390+
source = source.replaceAll(
391+
/\bfrom\s*["']([^"']+)["']/g,
392+
(_, path) =>
393+
`from "${resolveImport(path, outfile.path, basePath, esm, pages)}"`,
394+
);
395+
396+
source = source.replaceAll(
397+
/\bimport\s*["']([^"']+)["']/g,
398+
(_, path) =>
399+
`import "${resolveImport(path, outfile.path, basePath, esm, pages)}"`,
400+
);
401+
402+
source = source.replaceAll(
403+
/\bimport\([\s\n]*["']([^"']+)["'](?=[\s\n]*[,)])/g,
404+
(_, path) =>
405+
`import("${resolveImport(path, outfile.path, basePath, esm, pages)}"`,
387406
);
407+
408+
return source;
409+
}
410+
411+
function resolveImport(
412+
importPath: string,
413+
sourceFile: string,
414+
basePath: string,
415+
esm: EsmOptions,
416+
pages: Page[],
417+
): string {
418+
if (importPath.startsWith(".") || importPath.startsWith("/")) {
419+
const absoluteImportPath = join(dirname(sourceFile), importPath);
420+
const sourcePathOfImport = normalizePath(absoluteImportPath, basePath);
421+
const sourcePageOfImport = pages.find((page) =>
422+
page.sourcePath === sourcePathOfImport
423+
);
424+
425+
const sourceUriOfImport = sourcePageOfImport
426+
? "./" +
427+
relative(
428+
dirname(sourceFile),
429+
basePath + "/" + sourcePageOfImport.data.url,
430+
)
431+
: importPath;
432+
433+
return sourceUriOfImport.endsWith(".json")
434+
? sourceUriOfImport
435+
: replaceExtension(sourceUriOfImport, ".js");
436+
}
437+
438+
const resolved = import.meta.resolve(importPath);
439+
return handleEsm(resolved, esm) || resolved;
388440
}
389441

390442
function handleEsm(path: string, options: EsmOptions): string | undefined {

tests/__snapshots__/esbuild.test.ts.snap

Lines changed: 582 additions & 32 deletions
Large diffs are not rendered by default.

tests/assets/esbuild/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
/// <reference lib="dom" />
2-
import toUppercase from "./modules/to_uppercase.ts";
2+
import toUppercase, { toLowercase } from "./modules/to_uppercase.ts";
33
import data from "./data.json";
44

55
// https://github.com/lumeland/lume/issues/442
66
import "https://esm.sh/v127/prop-types@15.8.1/denonext/prop-types.development.mjs";
77

88
document.querySelectorAll("h1")?.forEach((h1) => {
99
h1.innerHTML = toUppercase(h1.innerHTML + data.foo);
10+
11+
toLowercase(h1.innerHTML)
12+
.then(lower => {
13+
h1.innerHTML = lower;
14+
});
1015
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
export default function toUppercase(text: string) {
22
return text.toUpperCase();
33
}
4+
5+
export async function toLowercase(text: string) {
6+
const { toLowercase } = await import("../other/to_lowercase.ts");
7+
8+
return toLowercase(text);
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function toLowercase(text: string) {
2+
return text.toLowerCase();
3+
}

tests/esbuild.test.ts

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Deno.test(
1111
src: "esbuild",
1212
});
1313

14+
site.data("basename", "toLower", "/other/to_lowercase.ts");
15+
1416
// Test ignore with a function filter
1517
site.ignore((path) => path === "/modules" || path.startsWith("/modules/"));
1618
site.use(esbuild());
@@ -29,6 +31,8 @@ Deno.test(
2931
src: "esbuild",
3032
});
3133

34+
site.data("basename", "toLower", "/other/to_lowercase.ts");
35+
3236
// Test ignore with a function filter
3337
site.ignore((path) => path === "/modules" || path.startsWith("/modules/"));
3438
site.use(esbuild({
@@ -41,19 +45,35 @@ Deno.test(
4145
await build(site);
4246

4347
// Normalize chunk name
48+
let chunkIndex = 0;
49+
const chunkMap: { [chunk: string]: string } = {};
4450
for (const page of site.pages) {
4551
const url = page.data.url;
4652

47-
if (url.match(/chunk-[\w]{8}\.js/)) {
48-
page.data.url = url.replace(/chunk-[\w]{8}\.js/, "chunk.js");
49-
page.data.basename = page.data.basename.replace(
50-
/chunk-[\w]{8}/,
51-
"chunk",
52-
);
53-
} else {
54-
const content = page.content as string;
55-
page.content = content.replace(/chunk-[\w]{8}\.js/, "chunk.js");
53+
const match = url.match(/chunk-[\w]{8}\.js/);
54+
if (!match) {
55+
continue;
56+
}
57+
58+
page.data.url = url.replace(
59+
/chunk-[\w]{8}\.js/,
60+
`chunk-${chunkIndex}.js`,
61+
);
62+
page.data.basename = page.data.basename.replace(
63+
/chunk-[\w]{8}/,
64+
`chunk-${chunkIndex}`,
65+
);
66+
chunkMap[match[0]] = `chunk-${chunkIndex}.js`;
67+
chunkIndex += 1;
68+
}
69+
for (const page of site.pages) {
70+
let content = page.content as string;
71+
for (
72+
const [originalChunkName, newChunkName] of Object.entries(chunkMap)
73+
) {
74+
content = content.replace(originalChunkName, newChunkName);
5675
}
76+
page.content = content;
5777
}
5878

5979
await assertSiteSnapshot(t, site);
@@ -69,6 +89,8 @@ Deno.test(
6989
src: "esbuild_jsx",
7090
});
7191

92+
site.data("basename", "toLower", "/other/to_lowercase.ts");
93+
7294
site.use(jsx({
7395
pageSubExtension: ".page",
7496
}));
@@ -91,6 +113,8 @@ Deno.test(
91113
src: "esbuild",
92114
});
93115

116+
site.data("basename", "toLower", "/other/to_lowercase.ts");
117+
94118
site.use(esbuild({
95119
options: {
96120
outExtension: { ".js": ".min.js" },
@@ -111,6 +135,8 @@ Deno.test(
111135
src: "esbuild",
112136
});
113137

138+
site.data("basename", "toLower", "/other/to_lowercase.ts");
139+
114140
site.use(esbuild({
115141
options: {
116142
entryNames: "js/[name].hash",
@@ -131,6 +157,8 @@ Deno.test(
131157
src: "esbuild",
132158
});
133159

160+
site.data("basename", "toLower", "/other/to_lowercase.ts");
161+
134162
site.use(esbuild({
135163
options: {
136164
entryNames: "one/[dir]/two/[name]/hash",
@@ -141,3 +169,27 @@ Deno.test(
141169
await assertSiteSnapshot(t, site);
142170
},
143171
);
172+
173+
// Disable sanitizeOps & sanitizeResources because esbuild doesn't close them
174+
Deno.test(
175+
"esbuild plugin without bundle",
176+
{ sanitizeOps: false, sanitizeResources: false },
177+
async (t) => {
178+
const site = getSite({
179+
src: "esbuild",
180+
});
181+
182+
site.data("basename", "toLower", "/other/to_lowercase.ts");
183+
184+
site.use(esbuild({
185+
options: {
186+
bundle: false,
187+
entryNames: "[dir]/[name].hash",
188+
outExtension: { ".js": ".min.js" },
189+
},
190+
}));
191+
192+
await build(site);
193+
await assertSiteSnapshot(t, site);
194+
},
195+
);

0 commit comments

Comments
 (0)