Skip to content

Commit 1b26bbd

Browse files
committed
fix(Examples): fix standalone examples build
1 parent a8167fc commit 1b26bbd

File tree

1 file changed

+122
-16
lines changed

1 file changed

+122
-16
lines changed

Documentation/scripts/build-examples.mjs

Lines changed: 122 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,19 @@ const EXAMPLE_SOURCES = [
4747
},
4848
];
4949

50-
function buildHtml(exampleName, bundleFile, isModule = false) {
51-
const exampleScript = isModule
52-
? `<script type="module" src="${bundleFile}"></script>`
53-
: `<script src="${bundleFile}"></script>`;
50+
function buildHtml(
51+
exampleName,
52+
bundleFile,
53+
isModule = false,
54+
inlineScript = null
55+
) {
56+
let exampleScript = `<script src="${bundleFile}"></script>`;
57+
if (isModule) {
58+
exampleScript = `<script type="module" src="${bundleFile}"></script>`;
59+
}
60+
if (inlineScript) {
61+
exampleScript = `<script>${inlineScript}</script>`;
62+
}
5463

5564
return `<!doctype html>
5665
<html lang="en">
@@ -112,16 +121,37 @@ async function collectEntries() {
112121
return entries;
113122
}
114123

115-
function assetLoader() {
124+
function getMimeType(filePath) {
125+
const ext = path.extname(filePath).toLowerCase();
126+
const mimeByExt = {
127+
'.png': 'image/png',
128+
'.jpg': 'image/jpeg',
129+
'.jpeg': 'image/jpeg',
130+
'.gif': 'image/gif',
131+
'.svg': 'image/svg+xml',
132+
'.webp': 'image/webp',
133+
};
134+
return mimeByExt[ext] || 'application/octet-stream';
135+
}
136+
137+
function toDataUri(filePath, source) {
138+
return `data:${getMimeType(filePath)};base64,${source.toString('base64')}`;
139+
}
140+
141+
function assetLoader({ inline = false } = {}) {
116142
const assetRegex = /\.(png|jpe?g)$/i;
143+
117144
return {
118-
name: 'asset-loader',
145+
name: inline ? 'asset-loader-inline' : 'asset-loader',
119146
async load(id) {
120147
if (!assetRegex.test(id)) {
121148
return null;
122149
}
123150

124151
const source = await fs.readFile(id);
152+
if (inline) {
153+
return `export default ${JSON.stringify(toDataUri(id, source))};`;
154+
}
125155
const refId = this.emitFile({
126156
type: 'asset',
127157
name: path.basename(id),
@@ -153,6 +183,60 @@ function replaceBasePath(basePath) {
153183
};
154184
}
155185

186+
function inlineCssUrls() {
187+
const cssFileRegex = /\.css$/i;
188+
const urlRegex = /url\((['"]?)([^'")]+)\1\)/g;
189+
return {
190+
name: 'inline-css-urls',
191+
async transform(code, id) {
192+
if (!cssFileRegex.test(id)) {
193+
return null;
194+
}
195+
196+
const matches = Array.from(code.matchAll(urlRegex));
197+
if (!matches.length) {
198+
return null;
199+
}
200+
201+
const replacements = await Promise.all(
202+
matches.map(async ([fullMatch, quote, rawUrl]) => {
203+
const assetUrl = rawUrl.trim();
204+
if (
205+
assetUrl.startsWith('data:') ||
206+
assetUrl.startsWith('http://') ||
207+
assetUrl.startsWith('https://') ||
208+
assetUrl.startsWith('//')
209+
) {
210+
return { fullMatch, replacement: fullMatch };
211+
}
212+
213+
const assetPath = path.resolve(path.dirname(id), assetUrl);
214+
try {
215+
const source = await fs.readFile(assetPath);
216+
const dataUrl = toDataUri(assetPath, source);
217+
return {
218+
fullMatch,
219+
replacement: `url(${quote}${dataUrl}${quote})`,
220+
};
221+
} catch (err) {
222+
return { fullMatch, replacement: fullMatch };
223+
}
224+
})
225+
);
226+
227+
let transformed = code;
228+
replacements.forEach(({ fullMatch, replacement }) => {
229+
transformed = transformed.replace(fullMatch, replacement);
230+
});
231+
232+
return {
233+
code: transformed,
234+
map: null,
235+
};
236+
},
237+
};
238+
}
239+
156240
async function walkFiles(dir, results = []) {
157241
const entries = await fs.readdir(dir, { withFileTypes: true });
158242
await Promise.all(
@@ -169,15 +253,15 @@ async function walkFiles(dir, results = []) {
169253
}
170254

171255
async function copyApplicationStaticAssets(entryPath, outDir) {
172-
const appDir = path.dirname(entryPath);
256+
const sourceDir = path.dirname(entryPath);
173257
const assetRegex = /\.(png|jpe?g|gif|svg|webp)$/i;
174-
const files = await walkFiles(appDir);
258+
const files = await walkFiles(sourceDir);
175259

176260
await Promise.all(
177261
files
178262
.filter((filePath) => assetRegex.test(filePath))
179263
.map(async (filePath) => {
180-
const relPath = path.relative(appDir, filePath);
264+
const relPath = path.relative(sourceDir, filePath);
181265
const destPath = path.resolve(outDir, relPath);
182266
await fs.mkdir(path.dirname(destPath), { recursive: true });
183267
await fs.copyFile(filePath, destPath);
@@ -205,7 +289,7 @@ async function build() {
205289
const stringPlugin = string.default ? string.default : string;
206290
const ignorePlugin = ignore.default ? ignore.default : ignore;
207291

208-
function createPlugins() {
292+
function createPlugins({ forInlineIife = false } = {}) {
209293
return [
210294
aliasPlugin({
211295
entries: [
@@ -217,6 +301,7 @@ async function build() {
217301
],
218302
}),
219303
ignorePlugin(['crypto']),
304+
...(forInlineIife ? [inlineCssUrls()] : []),
220305
webworkerPlugin({
221306
targetPlatform: 'browser',
222307
pattern: /^.+\.worker(?:\.js)?$/,
@@ -250,7 +335,7 @@ async function build() {
250335
},
251336
plugins: [autoprefixer],
252337
}),
253-
assetLoader(),
338+
assetLoader({ inline: forInlineIife }),
254339
replaceBasePath('/vtk-js'),
255340
];
256341
}
@@ -282,6 +367,14 @@ async function build() {
282367
assetFileNames: '_assets/[name]-[hash][extname]',
283368
});
284369
await esBundle.close();
370+
371+
await Promise.all(
372+
Object.entries(esEntries).map(async ([chunkName, entryPath]) => {
373+
const outDir = path.resolve(distDir, chunkName);
374+
await fs.mkdir(outDir, { recursive: true });
375+
await copyApplicationStaticAssets(entryPath, outDir);
376+
})
377+
);
285378
}
286379

287380
await Object.entries(applicationEntries).reduce(
@@ -292,18 +385,28 @@ async function build() {
292385
await fs.mkdir(outDir, { recursive: true });
293386
const bundle = await rollup({
294387
input: entryPath,
295-
plugins: createPlugins(),
388+
plugins: createPlugins({ forInlineIife: true }),
296389
});
297-
await bundle.write({
298-
file: path.resolve(outDir, 'index.js'),
390+
const { output } = await bundle.generate({
299391
format: 'iife',
300392
name: `${chunkName.replace(/[^\w$]/g, '_')}`,
301393
inlineDynamicImports: true,
302-
assetFileNames: '_assets/[name]-[hash][extname]',
303394
});
304395
await bundle.close();
305396

306-
await copyApplicationStaticAssets(entryPath, outDir);
397+
const appChunk = output.find((item) => item.type === 'chunk');
398+
if (!appChunk) {
399+
throw new Error(`Failed to generate inline IIFE for ${chunkName}`);
400+
}
401+
const inlineScript = appChunk.code.replace(
402+
/<\/script>/gi,
403+
'<\\/script>'
404+
);
405+
await fs.writeFile(
406+
path.resolve(outDir, 'index.html'),
407+
buildHtml(chunkName, './index.js', false, inlineScript),
408+
'utf8'
409+
);
307410
}),
308411
Promise.resolve()
309412
);
@@ -313,6 +416,9 @@ async function build() {
313416
const outDir = path.resolve(distDir, chunkName);
314417
const bundleFile = './index.js';
315418
const isModule = !Object.hasOwn(applicationEntries, chunkName);
419+
if (!isModule) {
420+
return;
421+
}
316422

317423
await fs.mkdir(outDir, { recursive: true });
318424
await fs.writeFile(

0 commit comments

Comments
 (0)