diff --git a/README.md b/README.md index b331a6d..accb96c 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ See also the ToDo-list at the end of the [changelog](./CHANGELOG.md) Once publicly released, the intent is to move the GIT-repository to github. Until that day though, it exists privately on this gitea server and corresponding npm-registry [npm.cerxes.net](https://npm.cerxes.net).\ TODO: change the links once this happens ## Prior work - - [Vite](https://vitejs.dev) seems to have already [done work])(https://github.com/vitejs/vite/blob/main/packages/vite/src/node/plugins/html.ts) to handle HTML in rollup. + - [Vite](https://vitejs.dev) seems to have already [done work](https://github.com/vitejs/vite/blob/main/packages/vite/src/node/plugins/html.ts) to handle HTML in rollup. - [rollup-plugin-html-entry](https://www.npmjs.com/package/rollup-plugin-html-entry) seems to be **dead**. Last version from 2020, there have been many changes to rollup`s plugin capabilities since then - [@rollup/plugin-html](https://www.npmjs.com/package/@rollup/plugin-html) is where this project was originally forked from. Its main focus was to generate an HTML to serve the resulting bundle. Which is different from supporting HTML as entry point since it did not resolve assets used in the HTML. Besides the project setup, not much of the original has been kept... - [@web/rollup-plugin-html](https://www.npmjs.com/package/@web/rollup-plugin-html) a plugin with similar intentions as this one (in active development anno 2023). diff --git a/package.json b/package.json index 210a3ee..5a4c1f5 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,9 @@ "rollup-plugin-livereload": "^2.0.5", "rollup-plugin-postcss": "^4.0.2", "ts-jest": "^29.1.2", - "typescript": "^5.4.3" + "typescript": "^5.4.3", + "typedoc": "^0.25.12", + "pretty-format": "^29.7.0" }, "types": "./types/index.d.ts", "jest": { @@ -104,6 +106,7 @@ "ci:lint": "pnpm build && pnpm lint-staged", "ci:test": "pnpm test -- --verbose", "test": "NODE_OPTIONS='--experimental-vm-modules' jest", - "save-test": "NODE_OPTIONS='--experimental-vm-modules' jest -u" + "save-test": "NODE_OPTIONS='--experimental-vm-modules' jest -u", + "docs": "typedoc src/index.ts --sort visibility --sort required-first --sort source-order" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1983693..e6a1de8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,9 @@ devDependencies: postcss: specifier: ^8.4.38 version: 8.4.38 + pretty-format: + specifier: ^29.7.0 + version: 29.7.0 puppeteer: specifier: ^21.11.0 version: 21.11.0(typescript@5.4.3) @@ -115,6 +118,9 @@ devDependencies: ts-jest: specifier: ^29.1.2 version: 29.1.2(@babel/core@7.24.3)(jest@29.7.0)(typescript@5.4.3) + typedoc: + specifier: ^0.25.12 + version: 0.25.12(typescript@5.4.3) typescript: specifier: ^5.4.3 version: 5.4.3 @@ -2154,6 +2160,10 @@ packages: engines: {node: '>=12'} dev: true + /ansi-sequence-parser@1.1.1: + resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -4187,6 +4197,10 @@ packages: hasBin: true dev: true + /jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + dev: true + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -4349,6 +4363,10 @@ packages: engines: {node: '>=12'} dev: true + /lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + dev: true + /magic-string@0.30.8: resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} @@ -4389,6 +4407,12 @@ packages: engines: {node: '>=8'} dev: true + /marked@4.3.0: + resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} + engines: {node: '>= 12'} + hasBin: true + dev: true + /mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} dev: true @@ -4468,6 +4492,13 @@ packages: brace-expansion: 2.0.1 dev: true + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -5612,6 +5643,15 @@ packages: engines: {node: '>=8'} dev: true + /shiki@0.14.7: + resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==} + dependencies: + ansi-sequence-parser: 1.1.1 + jsonc-parser: 3.2.1 + vscode-oniguruma: 1.7.0 + vscode-textmate: 8.0.0 + dev: true + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true @@ -6013,6 +6053,20 @@ packages: is-typedarray: 1.0.0 dev: true + /typedoc@0.25.12(typescript@5.4.3): + resolution: {integrity: sha512-F+qhkK2VoTweDXd1c42GS/By2DvI2uDF4/EpG424dTexSHdtCH52C6IcAvMA6jR3DzAWZjHpUOW+E02kyPNUNw==} + engines: {node: '>= 16'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x + dependencies: + lunr: 2.3.9 + marked: 4.3.0 + minimatch: 9.0.3 + shiki: 0.14.7 + typescript: 5.4.3 + dev: true + /typescript@5.4.3: resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==} engines: {node: '>=14.17'} @@ -6106,6 +6160,14 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /vscode-oniguruma@1.7.0: + resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} + dev: true + + /vscode-textmate@8.0.0: + resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + dev: true + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: diff --git a/src/index.ts b/src/index.ts index fd80e2a..cd9bafa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,464 +1,19 @@ +export * from "./plugin/index.ts"; +import {html} from "./plugin/index.ts"; +export default html; -import type { - Plugin, - OutputBundle, - OutputChunk, - OutputAsset, - NormalizedOutputOptions, - // ModuleInfo, - ResolvedId, - PreRenderedChunk, - RenderedChunk, -} from 'rollup'; +export type { + RewriteUrlCallback, RewriteUrlCallbackContext, + TransformCallback, RollupHtmlTransformContext, +} from "./plugin/html.ts" -import type { - LoadResult, - RollupHtmlOptions, - LoadNodeCallback, - LoadReference, BodyReference, AttributeReference, LoadFunction -} from '../types/index.d.ts'; +export type { + LoadNodeCallback, LoadResult, LoadReference, LoadedReference, LoadFunction, + LoadType, + RollupHtmlLoadContext, BodyReference, AttributeReference, -// createFilter function is a utility that constructs a filter function from include/exclude patterns. -import {createFilter} 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"; + ResolveCallback, ResolveResult, RollupHtmlResolveContext +} from "./types/index.ts"; - -// 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.ts"; -import {HtmlImport, HtmlModule} from "./html-module.ts"; - - -const defaults: RollupHtmlOptions = { - transform: (source: string)=>source,// NO-OP - load: makeLoader(), - resolve: ()=>true, - htmlFileNames: "[name].html", - include: [ - '**/*.(html|hbs)',// html or handlebars - ] -}; - -const modulePrefix = `// `; -const moduleSuffix = `// `; - -/** - * Creates a Rollup plugin that transforms HTML files. - * - * @param {RollupHtmlOptions} opts - The options for the plugin. - * @returns {Plugin} - The Rollup plugin. - */ -export default function html(opts: RollupHtmlOptions = {}): Plugin { - const { - publicPath, - transform, - rewriteUrl, - load, - htmlFileNames, - resolve, - include, - exclude, - } = Object.assign( - {}, - defaults, - opts - ); - if(publicPath){ throw new Error("TODO, do something with the public path or throw it out of the options. this is just to stop typescript complaining")} - - let filter = createFilter(include, exclude, {}); - - // TODO, we need to clear all these properly at sme point to avoid odd bugs in watch mode - let virtualSources = new Map(); - let addedEntries = new Map(); - let entryNames = new Map(); - - const pluginName = 'html2'; // TODO: Need a better name, and work to strip everything noted below except the short summary - /** - * Short summary: - * Intercepts the loading of the html files and parses it with parse5. - * The parsed result is iterated to check for external references that need to be including in the rollup build (via for example @rollup/plugin-url). - * A .js version of the html file is returned to rollup, optionally including a few imports left for rollup to resolve - * When the result is generated the rollup result for the html file and any of its inlined assets are stripped from the output. - * and replaced with a html file. - * - * Caveats: - * - to get the resulting html content file we're evaluating the resulting JS module and take its default export - * This evaluation step is done in the host NodeJS context, which might screw up things that expect a browser context - * [warn] other plugins such as CJS transformer and hot-reload can severely screw this up. - * - to fix the naming of resulting html files, and behave properly when files are entryPoints or not... we're fighting rollup alot - * issues are likely... - * - * - * Rework by testing a stripped down version with JS imports? - * - the logic in load should be moved to a transform, properly use rollups ability to specify the plugin should - * run 'pre' other hooks and see if that allows us to intercept before a commonjs or some other tool horribly transpiles our code - * - we might need to know which output is being used to properly extract the html back from the result? (in case of not being included in a JS file) - */ - return { - name: pluginName, - - // 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; - 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(), - } - } - } - } - } - }, - 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, - }; - } - }, - outputOptions(options){ - return { - ...options, - entryFileNames: (chunkInfo)=>{ - const moduleInfo = chunkInfo.facadeModuleId? this.getModuleInfo(chunkInfo.facadeModuleId) : null; - const htmlModule = moduleInfo?.meta?.[pluginName]; - // const htmlModule = chunkInfo.facadeModuleId ? htmlModules.get(chunkInfo.facadeModuleId!) : null; - const addedEntry = chunkInfo.facadeModuleId ? addedEntries.get(chunkInfo.facadeModuleId!) : null; - const defaultOption = options.entryFileNames ?? "[name]-[hash].js";// This default is copied from the docs. TODO: don't like overwrite it this way, can we remove the need for this or fetch the true default? - if(htmlModule){ - let fileName = typeof (htmlFileNames) === 'string' ? htmlFileNames : (<(chunkInfo:PreRenderedChunk)=>string>htmlFileNames)(chunkInfo); - if(fileName) { - return fileName; - } - }else if(addedEntry){ - return addedEntry; - } - return typeof (defaultOption) === 'string' ? defaultOption : (<(chunkInfo:PreRenderedChunk)=>string>defaultOption)(chunkInfo); - }, - // TODO do we need to do the same for chunks?? (what if they're dynamically imported?) - } - }, - resolveFileUrl(options){ - const moduleInfo = this.getModuleInfo(options.moduleId); - const htmlModule = moduleInfo?.meta?.[pluginName]; - if(htmlModule){ - // Simply use the relative path in our HTML-fileURLs instead of the default `new URL('${fileName}', document.baseURI).href`) - return `"${options.relativePath}"`; - } - }, - banner: { - // Injects a tag so we know where our bundle starts so we can safely ignore other stuff addded via a banner (ie. live-reload) - order:'post', - handler(chunk: RenderedChunk){ - if(chunk.facadeModuleId) { - const moduleInfo = chunk.facadeModuleId? this.getModuleInfo(chunk.facadeModuleId) : null; - const htmlModule = moduleInfo?.meta?.[pluginName]; - // const htmlModule = htmlModules.get(chunk.facadeModuleId); - if (htmlModule) { - return modulePrefix; // Overwrite any added banner with our own - } - } - 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); - } - } - }; -} - -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?) - } -} +// TODO should export loader and types (but rework it first) diff --git a/src/loader.ts b/src/loader/loader.ts similarity index 97% rename from src/loader.ts rename to src/loader/loader.ts index f107e18..47602ff 100644 --- a/src/loader.ts +++ b/src/loader/loader.ts @@ -6,10 +6,10 @@ import type { LoadReference, NodeMapping, AttributeReference, BodyReference, LoadedReference -} from '../types/index.d.ts'; +} from '../types/loader.d.ts'; import {parseFragment as parseHtmlFragment, serialize as serializeHtml, DefaultTreeAdapterMap} from "parse5"; -import {KnownMappings, defaultMapping} from "./loader-mappings.ts"; +import {KnownMappings, defaultMapping} from "./mapping.ts"; /** * Makes a unique but human-readable name from a path within a HTML file. diff --git a/src/loader-mappings.ts b/src/loader/mapping.ts similarity index 99% rename from src/loader-mappings.ts rename to src/loader/mapping.ts index f1bfd63..6b5cb37 100644 --- a/src/loader-mappings.ts +++ b/src/loader/mapping.ts @@ -2,7 +2,7 @@ import type { NodeMapping, -} from '../types/load.d.ts'; +} from '../types/loader.ts'; // TODO: specifying ext makes sense for inlined script to convey as what kind of content this should be treated as (i.e. is the inlined script JSX/Typescript/..., or the inlined style CSS/PCSS/SASS. Might be prerrable to support a 'compile-time' ext-attribute on the node) // but in the case of href/src references, it makes more sense to add it as a meta-data property (conveying how we expect it to be loaded) and the existing filename left as is. diff --git a/src/plugin/html.ts b/src/plugin/html.ts new file mode 100644 index 0000000..b71f559 --- /dev/null +++ b/src/plugin/html.ts @@ -0,0 +1,519 @@ +import type { + Plugin, + OutputBundle, + OutputChunk, + OutputAsset, + NormalizedOutputOptions, + // ModuleInfo, + ResolvedId, + PreRenderedChunk, + RenderedChunk, +} from 'rollup'; + +// 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 type {LoadNodeCallback, LoadFunction, LoadResult} from "../types/loader.ts"; +import type {ResolveCallback} from "../types/resolve.d.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; + +const modulePrefix = `// `; +const moduleSuffix = `// `; + + +/** + * Intantiates a Rollup plugin to transform HTML files., + * + */ +export function html(opts: { + publicPath?: string; + /** + * Follows the same logic as rollup's [entryFileNames](https://rollupjs.org/configuration-options/#output-entryfilenames) + */ + htmlFileNames?: string|((chunkInfo: PreRenderedChunk) => string); + + /** + * Transform a source file passed into this plugin to HTML. For example: a handlebars transform + * ```javascript + * transform(source){ + * return handlebars.compile(source)({myVar:'example'}) + * } + * ``` + */ + transform?: TransformCallback; + + /** + * Optional callback to rewrite how resources are referenced in the output HTML. + * For example to rewrite urls to paths from the root of your website: + * ```javascript + * rewriteUrl(relative, {rootPath, from}){ + * return `/${rootPath}`; + * } + * ``` + */ + rewriteUrl?: RewriteUrlCallback; + + /** + * Detect which references (``, ``, ``,...) to resolve from + * an HTML node. + * This rarely needs to be overloaded, but can be used to support non-native attributes used by custom-elements. + * + * Return false to skip any further processing on this node. \ + * Use the load function to add any resources from this node, and replace the import with a placeholder so the plugin + * knows where to inject the end result + */ + load?: LoadNodeCallback; + + /** + * Callback to filter which references actually need to be resolved. Here you can filter out: + * - Links to extensions that don't need to be handled through rollup + * - Resources that are external to the app (for example non-relative paths) + * - Page navigation within the app + * + * Return a falsey value to skip this reference. Return true to resolve as is. (or string to transform the id) + */ + resolve?: ResolveCallback; + + /** + * [Pattern](https://github.com/micromatch/picomatch#globbing-features) to include. + * For example: `['**\/*.(html|hbs)']` to include html and handblebars files. + * + * Includes only .html by default + */ + include?: FilterPattern; + + /** + * [Pattern](https://github.com/micromatch/picomatch#globbing-features) to exclude + */ + exclude?: FilterPattern +} = {}): Plugin { + const { + publicPath, + transform = (source: string)=>source, // NO-OP default + rewriteUrl, + load = makeLoader(), + htmlFileNames = "[name].html", + resolve = ()=>true, + include = [ + '**/*.(html)',// html or handlebars + ], + exclude, + } = opts; + if(publicPath){ throw new Error("TODO, do something with the public path or throw it out of the options. this is just to stop typescript complaining")} + + let filter = createFilter(include, exclude, {}); + + // TODO, we need to clear all these properly at sme point to avoid odd bugs in watch mode + let virtualSources = new Map(); + let addedEntries = new Map(); + let entryNames = new Map(); + + const pluginName = 'html2'; // TODO: Need a better name, and work to strip everything noted below except the short summary + /** + * Short summary: + * Intercepts the loading of the html files and parses it with parse5. + * The parsed result is iterated to check for external references that need to be including in the rollup build (via for example @rollup/plugin-url). + * A .js version of the html file is returned to rollup, optionally including a few imports left for rollup to resolve + * When the result is generated the rollup result for the html file and any of its inlined assets are stripped from the output. + * and replaced with a html file. + * + * Caveats: + * - to get the resulting html content file we're evaluating the resulting JS module and take its default export + * This evaluation step is done in the host NodeJS context, which might screw up things that expect a browser context + * [warn] other plugins such as CJS transformer and hot-reload can severely screw this up. + * - to fix the naming of resulting html files, and behave properly when files are entryPoints or not... we're fighting rollup alot + * issues are likely... + * + * + * Rework by testing a stripped down version with JS imports? + * - the logic in load should be moved to a transform, properly use rollups ability to specify the plugin should + * run 'pre' other hooks and see if that allows us to intercept before a commonjs or some other tool horribly transpiles our code + * - we might need to know which output is being used to properly extract the html back from the result? (in case of not being included in a JS file) + */ + return { + name: pluginName, + + // 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(), + } + } + } + } + } + }, + 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, + }; + } + }, + outputOptions(options){ + return { + ...options, + entryFileNames: (chunkInfo)=>{ + const moduleInfo = chunkInfo.facadeModuleId? this.getModuleInfo(chunkInfo.facadeModuleId) : null; + const htmlModule = moduleInfo?.meta?.[pluginName]; + // const htmlModule = chunkInfo.facadeModuleId ? htmlModules.get(chunkInfo.facadeModuleId!) : null; + const addedEntry = chunkInfo.facadeModuleId ? addedEntries.get(chunkInfo.facadeModuleId!) : null; + const defaultOption = options.entryFileNames ?? "[name]-[hash].js";// This default is copied from the docs. TODO: don't like overwrite it this way, can we remove the need for this or fetch the true default? + if(htmlModule){ + let fileName = typeof (htmlFileNames) === 'string' ? htmlFileNames : (<(chunkInfo:PreRenderedChunk)=>string>htmlFileNames)(chunkInfo); + if(fileName) { + return fileName; + } + }else if(addedEntry){ + return addedEntry; + } + return typeof (defaultOption) === 'string' ? defaultOption : (<(chunkInfo:PreRenderedChunk)=>string>defaultOption)(chunkInfo); + }, + // TODO do we need to do the same for chunks?? (what if they're dynamically imported?) + } + }, + resolveFileUrl(options){ + const moduleInfo = this.getModuleInfo(options.moduleId); + const htmlModule = moduleInfo?.meta?.[pluginName]; + if(htmlModule){ + // Simply use the relative path in our HTML-fileURLs instead of the default `new URL('${fileName}', document.baseURI).href`) + return `"${options.relativePath}"`; + } + }, + banner: { + // Injects a tag so we know where our bundle starts so we can safely ignore other stuff addded via a banner (ie. live-reload) + order:'post', + handler(chunk: RenderedChunk){ + if(chunk.facadeModuleId) { + const moduleInfo = chunk.facadeModuleId? this.getModuleInfo(chunk.facadeModuleId) : null; + const htmlModule = moduleInfo?.meta?.[pluginName]; + // const htmlModule = htmlModules.get(chunk.facadeModuleId); + if (htmlModule) { + return modulePrefix; // Overwrite any added banner with our own + } + } + 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); + } + } + }; +} + +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/index.ts b/src/plugin/index.ts new file mode 100644 index 0000000..2cb95fa --- /dev/null +++ b/src/plugin/index.ts @@ -0,0 +1 @@ +export {html} from "./html.ts"; diff --git a/src/html-module.ts b/src/types/html-module.ts similarity index 97% rename from src/html-module.ts rename to src/types/html-module.ts index 0653465..8d6edc7 100644 --- a/src/html-module.ts +++ b/src/types/html-module.ts @@ -9,7 +9,7 @@ import type { import type { LoadedReference -} from "../types/load.d.ts"; +} from "./loader.d.ts"; import type {DefaultTreeAdapterMap} from "parse5"; // Internal type diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..7c8f830 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,3 @@ +export type * from "./html-module.ts"; +export type * from "./loader.ts"; +export type * from "./resolve.ts"; diff --git a/types/load.d.ts b/src/types/loader.ts similarity index 97% rename from types/load.d.ts rename to src/types/loader.ts index 9ea6be3..e86d609 100644 --- a/types/load.d.ts +++ b/src/types/loader.ts @@ -15,7 +15,7 @@ export type AttributeReference = { }; export type BodyReference = { /** - * Indiciate this is an inlined reference (node body) + * Indicate this is an inlined reference (node body) */ body: boolean; /** diff --git a/types/resolve.d.ts b/src/types/resolve.ts similarity index 100% rename from types/resolve.d.ts rename to src/types/resolve.ts diff --git a/test/basic/__snapshots__/test.js.snap b/test/basic/__snapshots__/test.js.snap index 7812f87..1eb3353 100644 --- a/test/basic/__snapshots__/test.js.snap +++ b/test/basic/__snapshots__/test.js.snap @@ -1,78 +1,108 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`basic inline-script 1`] = ` -[ - { - "code": undefined, - "fileName": "script.body.script.js.js.map", - "map": undefined, - "source": "{"version":3,"file":"script.body.script.js.js","sources":["../batman.js","../script.html.body.script.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n","\\n import {b} from \\"./batman.js\\";\\n document.body.appendChild(\\n document.createTextNode(\`Inline script including \${b()}\`)\\n );\\n "],"names":[],"mappings":"AAAO,MAAM,CAAC,GAAG,IAAI,QAAQ,CAAC;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;ACCJ,QAAQ,CAAC,IAAI,CAAC,WAAW;AACrC,gBAAgB,QAAQ,CAAC,cAAc,CAAC,CAAC,wBAAwB,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AACzE,aAAa"}", - }, - { - "code": undefined, - "fileName": "script.html", - "map": undefined, - "source": " - - - - - -", - }, -] + ############### + # script.html # + ############### + + + + + + + + + ################################ + # script.body.script.js.js.map # + ################################ + {"version":3,"file":"script.body.script.js.js","sources":["../batman.js","../script.html.body.script.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n","\\n import {b} from \\"./batman.js\\";\\n document.body.appendChild(\\n document.createTextNode(\`Inline script including \${b()}\`)\\n );\\n "],"names":[],"mappings":"AAAO,MAAM,CAAC,GAAG,IAAI,QAAQ,CAAC;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;ACCJ,QAAQ,CAAC,IAAI,CAAC,WAAW;AACrC,gBAAgB,QAAQ,CAAC,cAAc,CAAC,CAAC,wBAAwB,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AACzE,aAAa"} + ################# + # RENDERED HTML # + ################# + + + + + + + + Inline script including batman + ########### + # CONSOLE # + ########### + [log] batman + [error] Failed to load resource: the server responded with a status of 404 (Not Found) + ############# + # RESPONSES # + ############# + 200 http://localhost/script.html + 404 http://localhost/favicon.ico `; exports[`basic simple 1`] = ` -[ - { - "code": "const b = ()=>'batman'; -console.log(b()); - -export { b }; -//# sourceMappingURL=batman.js.map -", - "fileName": "batman.js", - "map": SourceMap { - "file": "batman.js", - "mappings": "AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;", - "names": [], - "sources": [ - "../batman.js", - ], - "sourcesContent": [ - "export const b = ()=>'batman'; -console.log(b()); -", - ], - "version": 3, - }, - "source": undefined, - }, - { - "code": undefined, - "fileName": "batman.js.map", - "map": undefined, - "source": "{"version":3,"file":"batman.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"}", - }, - { - "code": undefined, - "fileName": "index.html", - "map": undefined, - "source": " - - - - - -", - }, -] + ############## + # index.html # + ############## + + + + + + + + ############# + # batman.js # + ############# + const b = ()=>'batman'; + console.log(b()); + + export { b }; + //# sourceMappingURL=batman.js.map + + ################# + # batman.js.map # + ################# + {"version":3,"file":"batman.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"} + ################# + # RENDERED HTML # + ################# + + + + + + + + ########### + # CONSOLE # + ########### + [log] batman + ############# + # RESPONSES # + ############# + 200 http://localhost/index.html + 200 http://localhost/batman.js + 200 http://localhost/favicon.ico `; diff --git a/test/basic/fixtures/script.html b/test/basic/fixtures/script.html index 22edbac..4342c62 100644 --- a/test/basic/fixtures/script.html +++ b/test/basic/fixtures/script.html @@ -1,5 +1,10 @@ + - - -", - "requestsFailed": [], - "responses": [ - "200 http://localhost/index.html", - "200 http://localhost/index.js", - "200 http://localhost/app.js", - ], -} + ############## + # index.html # + ############## + + + + Test bundle! + + + + + + I'm cool! + + +
Here the app should load!
+ + + + + ########## + # app.js # + ########## + async function app({root}){ + + const states = ['started', 'tick', 'ended']; + + for(let state of states){ + const text = \`App \${state}\`; + console.log(\`Test my sourcemap: \${text}\`); + root.innerHTML = \`
\${text}
\`; + await new Promise((resolve,reject)=> + setTimeout(()=>resolve(), 10) + ); + } + } + + export { app }; + //# sourceMappingURL=app.js.map + + ############## + # app.js.map # + ############## + {"version":3,"file":"app.js","sources":["../app.mjs"],"sourcesContent":["export async function app({root}){\\n\\n const states = ['started', 'tick', 'ended'];\\n\\n for(let state of states){\\n const text = \`App \${state}\`;\\n console.log(\`Test my sourcemap: \${text}\`);\\n root.innerHTML = \`
\${text}
\`;\\n await new Promise((resolve,reject)=>\\n setTimeout(()=>resolve(), 10)\\n );\\n }\\n}\\n"],"names":[],"mappings":"AAAO,eAAe,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;AACjC;AACA,IAAI,MAAM,MAAM,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAChD;AACA,IAAI,IAAI,IAAI,KAAK,IAAI,MAAM,CAAC;AAC5B,QAAQ,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;AACpC,QAAQ,OAAO,CAAC,GAAG,CAAC,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AAClD,QAAQ,IAAI,CAAC,SAAS,GAAG,CAAC,mCAAmC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AAChF,QAAQ,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM;AACzC,YAAY,UAAU,CAAC,IAAI,OAAO,EAAE,EAAE,EAAE,CAAC;AACzC,SAAS,CAAC;AACV,KAAK;AACL;;;;"} + ############ + # index.js # + ############ + // Dynamically loads libraries and bootstraps the application + (async ()=>{ + // Add a loader here if any + const root = document.getElementById('root'); + if(root) root.innerHTML= \`
My app has loaded!!
\`; + + try { + // Load app + const [ + appModule, + ] = await Promise.all([ + import('./app.js'), + ]); + + console.log("Bootstrapped, ready to go!"); + + // Wait for DOM to be ready + if(document.readyState === 'loading') { + await new Promise((resolve)=>document.addEventListener('DOMContentLoaded', resolve)); + } + + // Start the app! + await appModule.app({root}); + }catch(err){ + console.error(err); + } + })(); + //# sourceMappingURL=index.js.map + + ################ + # index.js.map # + ################ + {"version":3,"file":"index.js","sources":["../index.mjs"],"sourcesContent":["// Dynamically loads libraries and bootstraps the application\\n(async ()=>{\\n // Add a loader here if any\\n const root = document.getElementById('root')\\n if(root) root.innerHTML= \`
My app has loaded!!
\`;\\n\\n try {\\n // Load app\\n const [\\n appModule,\\n ] = await Promise.all([\\n import(\\"./app.mjs\\"),\\n ]);\\n\\n console.log(\\"Bootstrapped, ready to go!\\");\\n\\n // Wait for DOM to be ready\\n if(document.readyState === 'loading') {\\n await new Promise((resolve)=>document.addEventListener('DOMContentLoaded', resolve));\\n }\\n\\n // Start the app!\\n await appModule.app({root});\\n }catch(err){\\n console.error(err);\\n }\\n})()\\n"],"names":[],"mappings":"AAAA;AACA,CAAC,UAAU;AACX;AACA,IAAI,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,EAAC;AAChD,IAAI,GAAG,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,yDAAyD,CAAC,CAAC;AACzF;AACA,IAAI,IAAI;AACR;AACA,QAAQ,MAAM;AACd,YAAY,SAAS;AACrB,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;AAC9B,YAAY,OAAO,UAAW,CAAC;AAC/B,SAAS,CAAC,CAAC;AACX;AACA,QAAQ,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;AAClD;AACA;AACA,QAAQ,GAAG,QAAQ,CAAC,UAAU,KAAK,SAAS,EAAE;AAC9C,YAAY,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,GAAG,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC,CAAC;AACjG,SAAS;AACT;AACA;AACA,QAAQ,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACpC,KAAK,MAAM,GAAG,CAAC;AACf,QAAQ,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC3B,KAAK;AACL,CAAC"} + ################# + # RENDERED HTML # + ################# + + + + Test bundle! + + + + + + I'm cool! + + +
App ended
+ + + + + ########### + # CONSOLE # + ########### + [log] Bootstrapped, ready to go! + [log] Test my sourcemap: App started + [log] Test my sourcemap: App tick + [log] Test my sourcemap: App ended + ############# + # RESPONSES # + ############# + 200 http://localhost/index.html + 200 http://localhost/index.js + 200 http://localhost/app.js `; diff --git a/test/evaluated-web-bundle/test.js b/test/evaluated-web-bundle/test.js index ea38d9b..74113b4 100644 --- a/test/evaluated-web-bundle/test.js +++ b/test/evaluated-web-bundle/test.js @@ -8,6 +8,7 @@ import {runBrowserTest} from "../util/browser-test.ts"; import {fileURLToPath} from "node:url"; import handlebars from "handlebars"; +import {serializer} from "../util/index.ts"; const __dirname = dirname(fileURLToPath(import.meta.url)); process.chdir(join(__dirname, 'fixtures')); @@ -20,11 +21,15 @@ const defaultAssetInclude = [ ]; test('evaluated-web-bundle', async ()=>{ + expect.addSnapshotSerializer(serializer); const out = await runBrowserTest({ input: 'index.hbs', treeshake: 'smallest', plugins: [ html({ + include: [ + '**/*.(html|hbs)',// html or handlebars + ], transform(src) { return handlebars.compile(src)({ head: `I'm cool!` diff --git a/test/js-import/__snapshots__/test.js.snap b/test/js-import/__snapshots__/test.js.snap index 69dccee..8fbd91e 100644 --- a/test/js-import/__snapshots__/test.js.snap +++ b/test/js-import/__snapshots__/test.js.snap @@ -1,69 +1,32 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`js-import 1`] = ` -[ - { - "code": "var asset0 = "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2032%2032%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%20%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3A%2300ff0d%3Bstroke-width%3A5%3Bstroke-linecap%3Asquare%3Bstroke-linejoin%3Amiter%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M4.1%2014.72%2016%2026.31%2028.38%205.09%22%2F%3E%3C%2Fsvg%3E"; - -const html = \` - - - - - - - - - -\`; - -function render(){ - return html; -} - -export { render }; -//# sourceMappingURL=index.js.map -", - "fileName": "index.js", - "map": SourceMap { - "file": "index.js", - "mappings": "AAAA,aAAe;;ACAf,MAAA,IAAA,GAAA,CAAA;AACA,+BAA+B,EAAwD,MAAA,CAAA;AACvF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAa,CAAA;;ACRN,SAAS,MAAM,EAAE;AACxB,IAAI,OAAO,IAAI,CAAC;AAChB;;;;", - "names": [], - "sources": [ - "../icon.svg", - "../index.html", - "../index.js", - ], - "sourcesContent": [ - "export default "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2032%2032%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%20%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3A%2300ff0d%3Bstroke-width%3A5%3Bstroke-linecap%3Asquare%3Bstroke-linejoin%3Amiter%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M4.1%2014.72%2016%2026.31%2028.38%205.09%22%2F%3E%3C%2Fsvg%3E"", - " - - - - - - - - - - -", - "import html from "./index.html" - -export function render(){ - return html; -} -", - ], - "version": 3, - }, - "source": undefined, - }, - { - "code": undefined, - "fileName": "index.js.map", - "map": undefined, - "source": "{"version":3,"file":"index.js","sources":["../icon.svg","../index.html","../index.js"],"sourcesContent":["export default \\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2032%2032%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%20%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3A%2300ff0d%3Bstroke-width%3A5%3Bstroke-linecap%3Asquare%3Bstroke-linejoin%3Amiter%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M4.1%2014.72%2016%2026.31%2028.38%205.09%22%2F%3E%3C%2Fsvg%3E\\"","\\n \\n \\n \\n\\n \\n \\n \\n \\n \\n\\n","import html from \\"./index.html\\"\\n\\nexport function render(){\\n return html;\\n}\\n"],"names":[],"mappings":"AAAA,aAAe;;ACAf,MAAA,IAAA,GAAA,CAAA;AACA,+BAA+B,EAAwD,MAAA,CAAA;AACvF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAa,CAAA;;ACRN,SAAS,MAAM,EAAE;AACxB,IAAI,OAAO,IAAI,CAAC;AAChB;;;;"}", - }, -] + ############ + # index.js # + ############ + var asset0 = "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2032%2032%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%20%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3A%2300ff0d%3Bstroke-width%3A5%3Bstroke-linecap%3Asquare%3Bstroke-linejoin%3Amiter%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M4.1%2014.72%2016%2026.31%2028.38%205.09%22%2F%3E%3C%2Fsvg%3E"; + + const html = \` + + + + + + + + + + \`; + + function render(){ + return html; + } + + export { render }; + //# sourceMappingURL=index.js.map + + ################ + # index.js.map # + ################ + {"version":3,"file":"index.js","sources":["../icon.svg","../index.html","../index.js"],"sourcesContent":["export default \\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2032%2032%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%20%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3A%2300ff0d%3Bstroke-width%3A5%3Bstroke-linecap%3Asquare%3Bstroke-linejoin%3Amiter%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M4.1%2014.72%2016%2026.31%2028.38%205.09%22%2F%3E%3C%2Fsvg%3E\\"","\\n \\n \\n \\n\\n \\n \\n \\n \\n \\n\\n","import html from \\"./index.html\\"\\n\\nexport function render(){\\n return html;\\n}\\n"],"names":[],"mappings":"AAAA,aAAe;;ACAf,MAAA,IAAA,GAAA,CAAA;AACA,+BAA+B,EAAwD,MAAA,CAAA;AACvF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAa,CAAA;;ACRN,SAAS,MAAM,EAAE;AACxB,IAAI,OAAO,IAAI,CAAC;AAChB;;;;"} `; diff --git a/test/js-import/test.js b/test/js-import/test.js index 3d00cd5..d83253b 100644 --- a/test/js-import/test.js +++ b/test/js-import/test.js @@ -3,7 +3,7 @@ import {test, expect} from "@jest/globals"; import { rollup } from "rollup"; -import {debugPrintOutput, getCode} from "../util/index.ts"; +import {debugPrintOutput, getCode, serializer} from "../util/index.ts"; import html from "../../src/index.ts"; import handlebars from "handlebars"; @@ -22,6 +22,7 @@ const defaultAssetInclude = [ test('js-import', async () => { + expect.addSnapshotSerializer(serializer); const bundle = await rollup({ input: 'index.js', plugins: [ @@ -36,7 +37,7 @@ test('js-import', async () => { }); const code = await getCode(bundle); debugPrintOutput('js-import',code); - expect(code).toMatchSnapshot(); + expect({code}).toMatchSnapshot(); }); diff --git a/test/jsx-web-app/__snapshots__/test.js.snap b/test/jsx-web-app/__snapshots__/test.js.snap index 635eb88..a6c671d 100644 --- a/test/jsx-web-app/__snapshots__/test.js.snap +++ b/test/jsx-web-app/__snapshots__/test.js.snap @@ -1,36 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`web-bundle 1`] = ` -{ - "console": [ - "[info] %cDownload the React DevTools for a better development experience: https://reactjs.org/link/react-devtools font-weight:bold", - "[log] Bootstrapped, ready to go!", - "[log] Test my sourcemap: tick", - "[log] Test my sourcemap: ended", - ], - "errors": [], - "html": " - - - Test bundle! - - - - - - I'm cool! - - -
ended
- - - -", - "requestsFailed": [], - "responses": [ - "200 http://localhost/index.html", - "200 http://localhost/index.js", - "200 http://localhost/app.js", - ], -} + ################# + # RENDERED HTML # + ################# + + + + Test bundle! + + + + + + I'm cool! + + +
ended
+ + + + + ########### + # CONSOLE # + ########### + [info] %cDownload the React DevTools for a better development experience: https://reactjs.org/link/react-devtools font-weight:bold + [log] Bootstrapped, ready to go! + [log] Test my sourcemap: tick + [log] Test my sourcemap: ended + ############# + # RESPONSES # + ############# + 200 http://localhost/index.html + 200 http://localhost/index.js + 200 http://localhost/app.js `; diff --git a/test/jsx-web-app/test.js b/test/jsx-web-app/test.js index 7eed251..eb22999 100644 --- a/test/jsx-web-app/test.js +++ b/test/jsx-web-app/test.js @@ -15,6 +15,7 @@ import {runBrowserTest} from "../util/browser-test.ts"; import {fileURLToPath} from "node:url"; import handlebars from "handlebars"; +import {serializer} from "../util/index.ts"; // import {debugPrintOutput, getCode, runBrowserTest} from "../util/index.ts"; const __dirname = dirname(fileURLToPath(import.meta.url)); process.chdir(join(__dirname, 'fixtures')); @@ -29,11 +30,15 @@ const defaultAssetInclude = [ jest.setTimeout(30*1000);// Bundling react + typescript is getting heavy test('web-bundle', async () => { + expect.addSnapshotSerializer(serializer); const out = await runBrowserTest({ input: 'index.hbs', treeshake: 'smallest', plugins: [ html({ + include: [ + '**/*.(html|hbs)',// html or handlebars + ], transform(src) { return handlebars.compile(src)({ head: `I'm cool!` @@ -77,6 +82,7 @@ test('web-bundle', async () => { entryFileNames: '[name].[extname]', assetFileNames: '[name].[extname]', }); + delete out.code; // Filter out code output (because this would be a huge snapshot) expect(out).toMatchSnapshot(); // const code = await getCode(bundle, output); diff --git a/test/live-reload/__snapshots__/test.js.snap b/test/live-reload/__snapshots__/test.js.snap index 25deb89..06d28ad 100644 --- a/test/live-reload/__snapshots__/test.js.snap +++ b/test/live-reload/__snapshots__/test.js.snap @@ -1,54 +1,31 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`live-reload 1`] = ` -[ - { - "code": " -(function(l, r) { if (!l || l.getElementById('livereloadscript')) return; r = l.createElement('script'); r.async = 1; r.src = '//' + (self.location.host || 'localhost').split(':')[0] + ':/livereload.js?snipver=1'; r.id = 'livereloadscript'; l.getElementsByTagName('head')[0].appendChild(r) })(self.document); -const test = ()=>{ - return \`I'm "annoying" \${"in case we need to test \\\`string\\\` escaping."}. Hence this files \\'tries\\' to include all allowed forms of 'it'\`; -}; -console.log(test()); - -export { test }; -//# sourceMappingURL=batman.js.map -", - "fileName": "batman.js", - "map": SourceMap { - "file": "batman.js", - "mappings": ";;AAAY,MAAC,IAAI,GAAG,IAAI;AACxB,IAAI,OAAO,CAAC,eAAe,EAAE,8CAA8C,CAAC,iEAAiE,CAAC,CAAC;AAC/I,EAAC;AACD,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;;;;", - "names": [], - "sources": [ - "../batman.js", - ], - "sourcesContent": [ - "export const test = ()=>{ - return \`I'm "annoying" \${"in case we need to test \\\`string\\\` escaping."}. Hence this files \\'tries\\' to include all allowed forms of 'it'\`; -} -console.log(test()); -", - ], - "version": 3, - }, - "source": undefined, - }, - { - "code": undefined, - "fileName": "batman.js.map", - "map": undefined, - "source": "{"version":3,"file":"batman.js","sources":["../batman.js"],"sourcesContent":["export const test = ()=>{\\n return \`I'm \\"annoying\\" \${\\"in case we need to test \\\\\`string\\\\\` escaping.\\"}. Hence this files \\\\'tries\\\\' to include all allowed forms of 'it'\`;\\n}\\nconsole.log(test());\\n"],"names":[],"mappings":";;AAAY,MAAC,IAAI,GAAG,IAAI;AACxB,IAAI,OAAO,CAAC,eAAe,EAAE,8CAA8C,CAAC,iEAAiE,CAAC,CAAC;AAC/I,EAAC;AACD,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;;;;"}", - }, - { - "code": undefined, - "fileName": "index.html", - "map": undefined, - "source": " - - - - - -", - }, -] + ############## + # index.html # + ############## + + + + + + + + ############# + # batman.js # + ############# + + (function(l, r) { if (!l || l.getElementById('livereloadscript')) return; r = l.createElement('script'); r.async = 1; r.src = '//' + (self.location.host || 'localhost').split(':')[0] + ':/livereload.js?snipver=1'; r.id = 'livereloadscript'; l.getElementsByTagName('head')[0].appendChild(r) })(self.document); + const test = ()=>{ + return \`I'm "annoying" \${"in case we need to test \\\`string\\\` escaping."}. Hence this files \\'tries\\' to include all allowed forms of 'it'\`; + }; + console.log(test()); + + export { test }; + //# sourceMappingURL=batman.js.map + + ################# + # batman.js.map # + ################# + {"version":3,"file":"batman.js","sources":["../batman.js"],"sourcesContent":["export const test = ()=>{\\n return \`I'm \\"annoying\\" \${\\"in case we need to test \\\\\`string\\\\\` escaping.\\"}. Hence this files \\\\'tries\\\\' to include all allowed forms of 'it'\`;\\n}\\nconsole.log(test());\\n"],"names":[],"mappings":";;AAAY,MAAC,IAAI,GAAG,IAAI;AACxB,IAAI,OAAO,CAAC,eAAe,EAAE,8CAA8C,CAAC,iEAAiE,CAAC,CAAC;AAC/I,EAAC;AACD,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;;;;"} `; diff --git a/test/live-reload/test.js b/test/live-reload/test.js index 99027ea..4ce2e63 100644 --- a/test/live-reload/test.js +++ b/test/live-reload/test.js @@ -3,7 +3,7 @@ import {test, expect} from "@jest/globals"; import {rollup} from "rollup"; import liveReload from "rollup-plugin-livereload"; -import {debugPrintOutput, getCode} from "../util/index.ts"; +import {debugPrintOutput, getCode, serializer} from "../util/index.ts"; import html from "../../src/index.ts"; @@ -13,6 +13,7 @@ process.chdir(join(__dirname, 'fixtures')); test('live-reload', async () => { + expect.addSnapshotSerializer(serializer); const bundle = await rollup({ input: 'index.html', plugins: [ @@ -32,7 +33,7 @@ test('live-reload', async () => { file.code = file.code.replaceAll(portRE,":/livereload.js"); // remove any references to a port } } - expect(code).toMatchSnapshot(); + expect({code}).toMatchSnapshot(); }); // TODO various parameters diff --git a/test/multi-entry/__snapshots__/test.js.snap b/test/multi-entry/__snapshots__/test.js.snap index 0634627..5a7dfb6 100644 --- a/test/multi-entry/__snapshots__/test.js.snap +++ b/test/multi-entry/__snapshots__/test.js.snap @@ -1,125 +1,77 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`multi-entry 1`] = ` -[ - { - "code": "const b = ()=>'batman'; -console.log(b()); - -export { b }; -//# sourceMappingURL=batman.js.map -", - "fileName": "admin/batman.js", - "map": SourceMap { - "file": "batman.js", - "mappings": "AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;", - "names": [], - "sources": [ - "../../admin/batman.js", - ], - "sourcesContent": [ - "export const b = ()=>'batman'; -console.log(b()); -", - ], - "version": 3, - }, - "source": undefined, - }, - { - "code": undefined, - "fileName": "admin/batman.js.map", - "map": undefined, - "source": "{"version":3,"file":"batman.js","sources":["../../admin/batman.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"}", - }, - { - "code": undefined, - "fileName": "admin/index.body.script0.js.js.map", - "map": undefined, - "source": "{"version":3,"file":"index.body.script0.js.js","sources":["../../app/admin-deps.js","../../admin/index.html.body.script0.js"],"sourcesContent":["export function adminDeps(){\\n return \\"robin!\\";\\n}\\n","\\n import {bootstrap} from \\"../app/app.js\\"\\n import {adminDeps} from \\"../app/admin-deps.js\\";\\n bootstrap(document.getElementById('root'), adminDeps());\\n "],"names":[],"mappings":";;AAAO,SAAS,SAAS,EAAE;AAC3B,IAAI,OAAO,QAAQ,CAAC;AACpB;;ACCY,SAAS,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC"}", - }, - { - "code": undefined, - "fileName": "admin/index.html", - "map": undefined, - "source": " - - -
- - - - -", - }, - { - "code": "const bootstrap = (el,deps = [])=>{ - el.innerHtml = \` -
I'm "annoying" \${"in case we need to test \\\`string\\\` escaping."}. Hence this file \\'tries\\' to include all allowed forms of 'it'
-
Deps: \${deps}
- \`; -}; - -export { bootstrap as b }; -//# sourceMappingURL=app.js.map -", - "fileName": "app.js", - "map": SourceMap { - "file": "app.js", - "mappings": "AAAY,MAAC,SAAS,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,EAAE,GAAG;AACzC,IAAI,EAAE,CAAC,SAAS,GAAG,CAAC;AACpB,4BAA4B,EAAE,8CAA8C,CAAC;AAC7E,mBAAmB,EAAE,IAAI,CAAC;AAC1B,IAAI,CAAC,CAAC;AACN;;;;", - "names": [], - "sources": [ - "../app/app.js", - ], - "sourcesContent": [ - "export const bootstrap = (el,deps = [])=>{ - el.innerHtml = \` -
I'm "annoying" \${"in case we need to test \\\`string\\\` escaping."}. Hence this file \\'tries\\' to include all allowed forms of 'it'
-
Deps: \${deps}
- \`; -} -", - ], - "version": 3, - }, - "source": undefined, - }, - { - "code": undefined, - "fileName": "app.js.map", - "map": undefined, - "source": "{"version":3,"file":"app.js","sources":["../app/app.js"],"sourcesContent":["export const bootstrap = (el,deps = [])=>{\\n el.innerHtml = \`\\n
I'm \\"annoying\\" \${\\"in case we need to test \\\\\`string\\\\\` escaping.\\"}. Hence this file \\\\'tries\\\\' to include all allowed forms of 'it'
\\n
Deps: \${deps}
\\n \`;\\n}\\n"],"names":[],"mappings":"AAAY,MAAC,SAAS,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,EAAE,GAAG;AACzC,IAAI,EAAE,CAAC,SAAS,GAAG,CAAC;AACpB,4BAA4B,EAAE,8CAA8C,CAAC;AAC7E,mBAAmB,EAAE,IAAI,CAAC;AAC1B,IAAI,CAAC,CAAC;AACN;;;;"}", - }, - { - "code": undefined, - "fileName": "index.body.script.js.js.map", - "map": undefined, - "source": "{"version":3,"file":"index.body.script.js.js","sources":["../index.html.body.script.js"],"sourcesContent":["\\n import {bootstrap} from \\"./app/app.js\\"\\n bootstrap(document.getElementById('root'), \\"\\");\\n "],"names":[],"mappings":";;AAEY,SAAS,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC"}", - }, - { - "code": undefined, - "fileName": "index.html", - "map": undefined, - "source": " - - -
- - - -", - }, -] + #################### + # admin/index.html # + #################### + + + +
+ + + + + + ############## + # index.html # + ############## + + + +
+ + + + + ################### + # admin/batman.js # + ################### + const b = ()=>'batman'; + console.log(b()); + + export { b }; + //# sourceMappingURL=batman.js.map + + ####################### + # admin/batman.js.map # + ####################### + {"version":3,"file":"batman.js","sources":["../../admin/batman.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"} + ###################################### + # admin/index.body.script0.js.js.map # + ###################################### + {"version":3,"file":"index.body.script0.js.js","sources":["../../app/admin-deps.js","../../admin/index.html.body.script0.js"],"sourcesContent":["export function adminDeps(){\\n return \\"robin!\\";\\n}\\n","\\n import {bootstrap} from \\"../app/app.js\\"\\n import {adminDeps} from \\"../app/admin-deps.js\\";\\n bootstrap(document.getElementById('root'), adminDeps());\\n "],"names":[],"mappings":";;AAAO,SAAS,SAAS,EAAE;AAC3B,IAAI,OAAO,QAAQ,CAAC;AACpB;;ACCY,SAAS,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC"} + ########## + # app.js # + ########## + const bootstrap = (el,deps = [])=>{ + el.innerHtml = \` +
I'm "annoying" \${"in case we need to test \\\`string\\\` escaping."}. Hence this file \\'tries\\' to include all allowed forms of 'it'
+
Deps: \${deps}
+ \`; + }; + + export { bootstrap as b }; + //# sourceMappingURL=app.js.map + + ############## + # app.js.map # + ############## + {"version":3,"file":"app.js","sources":["../app/app.js"],"sourcesContent":["export const bootstrap = (el,deps = [])=>{\\n el.innerHtml = \`\\n
I'm \\"annoying\\" \${\\"in case we need to test \\\\\`string\\\\\` escaping.\\"}. Hence this file \\\\'tries\\\\' to include all allowed forms of 'it'
\\n
Deps: \${deps}
\\n \`;\\n}\\n"],"names":[],"mappings":"AAAY,MAAC,SAAS,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,EAAE,GAAG;AACzC,IAAI,EAAE,CAAC,SAAS,GAAG,CAAC;AACpB,4BAA4B,EAAE,8CAA8C,CAAC;AAC7E,mBAAmB,EAAE,IAAI,CAAC;AAC1B,IAAI,CAAC,CAAC;AACN;;;;"} + ############################### + # index.body.script.js.js.map # + ############################### + {"version":3,"file":"index.body.script.js.js","sources":["../index.html.body.script.js"],"sourcesContent":["\\n import {bootstrap} from \\"./app/app.js\\"\\n bootstrap(document.getElementById('root'), \\"\\");\\n "],"names":[],"mappings":";;AAEY,SAAS,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC"} `; diff --git a/test/multi-entry/test.js b/test/multi-entry/test.js index a8357d6..fe9ef1a 100644 --- a/test/multi-entry/test.js +++ b/test/multi-entry/test.js @@ -2,7 +2,7 @@ import {resolve, join, dirname} from "node:path"; import {test, expect} from "@jest/globals"; import { rollup } from "rollup"; -import {debugPrintOutput, getCode} from "../util/index.ts"; +import {debugPrintOutput, getCode, serializer} from "../util/index.ts"; import html from "../../src/index.ts"; @@ -12,6 +12,7 @@ process.chdir(join(__dirname, 'fixtures')); test('multi-entry', async () => { + expect.addSnapshotSerializer(serializer); const bundle = await rollup({ input: { ['index']: 'index.html', @@ -24,7 +25,7 @@ test('multi-entry', async () => { }); const code = await getCode(bundle); debugPrintOutput('multi-entry',code); - expect(code).toMatchSnapshot(); + expect({code}).toMatchSnapshot(); }); // TODO various parameters diff --git a/test/rewrite-url/__snapshots__/test.js.snap b/test/rewrite-url/__snapshots__/test.js.snap index f3a5625..0cc4625 100644 --- a/test/rewrite-url/__snapshots__/test.js.snap +++ b/test/rewrite-url/__snapshots__/test.js.snap @@ -1,3 +1,59 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`rewrite-url 1`] = `undefined`; +exports[`rewrite-url 1`] = ` + #################### + # admin/index.html # + #################### + + + +
+ + + + + ############## + # index.html # + ############## + + + +
+ + + + + ################ + # admin/app.js # + ################ + const bootstrap = (el,deps = [])=>{ + el.innerHtml = \` +
load the app
+ \`; + }; + + export { bootstrap }; + //# sourceMappingURL=app.js.map + + #################### + # admin/app.js.map # + #################### + {"version":3,"file":"app.js","sources":["../../admin/app.js"],"sourcesContent":["export const bootstrap = (el,deps = [])=>{\\n el.innerHtml = \`\\n
load the app
\\n \`;\\n}\\n"],"names":[],"mappings":"AAAY,MAAC,SAAS,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,EAAE,GAAG;AACzC,IAAI,EAAE,CAAC,SAAS,GAAG,CAAC;AACpB;AACA,IAAI,CAAC,CAAC;AACN;;;;"} + ################# + # RENDERED HTML # + ################# + + + +
+ + + + + ############# + # RESPONSES # + ############# + 200 http://localhost/admin + 200 http://localhost/admin/app.js + 200 http://localhost/favicon.ico +`; diff --git a/test/rewrite-url/test.js b/test/rewrite-url/test.js index d273b00..02387d4 100644 --- a/test/rewrite-url/test.js +++ b/test/rewrite-url/test.js @@ -1,7 +1,7 @@ import {resolve, join, dirname} from "node:path"; import {test, expect} from "@jest/globals"; -import {runBrowserTest} from "../util/index.ts"; +import {runBrowserTest, serializer} from "../util/index.ts"; import html from "../../src/index.ts"; @@ -11,6 +11,7 @@ process.chdir(join(__dirname, 'fixtures')); test('rewrite-url', async () => { + expect.addSnapshotSerializer(serializer); const out = await runBrowserTest({ input: { ['index']: 'index.html', @@ -34,7 +35,7 @@ test('rewrite-url', async () => { format: 'es', // iifi and cjs should be added to tests sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output }); - expect(out.code).toMatchSnapshot(); // Snapshot the result code + expect(out).toMatchSnapshot(); // Snapshot the result code // const bundle = await rollup({ // input: { diff --git a/test/templating/__snapshots__/test.js.snap b/test/templating/__snapshots__/test.js.snap index 3363f1d..6ea0146 100644 --- a/test/templating/__snapshots__/test.js.snap +++ b/test/templating/__snapshots__/test.js.snap @@ -1,49 +1,28 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`handlebars 1`] = ` -[ - { - "code": "const b = ()=>'batman'; -console.log(b()); - -export { b }; -//# sourceMappingURL=batman.js.map -", - "fileName": "batman.js", - "map": SourceMap { - "file": "batman.js", - "mappings": "AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;", - "names": [], - "sources": [ - "../batman.js", - ], - "sourcesContent": [ - "export const b = ()=>'batman'; -console.log(b()); -", - ], - "version": 3, - }, - "source": undefined, - }, - { - "code": undefined, - "fileName": "batman.js.map", - "map": undefined, - "source": "{"version":3,"file":"batman.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"}", - }, - { - "code": undefined, - "fileName": "index.html", - "map": undefined, - "source": " - - - - - - -", - }, -] + ############## + # index.html # + ############## + + + + + + + + + ############# + # batman.js # + ############# + const b = ()=>'batman'; + console.log(b()); + + export { b }; + //# sourceMappingURL=batman.js.map + + ################# + # batman.js.map # + ################# + {"version":3,"file":"batman.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"} `; diff --git a/test/templating/test.js b/test/templating/test.js index d34d429..16e3fef 100644 --- a/test/templating/test.js +++ b/test/templating/test.js @@ -3,7 +3,7 @@ import {test, expect} from "@jest/globals"; import { rollup } from "rollup"; -import {debugPrintOutput, getCode} from "../util/index.ts"; +import {debugPrintOutput, getCode, serializer} from "../util/index.ts"; import html from "../../src/index.ts"; import handlebars from "handlebars"; @@ -13,10 +13,14 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); process.chdir(join(__dirname, 'fixtures')); test('handlebars', async () => { + expect.addSnapshotSerializer(serializer); const bundle = await rollup({ input: 'index.hbs', plugins: [ html({ + include: [ + '**/*.(html|hbs)',// html or handlebars + ], transform(src){ return handlebars.compile(src)({a:'a'}) } @@ -25,7 +29,7 @@ test('handlebars', async () => { }); const code = await getCode(bundle); debugPrintOutput('handlebars',code); - expect(code).toMatchSnapshot(); + expect({code}).toMatchSnapshot(); }); diff --git a/test/url-plugin/__snapshots__/test.js.snap b/test/url-plugin/__snapshots__/test.js.snap index 699c3c8..8b504a6 100644 --- a/test/url-plugin/__snapshots__/test.js.snap +++ b/test/url-plugin/__snapshots__/test.js.snap @@ -1,97 +1,55 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`url-plugin copied-assets 1`] = ` -[ - { - "code": "const b = ()=>'batman'; -console.log(b()); - -export { b }; -//# sourceMappingURL=batman.js.map -", - "fileName": "batman.js", - "map": SourceMap { - "file": "batman.js", - "mappings": "AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;", - "names": [], - "sources": [ - "../batman.js", - ], - "sourcesContent": [ - "export const b = ()=>'batman'; -console.log(b()); -", - ], - "version": 3, - }, - "source": undefined, - }, - { - "code": undefined, - "fileName": "batman.js.map", - "map": undefined, - "source": "{"version":3,"file":"batman.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"}", - }, - { - "code": undefined, - "fileName": "index.html", - "map": undefined, - "source": " - - - - - - -", - }, -] + ############## + # index.html # + ############## + + + + + + + + + ############# + # batman.js # + ############# + const b = ()=>'batman'; + console.log(b()); + + export { b }; + //# sourceMappingURL=batman.js.map + + ################# + # batman.js.map # + ################# + {"version":3,"file":"batman.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"} `; exports[`url-plugin inlined-assets 1`] = ` -[ - { - "code": "const b = ()=>'batman'; -console.log(b()); - -export { b }; -//# sourceMappingURL=batman.js.map -", - "fileName": "batman.js", - "map": SourceMap { - "file": "batman.js", - "mappings": "AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;", - "names": [], - "sources": [ - "../batman.js", - ], - "sourcesContent": [ - "export const b = ()=>'batman'; -console.log(b()); -", - ], - "version": 3, - }, - "source": undefined, - }, - { - "code": undefined, - "fileName": "batman.js.map", - "map": undefined, - "source": "{"version":3,"file":"batman.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"}", - }, - { - "code": undefined, - "fileName": "index.html", - "map": undefined, - "source": " - - - - - - -", - }, -] + ############## + # index.html # + ############## + + + + + + + + + ############# + # batman.js # + ############# + const b = ()=>'batman'; + console.log(b()); + + export { b }; + //# sourceMappingURL=batman.js.map + + ################# + # batman.js.map # + ################# + {"version":3,"file":"batman.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>'batman';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"} `; diff --git a/test/url-plugin/test.js b/test/url-plugin/test.js index 250d5fb..dbdc058 100644 --- a/test/url-plugin/test.js +++ b/test/url-plugin/test.js @@ -4,7 +4,7 @@ import {test, expect} from "@jest/globals"; import { rollup } from "rollup"; import urlPlugin from "@rollup/plugin-url"; -import {debugPrintOutput, getCode} from "../util/index.ts"; +import {debugPrintOutput, getCode, serializer} from "../util/index.ts"; import html from "../../src/index.ts"; @@ -21,6 +21,7 @@ const defaultAssetInclude = [ ]; describe("url-plugin", ()=>{ + expect.addSnapshotSerializer(serializer); test('copied-assets', async () => { const bundle = await rollup({ input: 'index.html', @@ -35,7 +36,7 @@ describe("url-plugin", ()=>{ }); const code = await getCode(bundle); debugPrintOutput('copied-assets',code); - expect(code).toMatchSnapshot(); + expect({code}).toMatchSnapshot(); }); @@ -53,7 +54,7 @@ describe("url-plugin", ()=>{ }); const code = await getCode(bundle); debugPrintOutput('inlined-assets',code); - expect(code).toMatchSnapshot(); + expect({code}).toMatchSnapshot(); }); }) diff --git a/test/util/browser-test.ts b/test/util/browser-test.ts index 6b3755d..b3ec4e6 100644 --- a/test/util/browser-test.ts +++ b/test/util/browser-test.ts @@ -49,7 +49,7 @@ export async function runBrowserTest( ...test, log: test.log ?? console.log, onResult: (output)=>{ - testOutput = {...testOutput, ...output}; + Object.assign(testOutput, output); } })]: []) ] diff --git a/test/util/index.ts b/test/util/index.ts index ce9b7f2..f294e5b 100644 --- a/test/util/index.ts +++ b/test/util/index.ts @@ -1,5 +1,7 @@ // TODO: this should be the main module used, other should be imported manually if exceptions are needed? export * from "./browser-test.ts"; +export {defaultOutput} from "./default-output.ts"; +export {serializer} from "./test-serializer.ts"; export * from "./code-output.ts"; export * from "./print-code-output.ts"; diff --git a/test/util/serve-test.ts b/test/util/serve-test.ts index e80eb15..36b9c30 100644 --- a/test/util/serve-test.ts +++ b/test/util/serve-test.ts @@ -133,6 +133,10 @@ export default function serveTest (options: RollupServeTestOptions ): Plugin { let server : Server; let bundle : OutputBundle = {}; + const closeServer = async ()=>new Promise((resolve, reject)=>{ + server.close((err)=>err?reject(err):resolve(undefined)); + server = null as unknown as Server; // unset + }); const logTest = (msg: string, mode: 'info'|'warn' = 'info')=>{ if(isInDebugMode()){ @@ -217,9 +221,9 @@ export default function serveTest (options: RollupServeTestOptions ): Plugin { function closeServerOnTermination () { const terminationSignals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP'] terminationSignals.forEach(signal => { - process.on(signal, () => { + process.on(signal, async () => { if (server) { - server.close() + await closeServer(); process.exit() } }) @@ -229,7 +233,7 @@ export default function serveTest (options: RollupServeTestOptions ): Plugin { // release previous server instance if rollup is reloading configuration in watch mode // @ts-ignore if (server) { - server.close() + closeServer() } else { closeServerOnTermination() } @@ -279,8 +283,9 @@ export default function serveTest (options: RollupServeTestOptions ): Plugin { } } }, - closeBundle (){ + async closeBundle(){ // Done with the bundle + await closeServer(); } } } diff --git a/test/util/test-serializer.ts b/test/util/test-serializer.ts new file mode 100644 index 0000000..b3f668f --- /dev/null +++ b/test/util/test-serializer.ts @@ -0,0 +1,79 @@ +import type {runBrowserTest} from "./browser-test.ts"; +import type {expect} from "@jest/globals"; +import chalk from "chalk"; + +type TestOutput = Awaited>; +type Serializer = Parameters[0]; + +function headerFor(name: string): string[]{ + const hr = name.split('').map(()=>`#`).join('') + return [ + `##${hr}##`, + `# ${name} #`, + `##${hr}##`, + ]; +} + +export const serializer: Serializer = { + test: (val: TestOutput)=> !!( + (val?.code && Array.isArray(val?.code)) + || (val?.html && typeof(val?.html)==='string') + ), + serialize(val: TestOutput, + config, + indentation, + depth, + refs, + printer): string{ + const indent = (config.indent||'')+(indentation+''); + let linesOut: string[] = []; + + if(val.code){ + const fileLines: string[][] = val.code.slice().sort(((a,b)=>{ + const sortPropsA = [!a.fileName.endsWith('html'), a.fileName]; + const sortPropsB = [!b.fileName.endsWith('html'), b.fileName]; + for(let i = 0; i< 2;++i){ + if(sortPropsA[i]sortPropsB[i]) return 1; + } + return 0; + })).map(({fileName, code, source})=>{ + return [ + ...headerFor(fileName), + ...((code||source).split('\n')) + ] + }); + + linesOut = linesOut.concat(...fileLines); + } + + if(val.html){ + linesOut = linesOut.concat([ + ...headerFor("RENDERED HTML"), + ...(val.html.split('\n')), + ]); + } + if(val.errors?.length){ + linesOut = linesOut.concat([ + ...headerFor("ERRORS"), + ], ...val.errors.map(x=>x.split("\n"))); + } + if(val.console?.length){ + linesOut = linesOut.concat([ + ...headerFor("CONSOLE"), + ], ...val.console.map(x=>x.split("\n"))); + } + if(val.requestsFailed?.length){ + linesOut = linesOut.concat([ + ...headerFor("FAILED REQUESTS"), + ], ...val.requestsFailed.map(x=>x.split("\n"))); + } + if(val.responses?.length){ + linesOut = linesOut.concat([ + ...headerFor("RESPONSES"), + ], ...val.responses.map(x=>x.split("\n"))); + } + + return linesOut.map(x=>`${indent}${x}`).join('\n'); + }, +} diff --git a/tsconfig.json b/tsconfig.json index 8af2d8b..352232d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,12 +11,12 @@ "sourceMap": true, "strict": true, "target": "ESNext", - "module": "ESNext", + "module": "NodeNext", "moduleResolution": "NodeNext", "allowJs": true, "allowImportingTsExtensions": true }, - "exclude": ["dist", "node_modules", "test/types"], + "exclude": ["dist", "node_modules", "test/types","test/**/fixtures/*"], "include": ["src/**/*", "types/**/*"], "ts-node": { "esm": true // from the top of https://typestrong.org/ts-node/docs/imports/ diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index 0813177..0000000 --- a/types/index.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type {Plugin, OutputChunk, OutputAsset, OutputBundle, TransformModuleJSON, } from 'rollup'; -import {FilterPattern} from "@rollup/pluginutils"; -import type {DefaultTreeAdapterMap} from "parse5"; -import {PreRenderedChunk} from "rollup"; - -import type {LoadNodeCallback} from "./load.d.ts"; -export type * from "./load.d.ts" - -import type {ResolveCallback} from "./resolve.d.ts"; -export type * from "./resolve.d.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 interface RollupHtmlOptions { - publicPath?: string; - /** - * Follows the same logic as rollup's [entryFileNames](https://rollupjs.org/configuration-options/#output-entryfilenames). - */ - htmlFileNames?: string|((chunkInfo: PreRenderedChunk) => string); - - /** - * Transform a source file passed into this plugin to HTML. For example: a handlebars transform - * ``` - * transform(source){ - * return handlebars.compile(source)({myVar:'example'}) - * } - * ``` - */ - transform?: TransformCallback; - - /** - * Optional callback to rewrite how resources are referenced in the output HTML. - * For example to rewrite urls to as paths from the root of your website: - * ``` - * rewriteUrl(relative, {rootPath, from}){ - * return `/${rootPath}`; - * } - * ``` - */ - rewriteUrl?: RewriteUrlCallback; - - /** - * Detect which references (
, ) to resolve from a HTML node. - * This rarely needs to be overloaded, but can be used to support non-native attributes used by custom-elements. - * - * Return false to skip any further processing on this node. Use the load function to add any resources from this node, and replace the import with a placeholder so the plugin knows where to inject the end result - */ - load?: LoadNodeCallback; - /** - * Callback to filter which references actually need to be resolved. Here you can filter out: - * - Links to extensions that don't need to be handled through rollup - * - Resources that are external to the app (for example non-relative paths) - * - Page navigation within the app - * - * Return a falsey value to skip this reference. Return true to resolve as is. (or string to transform the id) - */ - resolve?: ResolveCallback; - - /** - * [Pattern](https://github.com/micromatch/picomatch#globbing-features) to include - */ - include?: FilterPattern; - /** - * [Pattern](https://github.com/micromatch/picomatch#globbing-features) to exclude - */ - exclude?: FilterPattern -} - - -/** - * A Rollup plugin which creates HTML files to serve Rollup bundles. - * @param options - Plugin options. - * @returns Plugin instance. - */ -export default function html(options?: RollupHtmlOptions): Plugin;