htmelt
Version:
Bundle your HTML assets with Esbuild and LightningCSS. Custom plugins, HMR platform, and more.
628 lines (615 loc) • 20.8 kB
JavaScript
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
};