From 2d4d097560768c4506fb1e45b4fe17bbf767ccee Mon Sep 17 00:00:00 2001 From: Miel Truyen Date: Wed, 27 Mar 2024 17:23:02 +0100 Subject: [PATCH] wip: reorganize plugin into its hooks --- src/plugin/hooks/generate-bundle.ts | 142 +++++++++++ src/plugin/hooks/index.ts | 3 + src/plugin/hooks/resolve-id.ts | 51 ++++ src/plugin/hooks/transform.ts | 200 +++++++++++++++ src/plugin/html.ts | 361 +++------------------------- 5 files changed, 428 insertions(+), 329 deletions(-) create mode 100644 src/plugin/hooks/generate-bundle.ts create mode 100644 src/plugin/hooks/index.ts create mode 100644 src/plugin/hooks/resolve-id.ts create mode 100644 src/plugin/hooks/transform.ts diff --git a/src/plugin/hooks/generate-bundle.ts b/src/plugin/hooks/generate-bundle.ts new file mode 100644 index 0000000..536c9b9 --- /dev/null +++ b/src/plugin/hooks/generate-bundle.ts @@ -0,0 +1,142 @@ +import type { + OutputBundle, + OutputChunk, + Plugin, +} from 'rollup'; +import {dirname} from "node:path"; +// nodejs imports (io, path) +import path from "node:path"; +import posix from "node:path/posix"; + +// utilities +import {HtmlModule} from "../../types/html-module.ts"; + + +export interface RewriteUrlCallbackContext { + from: string; + rootPath: string; +} +export type RewriteUrlCallback = (relative: string, context: RewriteUrlCallbackContext) => string|Promise; + +export function generateBundle({ + virtualSources, + pluginName, modulePrefix, rewriteUrl + }: { + virtualSources: Map, + pluginName: string, + modulePrefix: string, + rewriteUrl?: RewriteUrlCallback +}): Plugin['generateBundle']{ + return { + async handler(outputOptions, bundles){ + const bundleItems = Object.entries(bundles); + const virtualBundles = new Set(); + const facadeToChunk = new Map(); + const htmlResults = new Map(); + + for(const [bundleName, bundle] of bundleItems) { + const chunk = (bundle); + if(chunk.facadeModuleId) { + facadeToChunk.set(chunk.facadeModuleId, chunk); + + const moduleInfo = this.getModuleInfo(chunk.facadeModuleId); + const htmlModule = moduleInfo?.meta?.[pluginName]; + // const htmlModule = htmlModules.get(chunk.facadeModuleId); + + if(htmlModule){ htmlResults.set(bundleName, {chunk, htmlModule})} + else if(virtualSources.has(chunk.facadeModuleId)){ + virtualBundles.add(bundleName); + } + } + } + + for(const [bundleName, {chunk, htmlModule}] of htmlResults.entries()){ + if(htmlModule. document) { + // Delete the placeholder chunk from the bundle and emit an asset file for the HTML instead. + deleteFromBundle(bundleName, bundles); + + // Interpret the module and take its default export (TODO: if [NodeJS vm SourceTextModule](https://nodejs.org/api/vm.html#class-vmsourcetextmodule) ever lands, it would be cleaner to use that one instead of directly importing it) + let htmlContents: string; + + // Take out the sourceMapUrl if any (it will not have been written yet and tends to cause a crash, we don't need it anyway)) + let sanitizedCode = chunk.code; + + // Use the modulePrefix to filter out prepended code that is not relevant for us (like live-reload) + const moduleStart = sanitizedCode.indexOf(modulePrefix); + if(moduleStart>=0){ + sanitizedCode = sanitizedCode.slice(moduleStart+modulePrefix.length); + } + // Filter out any sourceMapping url that may have been added + const sourceMapRE = /\/\/# sourceMappingURL=(.+)/.exec(sanitizedCode); + if(sourceMapRE){ + sanitizedCode = sanitizedCode.slice(0,sourceMapRE.index)+sanitizedCode.slice(sourceMapRE.index+sourceMapRE[0].length); + } + + // Encode into a url that we can import(...) + // const importUrl = `data:text/javascript;base64,${Buffer.from(sanitizedCode).toString('base64')}`; // Safer, but unrecognizable if this throws an error + const importUrl = `data:text/javascript,${encodeURIComponent(sanitizedCode)}`; // Due to needed encoding still hard to read, but it might at least be recognizable by the user if it throws an error + + try{ + let htmlJsModule = await import(importUrl); + htmlContents = htmlJsModule.default; + }catch(err){ + throw new Error([ + `Failed to parse resulting HTML-module. Most likely this is due to a plugin that has altered the module in such a way that we cannot easely evaluate it in NodeJS.`, + `The code we tried to evaluate:`, + sanitizedCode.split('\n').map(x=>` ${x}`).join('\n'), + `The error we got:`, + err + ].join('\n')) + // TODO: We could try to fallback as follows, but the issues are likely to persist in the end result + // for(const htmlImport of htmlModule.imports){ + // if(htmlImport.referenceId) { + // const fileName = this.getFileName(htmlImport.referenceId); + // htmlImport.reference.set(fileName); + // } + // } + // serialized = serializeHtml(htmlModule.document); + } + + // Inject the inlined chunks (TODO cleanup) + for(const htmlImport of htmlModule.imports){ + const importResult = facadeToChunk.get(htmlImport.resolved?.id!); + if(importResult){ + if(htmlImport.type === 'chunk') { + htmlContents = htmlContents.replace(htmlImport.placeholder, importResult.code); + }else if(htmlImport.type === 'entryChunk'){ + const relPath = posix.relative(dirname(chunk.fileName), importResult.fileName); + const rootPath = path.posix.join(dirname(chunk.fileName), relPath); + const rewritten = rewriteUrl? await Promise.resolve(rewriteUrl(relPath, { + from: chunk.fileName, + rootPath, + })): relPath; + htmlContents = htmlContents.replace(htmlImport.placeholder, rewritten); + } + } + } + + this.emitFile({ + type: 'asset', + name: htmlModule.name, + fileName: chunk.fileName, + source: htmlContents, + }); + }else{ + throw new Error('something went wrong...'); + } + } + for( const bundleName of virtualBundles.keys()){ + deleteFromBundle(bundleName, bundles, false); + } + } + } +} + + +function deleteFromBundle(bundlename: string, bundle: OutputBundle, deleteMap: boolean = true){ + delete bundle[bundlename]; + if(deleteMap) { + delete bundle[`${bundlename}.map`];// Also delete any generated map files because they don't make any sense. (TODO: there seems to be no better way to detect this?) + } +} + diff --git a/src/plugin/hooks/index.ts b/src/plugin/hooks/index.ts new file mode 100644 index 0000000..0c80549 --- /dev/null +++ b/src/plugin/hooks/index.ts @@ -0,0 +1,3 @@ +export * from "./resolve-id.ts"; +export * from "./transform.ts"; +export * from "./generate-bundle.ts"; diff --git a/src/plugin/hooks/resolve-id.ts b/src/plugin/hooks/resolve-id.ts new file mode 100644 index 0000000..40d7b1c --- /dev/null +++ b/src/plugin/hooks/resolve-id.ts @@ -0,0 +1,51 @@ +import type { + Plugin, +} from 'rollup'; +import {createFilter} from '@rollup/pluginutils'; +import {extname} from "node:path"; + + +export function resolveId({ + virtualSources, + filter, + pluginName +}: { + virtualSources: Map, + filter: ReturnType, + pluginName: string +}): Plugin['resolveId']{ + return { + handler: async function (specifier, + importer, + options){ + if(virtualSources.has(specifier)) return specifier; // Resolve virtual sources we added + if(!filter(specifier)) return; + + // Let it be resolved like others (node_modules, project aliases, ..) + const resolved = await this.resolve(specifier, importer, { + skipSelf: true, + ...options, + }); + + if(resolved){ + const moduleExt = extname(resolved.id); + const moduleName = specifier.replace(new RegExp(`${moduleExt}\$`),''); // strip extension of the name if any + + return { + ...resolved, + meta: { + ...resolved.meta, + [pluginName]: { + specifier: specifier, + id: resolved.id, + name: moduleName, + imports: [], + assetId: null, + importers: new Set(), + } + } + } + } + } + } +} diff --git a/src/plugin/hooks/transform.ts b/src/plugin/hooks/transform.ts new file mode 100644 index 0000000..e02d0dd --- /dev/null +++ b/src/plugin/hooks/transform.ts @@ -0,0 +1,200 @@ +import type { + Plugin, +} from 'rollup'; +import {createFilter} from '@rollup/pluginutils'; +import {extname} from "node:path"; + +// parse5 package is used for parsing HTML. +import {parse as parseHtml, serialize as serializeHtml, DefaultTreeAdapterMap} from "parse5"; +// magic-string to transform code and keeping a sourcemap aligned +import MagicString from "magic-string"; + +// nodejs imports (io, path) +import posix from "node:path/posix"; +import crypto from "node:crypto"; + +import type {LoadNodeCallback, LoadFunction, LoadResult} from "../../types/loader.ts"; + +// utilities +import {makeInlineId} from "../../loader/loader.ts"; +import {HtmlImport} from "../../types/html-module.ts"; + + +export interface RollupHtmlTransformContext { + id?: string; + // bundle: OutputBundle; + // files: Record; +} +export type TransformCallback = (source: string, transformContext: RollupHtmlTransformContext) => string|Promise; + + +export function transform({ + virtualSources, + filter, + entryNames, + pluginName, + transform, + load, + }: { + virtualSources: Map, + filter: ReturnType, + entryNames: Map, + pluginName: string, + transform: TransformCallback, + load: LoadNodeCallback +}): Plugin['transform']{ + return { + order: 'pre', + async handler(...args){ + const [code, id] = args; + if (!filter(id)) return; + + // parse + const moduleInfo = this.getModuleInfo(id); + const moduleMeta = moduleInfo!.meta ?? {}; + let htmlModule = moduleMeta[pluginName]; + if(!htmlModule){ + const moduleExt = extname(id); + const moduleName = id.replace(new RegExp(`${moduleExt}\$`),''); // strip extension of the name if any + htmlModule = moduleMeta[pluginName] = { + id: id, + name: moduleName, + imports: [], + assetId: null, + importers: new Set(), + } + } + const contents = code; + + const htmlSrc = transform ? await transform(contents, { + id, + }) : contents; + + // Parse document and store it + const document = htmlModule.document = parseHtml(htmlSrc); + + // TODO working on this: to preserve sourcemaps as much as possible we're starting the magic string on the raw html source + // question is if we need to though. sourcemaps only make sense for inlined bits of code + //let htmlJS = new MagicString(htmlSrc);// This is where we want to go! + + // Figure out which references to load from this HTML by iterating all nodes (looking for src or href attributes) + let htmlImports: HtmlImport[] = htmlModule.imports = []; + if (document.childNodes) { + let nodeQueue = document.childNodes; + do { + const nextQueue: DefaultTreeAdapterMap['childNode'][][] = []; + await Promise.all(nodeQueue.map(async (node) => { + const el = (node); + const loadFunction: LoadFunction = async ({ + id: sourceId, + source, + type + })=>{ + if(!sourceId){ + sourceId = makeInlineId(id, node, 'js'); + } + if(source){ + virtualSources.set(sourceId, source); + } + const resolved = await this.resolve(sourceId, id, { + skipSelf: false, // defaults to true since rollup 4, and for virtual files this is problematic + isEntry: type==='entryChunk', + }); + if(!resolved){ + throw new Error(`Could not resolve ${sourceId} from ${id}`); + } + + const selfInfo = this.getModuleInfo(id); + + let entryName: string|undefined = undefined; + const parentName = entryNames.get(id)??selfInfo?.meta[pluginName].name; + if(type==='entryChunk'){ + entryName= posix.join(posix.dirname(parentName),sourceId); + entryName = entryName.slice(0,-(posix.extname(entryName).length)); // Cut off the extension (TODO, is this wise?) + } + + const importName = (source && selfInfo?.meta[pluginName].name) + ? makeInlineId(parentName, node, extname(sourceId)) + : entryName; + + const htmlImport: HtmlImport = { + id: sourceId, + resolved: resolved, + // loaded: loaded, + node: el, + type, + source, + referenceId: + (resolved && (['chunk','entryChunk'].includes(type!))) ? this.emitFile({ + type: 'chunk', // Might want to adapt, or make configurable (see LoadType) + id: resolved.id, + name: importName, + importer: id, + }) : null, + placeholder: `html-import-${crypto.randomBytes(32).toString('base64')}`, + index: htmlImports.length, + } + // if(entryName){ + // addedEntries.set(resolved.id, entryName);// (we could do this using meta?) + // } + htmlImports.push(htmlImport); + return htmlImport.placeholder; + } + + let toLoad: LoadResult | undefined = load? await Promise.resolve(load({ + node: el, + sourceId: id + }, loadFunction)) : undefined; + + if (toLoad !== false) { + let asParent = (node); + if (asParent.childNodes) { + nextQueue.push(asParent.childNodes); + } + } + })); + nodeQueue = nextQueue.flat(); + } while (nodeQueue.length > 0); + } + + // Beware leak of AST (we're starting MagicString on a parsed and modified version of the HTML file, sourcemappings in the HTML file will be off. (can't add a sourcemap for a html file anyway, unless it is outputted as JS module) + // @ts-ignore + let htmlJS = new MagicString(serializeHtml(htmlModule.document)); + htmlJS.replaceAll(/`/g,'\\\`').replaceAll(/\$\{/g,'\\${'); + + const moduleImports = []; + for(const htmlImport of htmlImports){ + if(htmlImport.type === 'default') { + const assetId: string = `asset${moduleImports.length}`; + moduleImports.push(`import ${assetId} from "${htmlImport.id}";`);// TODO: This is just the easy & safe solution. Would prefer to have recognizable names, and reeuse when something is the exact same resource.. + htmlJS = htmlJS.replace(htmlImport.placeholder, `\${${assetId}}`);// TODO: Should we be worried about windows absolute URLs here? + }else{ + // TODO: this will probably not do for complicated cases ( presumably no other method then emitting the chunk as file, loading its result but excluding it from the output bundle) + // html = html.replace(htmlImport.placeholder, htmlImport.loaded?.code||htmlImport.source||''); + } + } + + // Import all dependencies and wrap the HTML in a `...`, assign to a var and export (escaping any ` characters in the HTML) + htmlJS.prepend([ + ...moduleImports, + `export const html = \`` + ].join('\n')).append([ + `\`;`, + `export default html;`, + ].join('\n')); + + const map = htmlJS.generateMap({ + source: id, + file: `${id}.map`, + includeContent: true, + hires: 'boundary' + }); + + return { + code: htmlJS.toString(), + map: map.toString(), + meta: moduleMeta, + }; + } + } +} diff --git a/src/plugin/html.ts b/src/plugin/html.ts index b71f559..a012531 100644 --- a/src/plugin/html.ts +++ b/src/plugin/html.ts @@ -1,11 +1,6 @@ import type { Plugin, OutputBundle, - OutputChunk, - OutputAsset, - NormalizedOutputOptions, - // ModuleInfo, - ResolvedId, PreRenderedChunk, RenderedChunk, } from 'rollup'; @@ -13,37 +8,19 @@ import type { // createFilter function is a utility that constructs a filter function from include/exclude patterns. import {createFilter} from '@rollup/pluginutils'; import type {FilterPattern} from "@rollup/pluginutils"; -// parse5 package is used for parsing HTML. -import {parse as parseHtml, serialize as serializeHtml, DefaultTreeAdapterMap} from "parse5"; -// magic-string to transform code and keeping a sourcemap aligned -import MagicString from "magic-string"; -// nodejs imports (io, path) -import path, { extname, dirname } from "node:path"; -import {readFile} from "node:fs/promises" -import posix from "node:path/posix"; -import crypto from "node:crypto"; - // utilities -import {makeLoader, makeInlineId} from "../loader/loader.ts"; -import {HtmlImport, HtmlModule} from "../types/html-module.ts"; +import {makeLoader} from "../loader/loader.ts"; -import type {LoadNodeCallback, LoadFunction, LoadResult} from "../types/loader.ts"; +import type {LoadNodeCallback} from "../types/loader.ts"; import type {ResolveCallback} from "../types/resolve.d.ts"; +import * as hooks from "./hooks/index.ts"; +import type {TransformCallback} from "./hooks/transform.ts"; +import type {RewriteUrlCallback} from "./hooks/generate-bundle.ts"; -export interface RollupHtmlTransformContext { - id?: string; - // bundle: OutputBundle; - // files: Record; -} - -export interface RewriteUrlCallbackContext { - from: string; - rootPath: string; -} -export type RewriteUrlCallback = (relative: string, context: RewriteUrlCallbackContext) => string|Promise; -export type TransformCallback = (source: string, transformContext: RollupHtmlTransformContext) => string|Promise; +export type {TransformCallback,RollupHtmlTransformContext} from "./hooks/transform.ts"; +export type {RewriteUrlCallback, RewriteUrlCallbackContext} from "./hooks/generate-bundle.ts"; const modulePrefix = `// `; const moduleSuffix = `// `; @@ -163,205 +140,32 @@ export function html(opts: { // Track html entrypoints buildStart(options){ - entryNames = new Map(Object.entries(typeof(options.input)==='object'?options.input:{[options.input]:[options.input]}) - .map(([k,v])=>[v,k]) - ); - }, - - resolveId: { - async handler(specifier, - importer, - options){ - if(virtualSources.has(specifier)) return specifier; // Resolve virtual sources we added - if(!filter(specifier)) return; - - // Let it be resolved like others (node_modules, project aliases, ..) - const resolved = await this.resolve(specifier, importer, { - skipSelf: true, - ...options, - }); - - if(resolved){ - const moduleExt = extname(resolved.id); - const moduleName = specifier.replace(new RegExp(`${moduleExt}\$`),''); // strip extension of the name if any - - return { - ...resolved, - meta: { - ...resolved.meta, - [pluginName]: { - specifier: specifier, - id: resolved.id, - name: moduleName, - imports: [], - assetId: null, - importers: new Set(), - } - } - } - } + entryNames.clear(); + const entries = Object.entries(typeof(options.input)==='object'?options.input:{[options.input]:[options.input]}) + .map(([k,v])=>[v,k]); + for(const [k,v] of entries){ + entryNames.set(k,v); } }, + + resolveId: hooks.resolveId(({ + virtualSources, + filter, + pluginName + })), load: { async handler(id: string) { if (virtualSources.has(id)) return virtualSources.get(id); - // if (!filter(id)) return; - } - }, - transform: { - order: 'pre', - async handler(...args){ - const [code, id] = args; - if (!filter(id)) return; - - // parse - const moduleInfo = this.getModuleInfo(id); - const moduleMeta = moduleInfo!.meta ?? {}; - let htmlModule = moduleMeta[pluginName]; - if(!htmlModule){ - const moduleExt = extname(id); - const moduleName = id.replace(new RegExp(`${moduleExt}\$`),''); // strip extension of the name if any - htmlModule = moduleMeta[pluginName] = { - id: id, - name: moduleName, - imports: [], - assetId: null, - importers: new Set(), - } - } - const contents = code; - - const htmlSrc = transform ? await transform(contents, { - id, - }) : contents; - - // Parse document and store it - const document = htmlModule.document = parseHtml(htmlSrc); - - // TODO working on this: to preserve sourcemaps as much as possible we're starting the magic string on the raw html source - // question is if we need to though. sourcemaps only make sense for inlined bits of code - //let htmlJS = new MagicString(htmlSrc);// This is where we want to go! - - // Figure out which references to load from this HTML by iterating all nodes (looking for src or href attributes) - let htmlImports: HtmlImport[] = htmlModule.imports = []; - if (document.childNodes) { - let nodeQueue = document.childNodes; - do { - const nextQueue: DefaultTreeAdapterMap['childNode'][][] = []; - await Promise.all(nodeQueue.map(async (node) => { - const el = (node); - const loadFunction: LoadFunction = async ({ - id: sourceId, - source, - type - })=>{ - if(!sourceId){ - sourceId = makeInlineId(id, node, 'js'); - } - if(source){ - virtualSources.set(sourceId, source); - } - const resolved = await this.resolve(sourceId, id, { - skipSelf: false, // defaults to true since rollup 4, and for virtual files this is problematic - isEntry: type==='entryChunk', - }); - if(!resolved){ - throw new Error(`Could not resolve ${sourceId} from ${id}`); - } - - const selfInfo = this.getModuleInfo(id); - - let entryName: string|undefined = undefined; - const parentName = entryNames.get(id)??selfInfo?.meta[pluginName].name; - if(type==='entryChunk'){ - entryName= posix.join(posix.dirname(parentName),sourceId); - entryName = entryName.slice(0,-(posix.extname(entryName).length)); // Cut off the extension (TODO, is this wise?) - } - - const importName = (source && selfInfo?.meta[pluginName].name) - ? makeInlineId(parentName, node, extname(sourceId)) - : entryName; - - const htmlImport: HtmlImport = { - id: sourceId, - resolved: resolved, - // loaded: loaded, - node: el, - type, - source, - referenceId: - (resolved && (['chunk','entryChunk'].includes(type!))) ? this.emitFile({ - type: 'chunk', // Might want to adapt, or make configurable (see LoadType) - id: resolved.id, - name: importName, - importer: id, - }) : null, - placeholder: `html-import-${crypto.randomBytes(32).toString('base64')}`, - index: htmlImports.length, - } - // if(entryName){ - // addedEntries.set(resolved.id, entryName);// (we could do this using meta?) - // } - htmlImports.push(htmlImport); - return htmlImport.placeholder; - } - - let toLoad: LoadResult | undefined = load? await Promise.resolve(load({ - node: el, - sourceId: id - }, loadFunction)) : undefined; - - if (toLoad !== false) { - let asParent = (node); - if (asParent.childNodes) { - nextQueue.push(asParent.childNodes); - } - } - })); - nodeQueue = nextQueue.flat(); - } while (nodeQueue.length > 0); - } - - // Beware leak of AST (we're starting MagicString on a parsed and modified version of the HTML file, sourcemappings in the HTML file will be off. (can't add a sourcemap for a html file anyway, unless it is outputted as JS module) - // @ts-ignore - let htmlJS = new MagicString(serializeHtml(htmlModule.document)); - htmlJS.replaceAll(/`/g,'\\\`').replaceAll(/\$\{/g,'\\${'); - - const moduleImports = []; - for(const htmlImport of htmlImports){ - if(htmlImport.type === 'default') { - const assetId: string = `asset${moduleImports.length}`; - moduleImports.push(`import ${assetId} from "${htmlImport.id}";`);// TODO: This is just the easy & safe solution. Would prefer to have recognizable names, and reeuse when something is the exact same resource.. - htmlJS = htmlJS.replace(htmlImport.placeholder, `\${${assetId}}`);// TODO: Should we be worried about windows absolute URLs here? - }else{ - // TODO: this will probably not do for complicated cases ( presumably no other method then emitting the chunk as file, loading its result but excluding it from the output bundle) - // html = html.replace(htmlImport.placeholder, htmlImport.loaded?.code||htmlImport.source||''); - } - } - - // Import all dependencies and wrap the HTML in a `...`, assign to a var and export (escaping any ` characters in the HTML) - htmlJS.prepend([ - ...moduleImports, - `export const html = \`` - ].join('\n')).append([ - `\`;`, - `export default html;`, - ].join('\n')); - - const map = htmlJS.generateMap({ - source: id, - file: `${id}.map`, - includeContent: true, - hires: 'boundary' - }); - - return { - code: htmlJS.toString(), - map: map.toString(), - meta: moduleMeta, - }; } }, + transform: hooks.transform({ + virtualSources, + filter, + entryNames, + pluginName, + transform, + load + }), outputOptions(options){ return { ...options, @@ -407,113 +211,12 @@ export function html(opts: { return ''; } }, - async generateBundle(outputOptions, bundles){ - const bundleItems = Object.entries(bundles); - const virtualBundles = new Set(); - const facadeToChunk = new Map(); - const htmlResults = new Map(); - - for(const [bundleName, bundle] of bundleItems) { - const chunk = (bundle); - if(chunk.facadeModuleId) { - facadeToChunk.set(chunk.facadeModuleId, chunk); - - const moduleInfo = this.getModuleInfo(chunk.facadeModuleId); - const htmlModule = moduleInfo?.meta?.[pluginName]; - // const htmlModule = htmlModules.get(chunk.facadeModuleId); - - if(htmlModule){ htmlResults.set(bundleName, {chunk, htmlModule})} - else if(virtualSources.has(chunk.facadeModuleId)){ - virtualBundles.add(bundleName); - } - } - } - - for(const [bundleName, {chunk, htmlModule}] of htmlResults.entries()){ - if(htmlModule. document) { - // Delete the placeholder chunk from the bundle and emit an asset file for the HTML instead. - deleteFromBundle(bundleName, bundles); - - // Interpret the module and take its default export (TODO: if [NodeJS vm SourceTextModule](https://nodejs.org/api/vm.html#class-vmsourcetextmodule) ever lands, it would be cleaner to use that one instead of directly importing it) - let htmlContents: string; - - // Take out the sourceMapUrl if any (it will not have been written yet and tends to cause a crash, we don't need it anyway)) - let sanitizedCode = chunk.code; - - // Use the modulePrefix to filter out prepended code that is not relevant for us (like live-reload) - const moduleStart = sanitizedCode.indexOf(modulePrefix); - if(moduleStart>=0){ - sanitizedCode = sanitizedCode.slice(moduleStart+modulePrefix.length); - } - // Filter out any sourceMapping url that may have been added - const sourceMapRE = /\/\/# sourceMappingURL=(.+)/.exec(sanitizedCode); - if(sourceMapRE){ - sanitizedCode = sanitizedCode.slice(0,sourceMapRE.index)+sanitizedCode.slice(sourceMapRE.index+sourceMapRE[0].length); - } - - // Encode into a url that we can import(...) - // const importUrl = `data:text/javascript;base64,${Buffer.from(sanitizedCode).toString('base64')}`; // Safer, but unrecognizable if this throws an error - const importUrl = `data:text/javascript,${encodeURIComponent(sanitizedCode)}`; // Due to needed encoding still hard to read, but it might at least be recognizable by the user if it throws an error - - try{ - let htmlJsModule = await import(importUrl); - htmlContents = htmlJsModule.default; - }catch(err){ - throw new Error([ - `Failed to parse resulting HTML-module. Most likely this is due to a plugin that has altered the module in such a way that we cannot easely evaluate it in NodeJS.`, - `The code we tried to evaluate:`, - sanitizedCode.split('\n').map(x=>` ${x}`).join('\n'), - `The error we got:`, - err - ].join('\n')) - // TODO: We could try to fallback as follows, but the issues are likely to persist in the end result - // for(const htmlImport of htmlModule.imports){ - // if(htmlImport.referenceId) { - // const fileName = this.getFileName(htmlImport.referenceId); - // htmlImport.reference.set(fileName); - // } - // } - // serialized = serializeHtml(htmlModule.document); - } - - // Inject the inlined chunks (TODO cleanup) - for(const htmlImport of htmlModule.imports){ - const importResult = facadeToChunk.get(htmlImport.resolved?.id!); - if(importResult){ - if(htmlImport.type === 'chunk') { - htmlContents = htmlContents.replace(htmlImport.placeholder, importResult.code); - }else if(htmlImport.type === 'entryChunk'){ - const relPath = posix.relative(dirname(chunk.fileName), importResult.fileName); - const rootPath = path.posix.join(dirname(chunk.fileName), relPath); - const rewritten = rewriteUrl? await Promise.resolve(rewriteUrl(relPath, { - from: chunk.fileName, - rootPath, - })): relPath; - htmlContents = htmlContents.replace(htmlImport.placeholder, rewritten); - } - } - } - - this.emitFile({ - type: 'asset', - name: htmlModule.name, - fileName: chunk.fileName, - source: htmlContents, - }); - }else{ - throw new Error('something went wrong...'); - } - } - for( const bundleName of virtualBundles.keys()){ - deleteFromBundle(bundleName, bundles, false); - } - } + generateBundle: hooks.generateBundle({ + virtualSources, + pluginName, + modulePrefix, + rewriteUrl, + }) }; } -function deleteFromBundle(bundlename: string, bundle: OutputBundle, deleteMap: boolean = true){ - delete bundle[bundlename]; - if(deleteMap) { - delete bundle[`${bundlename}.map`];// Also delete any generated map files because they don't make any sense. (TODO: there seems to be no better way to detect this?) - } -}