UNPKG

htmelt

Version:

Bundle your HTML assets with Esbuild and LightningCSS. Custom plugins, HMR platform, and more.

628 lines (615 loc) 20.8 kB
import { esbuildBundles } from "./chunk-26E6E5EJ.mjs"; import { findExternalScripts } from "./chunk-SGZXFKQT.mjs"; // src/plugins/importGlob/index.mts import { resolve } from "path"; // src/plugins/importGlob/transformGlob.mts import { transformAsync, types as types2 } from "@babel/core"; import glob from "fast-glob"; import { dirname as dirname2 } from "path"; // src/plugins/importGlob/CodeError.mts var CodeError = class extends Error { constructor(message, nodePath) { super(message); this.nodePath = nodePath; } }; // src/plugins/importGlob/ImportGlobOptions.mts var isImportGlobOptions = (value) => typeof value === "object" && value !== null && Object.entries(value).every(([key, value2]) => { if (!["import", "eager"].includes(key)) { return false; } if (key === "import" && typeof value2 !== "string" && value2 !== void 0) { return false; } if (key === "eager" && typeof value2 !== "boolean" && value2 !== void 0) { return false; } return true; }); // src/plugins/importGlob/extractGlobArguments.mts var evaluateConfidently = (nodePath, argumentName) => { const evaluation = nodePath.evaluate(); if (!evaluation.confident) { throw new CodeError( `${argumentName} should be known at compile time.`, nodePath ); } return evaluation.value; }; function isArrayOfElements(value, predicate) { return Array.isArray(value) && value.every(predicate); } var extractGlobArguments = (nodePath) => { const globArguments = nodePath.get("arguments"); const globPatterns = evaluateConfidently( globArguments[0], "import.meta.glob first argument" ); if (typeof globPatterns !== "string" && !isArrayOfElements( globPatterns, (value) => typeof value === "string" )) { throw new CodeError( "import.meta.glob first argument should be a string or array of strings.", globArguments[0] ); } let globOptions = {}; if (globArguments[1]) { const receivedOptions = evaluateConfidently( globArguments[1], "import.meta.glob second argument" ); if (!isImportGlobOptions(receivedOptions)) { throw new CodeError( "import.meta.glob second argument should be an object of type `ImportGlobOptions`", globArguments[1] ); } globOptions = receivedOptions; } return { patterns: globPatterns, options: globOptions }; }; // src/plugins/importGlob/normalizeFiles.mts import { normalize, join, dirname } from "path"; var normalizeFiles = (files, current) => { const normalizedFiles = files.map(normalize).filter((file) => normalize(join(dirname(current), file)) !== normalize(current)); return normalizedFiles.map((file) => (/^[./\\]/.test(file) ? file : `./${file}`).replace(/\\/g, "/")); }; // src/plugins/importGlob/replaceImportGlobNode.mts import { types } from "@babel/core"; var createEagerIdentifier = (globIndex, pathIndex) => types.identifier(`__glob_${globIndex}_${pathIndex}`); var createValue = (globIndex, path3, pathIndex, options) => { if (options.eager) { return createEagerIdentifier(globIndex, pathIndex); } const importExpression = types.callExpression(types.import(), [types.stringLiteral(path3)]); if (!options.import) { return types.arrowFunctionExpression([], importExpression); } return types.arrowFunctionExpression( [], types.callExpression(types.memberExpression(importExpression, types.identifier("then")), [ types.arrowFunctionExpression( [types.identifier("m")], types.memberExpression(types.identifier("m"), types.identifier(options.import)) ) ]) ); }; var generateImportStatement = (globIndex, path3, pathIndex, options) => { const imported = options.import === void 0 ? types.importNamespaceSpecifier(createEagerIdentifier(globIndex, pathIndex)) : types.importSpecifier(createEagerIdentifier(globIndex, pathIndex), types.identifier(options.import)); return types.importDeclaration([imported], types.stringLiteral(path3)); }; var generateImports = (nodePath, globIndex, paths, options) => { const root = nodePath.findParent((path3) => path3.isProgram()); if (root === null) { throw new Error("Cannot find program root."); } const importStatements = paths.map( (path3, pathIndex) => generateImportStatement(globIndex, path3, pathIndex, options) ); root.unshiftContainer("body", importStatements); }; var replaceImportGlobNode = (nodePath, globIndex, paths, options) => { const replacement = types.objectExpression( paths.map( (path3, pathIndex) => types.objectProperty(types.stringLiteral(path3), createValue(globIndex, path3, pathIndex, options)) ) ); nodePath.replaceWith(replacement); if (options.eager) { generateImports(nodePath, globIndex, paths, options); } }; // src/plugins/importGlob/transformGlob.mts function babelPluginGlobTransformation(api) { api.assertVersion(7); return { pre() { this.counter = 0; }, visitor: { // eslint-disable-next-line @typescript-eslint/naming-convention CallExpression: (nodePath, state) => { if (types2.isMemberExpression(nodePath.node.callee) && types2.isMetaProperty(nodePath.node.callee.object) && types2.isIdentifier(nodePath.node.callee.property, { name: "glob" })) { const { patterns, options } = extractGlobArguments(nodePath); const files = normalizeFiles( glob.sync(patterns, { cwd: dirname2(state.opts.path), fs: state.opts.fs }), state.opts.path ); state.opts.onGlobImport?.(patterns, files); replaceImportGlobNode(nodePath, state.counter, files, options); ++state.counter; } } } }; } var getLine = (source, line) => source.split("\n")[line - 1]; var transformGlob = async (code, config) => { if (!/import\.meta\.glob\(/.test(code)) { return; } const plugins = []; if (config.jsx) { plugins.push("jsx"); } try { const babelOutput = await transformAsync(code, { parserOpts: { sourceType: "module", plugins }, plugins: [[babelPluginGlobTransformation, config]], sourceMaps: true }); if (!babelOutput?.code) { throw new Error("Failed to transform file via babel."); } return { code: babelOutput.code, map: babelOutput.map }; } catch (error) { if (error instanceof CodeError && error.nodePath.node.loc) { const location = error.nodePath.node.loc; Reflect.set(error, "location", { column: location.start.column, line: location.start.line, lineText: getLine(code, location.start.line), file: config.path, length: location.end.line === location.start.line ? location.end.column - location.start.column : 1 }); } throw error; } }; // src/plugins/importGlob/index.mts var createPlugin = (watcher) => { return { name: "esbuild-plugin-import-glob", setup(build2) { build2.onTransform({ loaders: ["js", "jsx"] }, async (args) => { let onGlobImport; if (watcher) { const importer = args.initialPath || args.path; onGlobImport = (glob2) => { const rootDirs = /* @__PURE__ */ new Set(); const globs = Array.isArray(glob2) ? glob2 : [glob2]; for (const glob3 of globs) { const parts = glob3.split("/"); const globStarIdx = parts.findIndex((p) => p.includes("*")); const rootDir = parts.slice(0, globStarIdx).join("/"); rootDirs.add(resolve(importer, "..", rootDir)); } for (const rootDir of rootDirs) { watcher.watchDirectory(rootDir, importer); } }; } return transformGlob(args.code, { path: args.path, jsx: args.loader === "jsx", onGlobImport }); }); } }; }; var importGlob_default = createPlugin; // src/plugins/importMetaUrl.mts import { getBlock, getIdentifierValue, getLocation, parse, TokenType, walk } from "@chialab/estransform"; import { appendSearchParam, getSearchParam, isUrl } from "@chialab/node-resolve"; import * as mime from "mrmime"; import * as path from "path"; function importMetaUrl_default({ emit = true } = {}) { const plugin = { name: "meta-url", async setup(build2) { const { absWorkingDir = process.cwd(), platform, bundle, format, sourcesContent, sourcemap } = build2.initialOptions; const usePlainScript = platform === "browser" && (format === "iife" ? !bundle : format !== "esm"); const isNode = platform === "node" && format !== "esm"; const baseUrl = (() => { if (usePlainScript) { return "__currentScriptUrl__"; } if (isNode) { return "'file://' + __filename"; } return "import.meta.url"; })(); build2.onTransform({ loaders: ["tsx", "ts", "jsx", "js"] }, async (args) => { const code = args.code; if (!code.includes("import.meta.url") || !code.includes("URL(")) { return; } const promises = []; const { helpers, processor } = parse( code, path.relative(absWorkingDir, args.path) ); const warnings = []; await walk(processor, () => { const value = getMetaUrl(processor); if (typeof value !== "string" || isUrl(value)) { return; } const id = getSearchParam(value, "hash"); if (id && build2.isEmittedPath(id)) { return; } const tokens = getBlock(processor, TokenType.parenL, TokenType.parenR); const startToken = tokens[0]; const endToken = tokens[tokens.length - 1]; promises.push( Promise.resolve().then(async () => { const requestName = value.split("?")[0]; const { path: resolvedPath, pluginData } = await build2.resolveLocallyFirst(requestName, { kind: "dynamic-import", importer: args.path, namespace: "file", resolveDir: path.dirname(args.path), pluginData: null }); if (resolvedPath) { if (pluginData !== build2.RESOLVED_AS_FILE) { const location2 = getLocation(code, startToken.start); warnings.push({ id: "import-meta-module-resolution", pluginName: "meta-url", text: `Resolving '${requestName}' as module is not a standard behavior and may be removed in a future relase of the plugin.`, location: { file: args.path, namespace: args.namespace, ...location2, length: endToken.end - startToken.start, lineText: code.split("\n")[location2.line - 1], suggestion: "Externalize module import using a JS proxy file." }, notes: [], detail: "" }); } const entryLoader = build2.getLoader(resolvedPath) || "file"; const isChunk = entryLoader !== "file" && entryLoader !== "json"; let entryPoint; if (emit) { if (isChunk) { const chunk = await build2.emitChunk({ path: resolvedPath }); entryPoint = appendSearchParam(chunk.path, "hash", chunk.id); } else { const file = await build2.emitFile(resolvedPath); entryPoint = appendSearchParam(file.path, "hash", file.id); } } else { entryPoint = path.relative( path.dirname(args.path), resolvedPath ); } if (format === "iife" && bundle) { const { outputFiles } = await build2.emitChunk({ path: `./${entryPoint}`, write: false }); if (outputFiles) { const mimeType = mime.lookup(outputFiles[0].path); const base64 = Buffer.from( outputFiles[0].contents ).toString("base64"); helpers.overwrite( startToken.start, endToken.end, `new URL('data:${mimeType};base64,${base64}')` ); } } else { helpers.overwrite( startToken.start, endToken.end, `new URL('./${entryPoint}', ${baseUrl})` ); } return; } const location = getLocation(code, startToken.start); warnings.push({ id: "import-meta-reference-not-found", pluginName: "meta-url", text: `Unable to resolve '${requestName}' file.`, location: { file: args.path, namespace: args.namespace, ...location, length: endToken.end - startToken.start, lineText: code.split("\n")[location.line - 1], suggestion: "" }, notes: [], detail: "" }); }) ); }); await Promise.all(promises); if (!helpers.isDirty()) { return { warnings }; } if (usePlainScript) { helpers.prepend( "var __currentScriptUrl__ = document.currentScript && document.currentScript.src || document.baseURI;\n" ); } const transformResult = await helpers.generate({ sourcemap: !!sourcemap, sourcesContent }); if (transformResult.map) { transformResult.map.sources = []; transformResult.map.sourcesContent = []; } return { ...transformResult, warnings }; }); } }; return plugin; } function getMetaUrl(processor) { let fnToken; let iterator = processor.currentIndex(); if (processor.matches5( TokenType._new, TokenType.name, TokenType.dot, TokenType.name, TokenType.parenL )) { fnToken = processor.tokenAtRelativeIndex(2); iterator += 3; } else if (processor.matches3(TokenType._new, TokenType.name, TokenType.parenL)) { fnToken = processor.tokenAtRelativeIndex(1); iterator += 2; } if (!fnToken || processor.identifierNameForToken(fnToken) !== "URL") { return; } const args = []; let currentArg = []; let currentToken = processor.tokens[++iterator]; while (currentToken && currentToken.type !== TokenType.parenR) { if (currentToken.type === TokenType.comma) { if (!currentArg.length) { return; } args.push(currentArg); currentArg = []; currentToken = processor.tokens[++iterator]; continue; } if (args.length === 0) { if (currentToken.type !== TokenType.string && currentToken.type !== TokenType.name) { return; } } if (args.length === 1) { if (currentArg.length > 5) { return; } if (currentArg.length === 0 && (currentToken.type !== TokenType.name || processor.identifierNameForToken(currentToken) !== "import")) { return; } if (currentArg.length === 1 && currentToken.type !== TokenType.dot) { return; } if (currentArg.length === 2 && (currentToken.type !== TokenType.name || processor.identifierNameForToken(currentToken) !== "meta")) { return; } if (currentArg.length === 3 && currentToken.type !== TokenType.dot) { return; } if (currentArg.length === 4 && (currentToken.type !== TokenType.name || processor.identifierNameForToken(currentToken) !== "url")) { return; } } if (args.length === 2) { return; } currentArg.push(currentToken); currentToken = processor.tokens[++iterator]; } if (args.length !== 1) { return; } const firstArg = args[0][0]; if (firstArg.type !== TokenType.string) { return getIdentifierValue(processor, firstArg); } return processor.stringValueForToken(firstArg); } // src/esbuild.mts import { fileToId, getAttribute, isRelativePath } from "@htmelt/plugin"; import * as esbuild from "esbuild"; import { wrapPlugins } from "esbuild-extra"; import { readFileSync, writeFileSync } from "fs"; import { yellow } from "kleur/colors"; import * as path2 from "path"; async function compileSeparateEntry(file, config, { watch, ...options } = {}) { const filePath = decodeURIComponent(new URL(file, import.meta.url).pathname); const esbuildOptions = wrapPlugins({ ...config.esbuild, ...options, format: options.format ?? "iife", plugins: options.plugins || config.esbuild.plugins?.filter((p) => p.name !== "dev-exports"), sourcemap: options.sourcemap ?? (config.mode == "development" ? "inline" : false), bundle: true, write: false, entryPoints: [filePath] }); let result; if (watch) { const context2 = await esbuild.context(esbuildOptions); result = await context2.rebuild(); result.context = context2; } else { result = await esbuild.build(esbuildOptions); } if (options.sourcemap === true || options.metafile === true) { return result; } return result.outputFiles[0].text; } function findRelativeScripts(document, file, config) { const results = []; for (const scriptNode of findExternalScripts(document)) { const srcAttr = scriptNode.attrs.find((a) => a.name === "src"); if (srcAttr && isRelativePath(srcAttr.value)) { const srcPath = path2.join(path2.dirname(file), srcAttr.value); results.push({ node: scriptNode, srcAttr, srcPath, outPath: config.getBuildPath(srcPath), isModule: getAttribute(scriptNode, "type") === "module" }); } } return results; } function buildEntryScripts(scripts, isStandalone, config, flags = {}, bundle) { for (const srcPath of scripts) { console.log(yellow("\u2301"), fileToId(srcPath)); } let plugins = config.esbuild.plugins || []; plugins = [ ...plugins, importMetaUrl_default(), importGlob_default(config.relatedWatcher) ]; if (bundle) { plugins.unshift(assignBundlePlugin(bundle)); } if (flags.watch && isStandalone) { plugins.push(standAloneScriptPlugin(isStandalone, config)); } return esbuild.context( wrapPlugins({ format: "esm", charset: "utf8", sourcemap: config.mode == "development", minify: flags.minify, ...config.esbuild, entryPoints: [...scripts], entryNames: "[dir]/[name]" + (flags.watch ? "" : ".[hash]"), outbase: config.src, outdir: config.build, metafile: true, write: true, bundle: true, splitting: true, treeShaking: !flags.watch, ignoreAnnotations: flags.watch, plugins }) ); } function assignBundlePlugin(bundle) { return { name: "htmelt/assignBundle", setup(build2) { esbuildBundles.set(build2.initialOptions, bundle); } }; } function standAloneScriptPlugin(isStandalone, config) { return { name: "htmelt/standaloneScripts", setup(build2) { let stubPath; build2.onEnd(({ metafile }) => { if (!metafile) return; for (let [outFile, output] of Object.entries(metafile.outputs)) { if (!output.entryPoint) continue; const entry = path2.resolve(output.entryPoint); if (!isStandalone(entry)) continue; if (!stubPath) { stubPath = path2.join(config.build, "htmelt-stub.js"); writeFileSync(stubPath, "globalThis.htmelt = {export(){}};"); } let stubImportId = path2.relative(path2.dirname(outFile), stubPath); if (stubImportId[0] !== ".") { stubImportId = "./" + stubImportId; } outFile = path2.resolve(outFile); let code = readFileSync(outFile, "utf8"); code = `import "${stubImportId}"; ` + code; writeFileSync(outFile, code); } }); } }; } export { importGlob_default, importMetaUrl_default, compileSeparateEntry, findRelativeScripts, buildEntryScripts };