wip: reorganize plugin into its hooks

This commit is contained in:
Miel Truyen 2024-03-27 17:23:02 +01:00
parent 93f99c732f
commit 2d4d097560
5 changed files with 428 additions and 329 deletions

View File

@ -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<string>;
export function generateBundle({
virtualSources,
pluginName, modulePrefix, rewriteUrl
}: {
virtualSources: Map<string, string>,
pluginName: string,
modulePrefix: string,
rewriteUrl?: RewriteUrlCallback
}): Plugin['generateBundle']{
return {
async handler(outputOptions, bundles){
const bundleItems = Object.entries(bundles);
const virtualBundles = new Set<string>();
const facadeToChunk = new Map<string,OutputChunk>();
const htmlResults = new Map<string, {chunk: OutputChunk, htmlModule: HtmlModule}>();
for(const [bundleName, bundle] of bundleItems) {
const chunk = (<OutputChunk>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?)
}
}

View File

@ -0,0 +1,3 @@
export * from "./resolve-id.ts";
export * from "./transform.ts";
export * from "./generate-bundle.ts";

View File

@ -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<string, string>,
filter: ReturnType<typeof createFilter>,
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(),
}
}
}
}
}
}
}

View File

@ -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<string, (OutputChunk | OutputAsset)[]>;
}
export type TransformCallback = (source: string, transformContext: RollupHtmlTransformContext) => string|Promise<string>;
export function transform({
virtualSources,
filter,
entryNames,
pluginName,
transform,
load,
}: {
virtualSources: Map<string, string>,
filter: ReturnType<typeof createFilter>,
entryNames: Map<string,string>,
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 = (<DefaultTreeAdapterMap['element']>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: <string>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 = (<DefaultTreeAdapterMap['parentNode']>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,
};
}
}
}

View File

@ -1,11 +1,6 @@
import type { import type {
Plugin, Plugin,
OutputBundle, OutputBundle,
OutputChunk,
OutputAsset,
NormalizedOutputOptions,
// ModuleInfo,
ResolvedId,
PreRenderedChunk, PreRenderedChunk,
RenderedChunk, RenderedChunk,
} from 'rollup'; } from 'rollup';
@ -13,37 +8,19 @@ import type {
// createFilter function is a utility that constructs a filter function from include/exclude patterns. // createFilter function is a utility that constructs a filter function from include/exclude patterns.
import {createFilter} from '@rollup/pluginutils'; import {createFilter} from '@rollup/pluginutils';
import type {FilterPattern} 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 // utilities
import {makeLoader, makeInlineId} from "../loader/loader.ts"; import {makeLoader} from "../loader/loader.ts";
import {HtmlImport, HtmlModule} from "../types/html-module.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 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 { export type {TransformCallback,RollupHtmlTransformContext} from "./hooks/transform.ts";
id?: string; export type {RewriteUrlCallback, RewriteUrlCallbackContext} from "./hooks/generate-bundle.ts";
// bundle: OutputBundle;
// files: Record<string, (OutputChunk | OutputAsset)[]>;
}
export interface RewriteUrlCallbackContext {
from: string;
rootPath: string;
}
export type RewriteUrlCallback = (relative: string, context: RewriteUrlCallbackContext) => string|Promise<string>;
export type TransformCallback = (source: string, transformContext: RollupHtmlTransformContext) => string|Promise<string>;
const modulePrefix = `// <html-module>`; const modulePrefix = `// <html-module>`;
const moduleSuffix = `// </html-module>`; const moduleSuffix = `// </html-module>`;
@ -163,205 +140,32 @@ export function html(opts: {
// Track html entrypoints // Track html entrypoints
buildStart(options){ buildStart(options){
entryNames = new Map(Object.entries(typeof(options.input)==='object'?options.input:{[options.input]:[options.input]}) entryNames.clear();
.map(([k,v])=>[v,k]) 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: {
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(),
}
}
}
}
} }
}, },
resolveId: hooks.resolveId(({
virtualSources,
filter,
pluginName
})),
load: { load: {
async handler(id: string) { async handler(id: string) {
if (virtualSources.has(id)) return virtualSources.get(id); 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 = (<DefaultTreeAdapterMap['element']>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: <string>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 = (<DefaultTreeAdapterMap['parentNode']>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){ outputOptions(options){
return { return {
...options, ...options,
@ -407,113 +211,12 @@ export function html(opts: {
return ''; return '';
} }
}, },
async generateBundle(outputOptions, bundles){ generateBundle: hooks.generateBundle({
const bundleItems = Object.entries(bundles); virtualSources,
const virtualBundles = new Set<string>(); pluginName,
const facadeToChunk = new Map<string,OutputChunk>(); modulePrefix,
const htmlResults = new Map<string, {chunk: OutputChunk, htmlModule: HtmlModule}>(); rewriteUrl,
})
for(const [bundleName, bundle] of bundleItems) {
const chunk = (<OutputChunk>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?)
}
}