jq79 0.3.32 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dev/dev.ts +12 -2
- package/dev/vite.ts +49 -9
- package/dist/cli.js +8 -2
- package/dist/dev.cjs +8 -2
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.js +8 -2
- package/dist/dev.js.map +1 -1
- package/dist/jq79.cjs +10 -9
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.global.js +10 -9
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +10 -9
- package/dist/jq79.js.map +1 -1
- package/dist/reactive.d.ts +1 -0
- package/dist/vite.cjs +55 -7
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.js +55 -7
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
- package/src/jq79.ts +137 -36
- package/src/reactive.ts +83 -7
- package/src/transform.ts +102 -1
package/dist/reactive.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const $reactive: <T extends Record<string, any>>(data: T) => Reac
|
|
|
15
15
|
export type EffectScope = {
|
|
16
16
|
effect: (run: () => void) => void;
|
|
17
17
|
onDispose: (fn: Unsubscribe) => void;
|
|
18
|
+
refresh: () => void;
|
|
18
19
|
dispose: () => void;
|
|
19
20
|
};
|
|
20
21
|
export declare const createEffectScope: (scope: Record<string, any>) => EffectScope;
|
package/dist/vite.cjs
CHANGED
|
@@ -28,18 +28,66 @@ var import_node_path = require("path");
|
|
|
28
28
|
var import_vite = require("vite");
|
|
29
29
|
var COMPONENT_QUERY = "?jq79";
|
|
30
30
|
var SCRIPT_BLOCK_RE = /<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi;
|
|
31
|
-
var
|
|
32
|
-
var
|
|
31
|
+
var IMPORT_CALL_RE = /import\s*\(\s*(["'])([^"'\n]+?)\1\s*\)/y;
|
|
32
|
+
var STATIC_IMPORT_RE = /import\s*(?:[\w$\s,{}*]+?\s*from\s*)?(["'])([^"'\n]+)\1/y;
|
|
33
|
+
var skipString = (src, start) => {
|
|
34
|
+
const quote = src[start];
|
|
35
|
+
let i = start + 1;
|
|
36
|
+
while (i < src.length) {
|
|
37
|
+
if (src[i] === "\\") {
|
|
38
|
+
i += 2;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (src[i] === quote) return i + 1;
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
return src.length;
|
|
45
|
+
};
|
|
46
|
+
var importSpecifiers = (script) => {
|
|
47
|
+
const specs = [];
|
|
48
|
+
let i = 0;
|
|
49
|
+
while (i < script.length) {
|
|
50
|
+
const ch = script[i];
|
|
51
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
52
|
+
i = skipString(script, i);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === "/" && script[i + 1] === "/") {
|
|
56
|
+
const end = script.indexOf("\n", i);
|
|
57
|
+
i = end === -1 ? script.length : end + 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (ch === "/" && script[i + 1] === "*") {
|
|
61
|
+
const end = script.indexOf("*/", i + 2);
|
|
62
|
+
i = end === -1 ? script.length : end + 2;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === "i" && (i === 0 || !/[\w$.]/.test(script[i - 1]))) {
|
|
66
|
+
IMPORT_CALL_RE.lastIndex = i;
|
|
67
|
+
const call = IMPORT_CALL_RE.exec(script);
|
|
68
|
+
if (call) {
|
|
69
|
+
specs.push(call[2]);
|
|
70
|
+
i = IMPORT_CALL_RE.lastIndex;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
STATIC_IMPORT_RE.lastIndex = i;
|
|
74
|
+
const staticImport = STATIC_IMPORT_RE.exec(script);
|
|
75
|
+
if (staticImport) {
|
|
76
|
+
specs.push(staticImport[2]);
|
|
77
|
+
i = STATIC_IMPORT_RE.lastIndex;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
return specs;
|
|
84
|
+
};
|
|
33
85
|
var isHtmlUrl = (spec) => /\.html?([?#]|$)/.test(spec);
|
|
34
86
|
var isExternalUrl = (spec) => /^[a-z][a-z0-9+.-]*:/i.test(spec) || spec.startsWith("/");
|
|
35
87
|
var hoistableImports = (source, include) => {
|
|
36
88
|
const specifiers = /* @__PURE__ */ new Set();
|
|
37
89
|
for (const [, script] of source.matchAll(SCRIPT_BLOCK_RE)) {
|
|
38
|
-
const
|
|
39
|
-
...[...script.matchAll(IMPORT_LITERAL_RE)].map((match) => match[2]),
|
|
40
|
-
...[...script.matchAll(STATIC_IMPORT_LITERAL_RE)].map((match) => match[2])
|
|
41
|
-
];
|
|
42
|
-
for (const spec of specs) {
|
|
90
|
+
for (const spec of importSpecifiers(script)) {
|
|
43
91
|
if (isExternalUrl(spec)) continue;
|
|
44
92
|
if (isHtmlUrl(spec) && !include.test(spec)) continue;
|
|
45
93
|
specifiers.add(spec);
|
package/dist/vite.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../dev/vite.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\"\nimport { relative } from \"node:path\"\nimport { preprocessCSS } from \"vite\"\nimport type { Plugin, ResolvedConfig } from \"vite\"\n\n// Vite plugin: import .html single-file components as modules.\n//\n// import { jq79 } from \"jq79/vite\" // vite.config\n// import UserCard from \"./UserCard.html\" // app code\n//\n// The imported value is a Component79 built from the file's source - the same\n// thing `await Component79.fetch(url)` resolves to, but bundled at build time\n// instead of fetched at runtime. The component source is inlined verbatim, so\n// a file keeps working unchanged if it's ever served from public/ and loaded\n// with fetch instead - with one deliberate exception: <style lang=\"scss\"> (or\n// less/stylus/sass) is compiled to plain CSS here. A component using `lang`\n// therefore only works through the bundler; loaded with fetch() it would\n// reach the runtime uncompiled, which the runtime warns about.\n//\n// Only .html files imported from other modules are claimed; entry points\n// (index.html) have no importer and imports carrying an explicit query\n// (?raw, ?url) keep their built-in Vite meaning.\n\nexport interface Jq79PluginOptions {\n // which import specifiers are treated as components (default: any .html)\n include?: RegExp\n // resolved absolute paths to skip even when `include` matches\n exclude?: RegExp\n}\n\n// claimed modules get this suffix so their id no longer ends in \".html\" and\n// Vite's own html handling (entries, asset pipeline) leaves them alone\nconst COMPONENT_QUERY = \"?jq79\"\n\nconst SCRIPT_BLOCK_RE = /<script\\b[^>]*>([\\s\\S]*?)<\\/script\\s*>/gi\n// import(\"...\") with a literal specifier; the lookbehind skips $__import and\n// property accesses like foo.import(...)\nconst IMPORT_LITERAL_RE = /(?<![\\w$.])import\\s*\\(\\s*([\"'])([^\"'\\n]+?)\\1\\s*\\)/g\n// static import statements (factory scripts): optional clause + literal\n// specifier. The clause can't contain parens/quotes, so dynamic import()\n// and import.meta never match\nconst STATIC_IMPORT_LITERAL_RE = /(?<![\\w$.])import\\s*(?:[\\w$\\s,{}*]+?\\s*from\\s*)?([\"'])([^\"'\\n]+)\\1/g\n\nconst isHtmlUrl = (spec: string) => /\\.html?([?#]|$)/.test(spec)\nconst isExternalUrl = (spec: string) => /^[a-z][a-z0-9+.-]*:/i.test(spec) || spec.startsWith(\"/\")\n\n// literal import specifiers in the component's script blocks - dynamic\n// `import(\"...\")` calls and static factory-script imports - that should\n// resolve from the bundle instead of at runtime. Absolute paths and full\n// URLs are left alone (they point at served files, e.g. public/), and so\n// are .html specifiers the plugin wouldn't claim as components\nconst hoistableImports = (source: string, include: RegExp): string[] => {\n const specifiers = new Set<string>()\n for (const [, script] of source.matchAll(SCRIPT_BLOCK_RE)) {\n const specs = [\n ...[...script.matchAll(IMPORT_LITERAL_RE)].map(match => match[2]),\n ...[...script.matchAll(STATIC_IMPORT_LITERAL_RE)].map(match => match[2]),\n ]\n for (const spec of specs) {\n if (isExternalUrl(spec)) continue\n if (isHtmlUrl(spec) && !include.test(spec)) continue // html left to runtime fetch\n specifiers.add(spec) // a claimed component, a source file or an npm package\n }\n }\n return [...specifiers]\n}\n\n// a <style> block with its attribute string, so `lang` can be read and the\n// content replaced. Attribute values are matched as quoted chunks so a \">\"\n// inside one doesn't end the tag early\nconst STYLE_BLOCK_RE = /<style((?:\"[^\"]*\"|'[^']*'|[^>\"'])*)>([\\s\\S]*?)<\\/style\\s*>/gi\nconst LANG_ATTR_RE = /\\blang\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/i\n\n// compiles <style lang=\"scss|less|styl|sass\"> blocks to plain CSS with Vite's\n// own preprocessing (the same call @vitejs/plugin-vue makes), so the runtime\n// only ever sees CSS. The preprocessor picks its parser from the extension,\n// and resolving relative @use/@import against a filename in the component's\n// own directory is what makes `@use \"./vars\"` work. Files the preprocessor\n// pulls in are registered as watch deps, so editing a partial re-runs HMR for\n// every component that uses it. `lang` is dropped from the emitted tag: what\n// the runtime parses is a plain <style> (with `scoped` and the rest intact)\nconst compileStyleBlocks = async (\n source: string,\n file: string,\n config: ResolvedConfig,\n addWatchFile: (id: string) => void\n): Promise<string> => {\n const blocks = [...source.matchAll(STYLE_BLOCK_RE)]\n const compiled = await Promise.all(\n blocks.map(async ([, attrs, content]) => {\n const lang = attrs.match(LANG_ATTR_RE)\n if (!lang) return null\n const extension = lang[1] ?? lang[2] ?? lang[3]\n const result = await preprocessCSS(content, `${file}.${extension}`, config)\n result.deps?.forEach(addWatchFile)\n return { attrs: attrs.replace(LANG_ATTR_RE, \"\").trimEnd(), css: result.code }\n })\n )\n\n let out = \"\"\n let last = 0\n blocks.forEach((block, i) => {\n const done = compiled[i]\n if (!done) return\n out += source.slice(last, block.index) + `<style${done.attrs}>${done.css}</style>`\n last = block.index + block[0].length\n })\n return out + source.slice(last)\n}\n\n// the emitted module. Literal import(\"...\") specifiers found in the\n// component's scripts become real module imports, handed to Component79 as a\n// resolution map: at runtime $__import checks the map before falling back to\n// fetch, so bundled components ship with their imports and nothing changes\n// for unbundled ones. Claimed components import as their default (a\n// Component79, matching what runtime fetch resolves to); everything else as\n// a namespace (matching native import()).\n//\n// In dev, `hot.data` carries the exported instance across updates: importers\n// hold a reference to the *first* module evaluation's instance, so later\n// evaluations patch that same instance in place instead of exporting a new one\n// nobody sees. The patching itself is the runtime's `hotReplace` - the same\n// swap the jq79/dev server drives, from the one place that can reach a\n// component's markers. An instance only used as a definition has nothing to\n// re-render (nested clones can't be reached from this module), so it falls\n// back to a full reload.\nconst componentModule = (source: string, include: RegExp, filename: string): string => {\n const hoisted = hoistableImports(source, include)\n const imports = hoisted\n .map((spec, i) =>\n include.test(spec)\n ? `import __jq79_${i} from ${JSON.stringify(spec)}`\n : `import * as __jq79_${i} from ${JSON.stringify(spec)}`\n )\n .join(\"\\n\")\n const modulesMap = `{ ${hoisted.map((spec, i) => `${JSON.stringify(spec)}: __jq79_${i}`).join(\", \")} }`\n\n return `\nimport { Component79 } from \"jq79\"\n${imports}\n\nconst src = ${JSON.stringify(source)}\nconst modules = ${modulesMap}\nconst filename = ${JSON.stringify(filename)}\n\nlet component\n\nif (import.meta.hot && import.meta.hot.data.component) {\n const prior = import.meta.hot.data.component\n prior.modules = modules\n prior.filename = filename\n // re-renders it where it stands, keeping its data. false means it was never\n // rendered - a definition used only as a nested component - and a reload is\n // the only way to reach the clones made from it\n if (!prior.hotReplace(src) && !prior.data) import.meta.hot.invalidate()\n component = prior\n} else {\n component = new Component79(src, { modules, filename })\n}\n\nif (import.meta.hot) {\n import.meta.hot.data.component = component\n import.meta.hot.accept()\n}\n\nexport default component\n`\n}\n\nexport function jq79(options: Jq79PluginOptions = {}): Plugin {\n const include = options.include ?? /\\.html$/\n const { exclude } = options\n\n let config: ResolvedConfig | null = null\n\n return {\n name: \"jq79\",\n enforce: \"pre\",\n\n configResolved(resolved) {\n config = resolved\n },\n\n async resolveId(source, importer) {\n if (!importer) return null // entry points are never components\n if (source.includes(\"?\")) return null // ?raw, ?url, ... keep their meaning\n if (!include.test(source)) return null\n\n const resolved = await this.resolve(source, importer, { skipSelf: true })\n if (!resolved || resolved.external) return null\n if (exclude?.test(resolved.id)) return null\n return resolved.id + COMPONENT_QUERY\n },\n\n async load(id) {\n if (!id.endsWith(COMPONENT_QUERY)) return null\n const file = id.slice(0, -COMPONENT_QUERY.length)\n\n let source = await readFile(file, \"utf8\")\n if (config) source = await compileStyleBlocks(source, file, config, dep => this.addWatchFile(dep))\n\n // the runtime names the component's setup scripts after this, so devtools\n // shows a path the user recognizes instead of an anonymous VM script\n const filename = config ? relative(config.root, file) : file\n\n return { code: componentModule(source, include, filename), map: null }\n },\n }\n}\n\nexport default jq79\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../dev/vite.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\"\nimport { relative } from \"node:path\"\nimport { preprocessCSS } from \"vite\"\nimport type { Plugin, ResolvedConfig } from \"vite\"\n\n// Vite plugin: import .html single-file components as modules.\n//\n// import { jq79 } from \"jq79/vite\" // vite.config\n// import UserCard from \"./UserCard.html\" // app code\n//\n// The imported value is a Component79 built from the file's source - the same\n// thing `await Component79.fetch(url)` resolves to, but bundled at build time\n// instead of fetched at runtime. The component source is inlined verbatim, so\n// a file keeps working unchanged if it's ever served from public/ and loaded\n// with fetch instead - with one deliberate exception: <style lang=\"scss\"> (or\n// less/stylus/sass) is compiled to plain CSS here. A component using `lang`\n// therefore only works through the bundler; loaded with fetch() it would\n// reach the runtime uncompiled, which the runtime warns about.\n//\n// Only .html files imported from other modules are claimed; entry points\n// (index.html) have no importer and imports carrying an explicit query\n// (?raw, ?url) keep their built-in Vite meaning.\n\nexport interface Jq79PluginOptions {\n // which import specifiers are treated as components (default: any .html)\n include?: RegExp\n // resolved absolute paths to skip even when `include` matches\n exclude?: RegExp\n}\n\n// claimed modules get this suffix so their id no longer ends in \".html\" and\n// Vite's own html handling (entries, asset pipeline) leaves them alone\nconst COMPONENT_QUERY = \"?jq79\"\n\nconst SCRIPT_BLOCK_RE = /<script\\b[^>]*>([\\s\\S]*?)<\\/script\\s*>/gi\n// import(\"...\") with a literal specifier, tried at word boundaries the\n// scanner below reaches (which is what skips $__import and foo.import(...))\nconst IMPORT_CALL_RE = /import\\s*\\(\\s*([\"'])([^\"'\\n]+?)\\1\\s*\\)/y\n// static import statements (factory scripts): optional clause + literal\n// specifier. The clause can't contain parens/quotes, so dynamic import()\n// and import.meta never match\nconst STATIC_IMPORT_RE = /import\\s*(?:[\\w$\\s,{}*]+?\\s*from\\s*)?([\"'])([^\"'\\n]+)\\1/y\n\nconst skipString = (src: string, start: number): number => {\n const quote = src[start]\n let i = start + 1\n while (i < src.length) {\n if (src[i] === \"\\\\\") { i += 2; continue }\n if (src[i] === quote) return i + 1\n i++\n }\n return src.length\n}\n\n// the literal import specifiers in one script body. A scanner rather than a\n// bare matchAll, because a specifier mentioned in a comment or a string is\n// not an import: hoisting a commented-out `import(\"./old.html\")` would pull\n// dead files into the bundle - or break the build once the file is gone\nconst importSpecifiers = (script: string): string[] => {\n const specs: string[] = []\n let i = 0\n while (i < script.length) {\n const ch = script[i]\n if (ch === \"'\" || ch === '\"' || ch === \"`\") { i = skipString(script, i); continue }\n if (ch === \"/\" && script[i + 1] === \"/\") {\n const end = script.indexOf(\"\\n\", i)\n i = end === -1 ? script.length : end + 1\n continue\n }\n if (ch === \"/\" && script[i + 1] === \"*\") {\n const end = script.indexOf(\"*/\", i + 2)\n i = end === -1 ? script.length : end + 2\n continue\n }\n if (ch === \"i\" && (i === 0 || !/[\\w$.]/.test(script[i - 1]))) {\n IMPORT_CALL_RE.lastIndex = i\n const call = IMPORT_CALL_RE.exec(script)\n if (call) { specs.push(call[2]); i = IMPORT_CALL_RE.lastIndex; continue }\n STATIC_IMPORT_RE.lastIndex = i\n const staticImport = STATIC_IMPORT_RE.exec(script)\n if (staticImport) { specs.push(staticImport[2]); i = STATIC_IMPORT_RE.lastIndex; continue }\n }\n i++\n }\n return specs\n}\n\nconst isHtmlUrl = (spec: string) => /\\.html?([?#]|$)/.test(spec)\nconst isExternalUrl = (spec: string) => /^[a-z][a-z0-9+.-]*:/i.test(spec) || spec.startsWith(\"/\")\n\n// literal import specifiers in the component's script blocks - dynamic\n// `import(\"...\")` calls and static factory-script imports - that should\n// resolve from the bundle instead of at runtime. Absolute paths and full\n// URLs are left alone (they point at served files, e.g. public/), and so\n// are .html specifiers the plugin wouldn't claim as components\nconst hoistableImports = (source: string, include: RegExp): string[] => {\n const specifiers = new Set<string>()\n for (const [, script] of source.matchAll(SCRIPT_BLOCK_RE)) {\n for (const spec of importSpecifiers(script)) {\n if (isExternalUrl(spec)) continue\n if (isHtmlUrl(spec) && !include.test(spec)) continue // html left to runtime fetch\n specifiers.add(spec) // a claimed component, a source file or an npm package\n }\n }\n return [...specifiers]\n}\n\n// a <style> block with its attribute string, so `lang` can be read and the\n// content replaced. Attribute values are matched as quoted chunks so a \">\"\n// inside one doesn't end the tag early\nconst STYLE_BLOCK_RE = /<style((?:\"[^\"]*\"|'[^']*'|[^>\"'])*)>([\\s\\S]*?)<\\/style\\s*>/gi\nconst LANG_ATTR_RE = /\\blang\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/i\n\n// compiles <style lang=\"scss|less|styl|sass\"> blocks to plain CSS with Vite's\n// own preprocessing (the same call @vitejs/plugin-vue makes), so the runtime\n// only ever sees CSS. The preprocessor picks its parser from the extension,\n// and resolving relative @use/@import against a filename in the component's\n// own directory is what makes `@use \"./vars\"` work. Files the preprocessor\n// pulls in are registered as watch deps, so editing a partial re-runs HMR for\n// every component that uses it. `lang` is dropped from the emitted tag: what\n// the runtime parses is a plain <style> (with `scoped` and the rest intact)\nconst compileStyleBlocks = async (\n source: string,\n file: string,\n config: ResolvedConfig,\n addWatchFile: (id: string) => void\n): Promise<string> => {\n const blocks = [...source.matchAll(STYLE_BLOCK_RE)]\n const compiled = await Promise.all(\n blocks.map(async ([, attrs, content]) => {\n const lang = attrs.match(LANG_ATTR_RE)\n if (!lang) return null\n const extension = lang[1] ?? lang[2] ?? lang[3]\n const result = await preprocessCSS(content, `${file}.${extension}`, config)\n result.deps?.forEach(addWatchFile)\n return { attrs: attrs.replace(LANG_ATTR_RE, \"\").trimEnd(), css: result.code }\n })\n )\n\n let out = \"\"\n let last = 0\n blocks.forEach((block, i) => {\n const done = compiled[i]\n if (!done) return\n out += source.slice(last, block.index) + `<style${done.attrs}>${done.css}</style>`\n last = block.index + block[0].length\n })\n return out + source.slice(last)\n}\n\n// the emitted module. Literal import(\"...\") specifiers found in the\n// component's scripts become real module imports, handed to Component79 as a\n// resolution map: at runtime $__import checks the map before falling back to\n// fetch, so bundled components ship with their imports and nothing changes\n// for unbundled ones. Claimed components import as their default (a\n// Component79, matching what runtime fetch resolves to); everything else as\n// a namespace (matching native import()).\n//\n// In dev, `hot.data` carries the exported instance across updates: importers\n// hold a reference to the *first* module evaluation's instance, so later\n// evaluations patch that same instance in place instead of exporting a new one\n// nobody sees. The patching itself is the runtime's `hotReplace` - the same\n// swap the jq79/dev server drives, from the one place that can reach a\n// component's markers. An instance only used as a definition has nothing to\n// re-render (nested clones can't be reached from this module), so it falls\n// back to a full reload.\nconst componentModule = (source: string, include: RegExp, filename: string): string => {\n const hoisted = hoistableImports(source, include)\n const imports = hoisted\n .map((spec, i) =>\n include.test(spec)\n ? `import __jq79_${i} from ${JSON.stringify(spec)}`\n : `import * as __jq79_${i} from ${JSON.stringify(spec)}`\n )\n .join(\"\\n\")\n const modulesMap = `{ ${hoisted.map((spec, i) => `${JSON.stringify(spec)}: __jq79_${i}`).join(\", \")} }`\n\n return `\nimport { Component79 } from \"jq79\"\n${imports}\n\nconst src = ${JSON.stringify(source)}\nconst modules = ${modulesMap}\nconst filename = ${JSON.stringify(filename)}\n\nlet component\n\nif (import.meta.hot && import.meta.hot.data.component) {\n const prior = import.meta.hot.data.component\n prior.modules = modules\n prior.filename = filename\n // re-renders it where it stands, keeping its data. false means it was never\n // rendered - a definition used only as a nested component - and a reload is\n // the only way to reach the clones made from it\n if (!prior.hotReplace(src) && !prior.data) import.meta.hot.invalidate()\n component = prior\n} else {\n component = new Component79(src, { modules, filename })\n}\n\nif (import.meta.hot) {\n import.meta.hot.data.component = component\n import.meta.hot.accept()\n}\n\nexport default component\n`\n}\n\nexport function jq79(options: Jq79PluginOptions = {}): Plugin {\n const include = options.include ?? /\\.html$/\n const { exclude } = options\n\n let config: ResolvedConfig | null = null\n\n return {\n name: \"jq79\",\n enforce: \"pre\",\n\n configResolved(resolved) {\n config = resolved\n },\n\n async resolveId(source, importer) {\n if (!importer) return null // entry points are never components\n if (source.includes(\"?\")) return null // ?raw, ?url, ... keep their meaning\n if (!include.test(source)) return null\n\n const resolved = await this.resolve(source, importer, { skipSelf: true })\n if (!resolved || resolved.external) return null\n if (exclude?.test(resolved.id)) return null\n return resolved.id + COMPONENT_QUERY\n },\n\n async load(id) {\n if (!id.endsWith(COMPONENT_QUERY)) return null\n const file = id.slice(0, -COMPONENT_QUERY.length)\n\n let source = await readFile(file, \"utf8\")\n if (config) source = await compileStyleBlocks(source, file, config, dep => this.addWatchFile(dep))\n\n // the runtime names the component's setup scripts after this, so devtools\n // shows a path the user recognizes instead of an anonymous VM script\n const filename = config ? relative(config.root, file) : file\n\n return { code: componentModule(source, include, filename), map: null }\n },\n }\n}\n\nexport default jq79\n"],"mappings":"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","names":[]}
|
package/dist/vite.js
CHANGED
|
@@ -4,18 +4,66 @@ import { relative } from "path";
|
|
|
4
4
|
import { preprocessCSS } from "vite";
|
|
5
5
|
var COMPONENT_QUERY = "?jq79";
|
|
6
6
|
var SCRIPT_BLOCK_RE = /<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi;
|
|
7
|
-
var
|
|
8
|
-
var
|
|
7
|
+
var IMPORT_CALL_RE = /import\s*\(\s*(["'])([^"'\n]+?)\1\s*\)/y;
|
|
8
|
+
var STATIC_IMPORT_RE = /import\s*(?:[\w$\s,{}*]+?\s*from\s*)?(["'])([^"'\n]+)\1/y;
|
|
9
|
+
var skipString = (src, start) => {
|
|
10
|
+
const quote = src[start];
|
|
11
|
+
let i = start + 1;
|
|
12
|
+
while (i < src.length) {
|
|
13
|
+
if (src[i] === "\\") {
|
|
14
|
+
i += 2;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (src[i] === quote) return i + 1;
|
|
18
|
+
i++;
|
|
19
|
+
}
|
|
20
|
+
return src.length;
|
|
21
|
+
};
|
|
22
|
+
var importSpecifiers = (script) => {
|
|
23
|
+
const specs = [];
|
|
24
|
+
let i = 0;
|
|
25
|
+
while (i < script.length) {
|
|
26
|
+
const ch = script[i];
|
|
27
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
28
|
+
i = skipString(script, i);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (ch === "/" && script[i + 1] === "/") {
|
|
32
|
+
const end = script.indexOf("\n", i);
|
|
33
|
+
i = end === -1 ? script.length : end + 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (ch === "/" && script[i + 1] === "*") {
|
|
37
|
+
const end = script.indexOf("*/", i + 2);
|
|
38
|
+
i = end === -1 ? script.length : end + 2;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ch === "i" && (i === 0 || !/[\w$.]/.test(script[i - 1]))) {
|
|
42
|
+
IMPORT_CALL_RE.lastIndex = i;
|
|
43
|
+
const call = IMPORT_CALL_RE.exec(script);
|
|
44
|
+
if (call) {
|
|
45
|
+
specs.push(call[2]);
|
|
46
|
+
i = IMPORT_CALL_RE.lastIndex;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
STATIC_IMPORT_RE.lastIndex = i;
|
|
50
|
+
const staticImport = STATIC_IMPORT_RE.exec(script);
|
|
51
|
+
if (staticImport) {
|
|
52
|
+
specs.push(staticImport[2]);
|
|
53
|
+
i = STATIC_IMPORT_RE.lastIndex;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
return specs;
|
|
60
|
+
};
|
|
9
61
|
var isHtmlUrl = (spec) => /\.html?([?#]|$)/.test(spec);
|
|
10
62
|
var isExternalUrl = (spec) => /^[a-z][a-z0-9+.-]*:/i.test(spec) || spec.startsWith("/");
|
|
11
63
|
var hoistableImports = (source, include) => {
|
|
12
64
|
const specifiers = /* @__PURE__ */ new Set();
|
|
13
65
|
for (const [, script] of source.matchAll(SCRIPT_BLOCK_RE)) {
|
|
14
|
-
const
|
|
15
|
-
...[...script.matchAll(IMPORT_LITERAL_RE)].map((match) => match[2]),
|
|
16
|
-
...[...script.matchAll(STATIC_IMPORT_LITERAL_RE)].map((match) => match[2])
|
|
17
|
-
];
|
|
18
|
-
for (const spec of specs) {
|
|
66
|
+
for (const spec of importSpecifiers(script)) {
|
|
19
67
|
if (isExternalUrl(spec)) continue;
|
|
20
68
|
if (isHtmlUrl(spec) && !include.test(spec)) continue;
|
|
21
69
|
specifiers.add(spec);
|
package/dist/vite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../dev/vite.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\"\nimport { relative } from \"node:path\"\nimport { preprocessCSS } from \"vite\"\nimport type { Plugin, ResolvedConfig } from \"vite\"\n\n// Vite plugin: import .html single-file components as modules.\n//\n// import { jq79 } from \"jq79/vite\" // vite.config\n// import UserCard from \"./UserCard.html\" // app code\n//\n// The imported value is a Component79 built from the file's source - the same\n// thing `await Component79.fetch(url)` resolves to, but bundled at build time\n// instead of fetched at runtime. The component source is inlined verbatim, so\n// a file keeps working unchanged if it's ever served from public/ and loaded\n// with fetch instead - with one deliberate exception: <style lang=\"scss\"> (or\n// less/stylus/sass) is compiled to plain CSS here. A component using `lang`\n// therefore only works through the bundler; loaded with fetch() it would\n// reach the runtime uncompiled, which the runtime warns about.\n//\n// Only .html files imported from other modules are claimed; entry points\n// (index.html) have no importer and imports carrying an explicit query\n// (?raw, ?url) keep their built-in Vite meaning.\n\nexport interface Jq79PluginOptions {\n // which import specifiers are treated as components (default: any .html)\n include?: RegExp\n // resolved absolute paths to skip even when `include` matches\n exclude?: RegExp\n}\n\n// claimed modules get this suffix so their id no longer ends in \".html\" and\n// Vite's own html handling (entries, asset pipeline) leaves them alone\nconst COMPONENT_QUERY = \"?jq79\"\n\nconst SCRIPT_BLOCK_RE = /<script\\b[^>]*>([\\s\\S]*?)<\\/script\\s*>/gi\n// import(\"...\") with a literal specifier; the lookbehind skips $__import and\n// property accesses like foo.import(...)\nconst IMPORT_LITERAL_RE = /(?<![\\w$.])import\\s*\\(\\s*([\"'])([^\"'\\n]+?)\\1\\s*\\)/g\n// static import statements (factory scripts): optional clause + literal\n// specifier. The clause can't contain parens/quotes, so dynamic import()\n// and import.meta never match\nconst STATIC_IMPORT_LITERAL_RE = /(?<![\\w$.])import\\s*(?:[\\w$\\s,{}*]+?\\s*from\\s*)?([\"'])([^\"'\\n]+)\\1/g\n\nconst isHtmlUrl = (spec: string) => /\\.html?([?#]|$)/.test(spec)\nconst isExternalUrl = (spec: string) => /^[a-z][a-z0-9+.-]*:/i.test(spec) || spec.startsWith(\"/\")\n\n// literal import specifiers in the component's script blocks - dynamic\n// `import(\"...\")` calls and static factory-script imports - that should\n// resolve from the bundle instead of at runtime. Absolute paths and full\n// URLs are left alone (they point at served files, e.g. public/), and so\n// are .html specifiers the plugin wouldn't claim as components\nconst hoistableImports = (source: string, include: RegExp): string[] => {\n const specifiers = new Set<string>()\n for (const [, script] of source.matchAll(SCRIPT_BLOCK_RE)) {\n const specs = [\n ...[...script.matchAll(IMPORT_LITERAL_RE)].map(match => match[2]),\n ...[...script.matchAll(STATIC_IMPORT_LITERAL_RE)].map(match => match[2]),\n ]\n for (const spec of specs) {\n if (isExternalUrl(spec)) continue\n if (isHtmlUrl(spec) && !include.test(spec)) continue // html left to runtime fetch\n specifiers.add(spec) // a claimed component, a source file or an npm package\n }\n }\n return [...specifiers]\n}\n\n// a <style> block with its attribute string, so `lang` can be read and the\n// content replaced. Attribute values are matched as quoted chunks so a \">\"\n// inside one doesn't end the tag early\nconst STYLE_BLOCK_RE = /<style((?:\"[^\"]*\"|'[^']*'|[^>\"'])*)>([\\s\\S]*?)<\\/style\\s*>/gi\nconst LANG_ATTR_RE = /\\blang\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/i\n\n// compiles <style lang=\"scss|less|styl|sass\"> blocks to plain CSS with Vite's\n// own preprocessing (the same call @vitejs/plugin-vue makes), so the runtime\n// only ever sees CSS. The preprocessor picks its parser from the extension,\n// and resolving relative @use/@import against a filename in the component's\n// own directory is what makes `@use \"./vars\"` work. Files the preprocessor\n// pulls in are registered as watch deps, so editing a partial re-runs HMR for\n// every component that uses it. `lang` is dropped from the emitted tag: what\n// the runtime parses is a plain <style> (with `scoped` and the rest intact)\nconst compileStyleBlocks = async (\n source: string,\n file: string,\n config: ResolvedConfig,\n addWatchFile: (id: string) => void\n): Promise<string> => {\n const blocks = [...source.matchAll(STYLE_BLOCK_RE)]\n const compiled = await Promise.all(\n blocks.map(async ([, attrs, content]) => {\n const lang = attrs.match(LANG_ATTR_RE)\n if (!lang) return null\n const extension = lang[1] ?? lang[2] ?? lang[3]\n const result = await preprocessCSS(content, `${file}.${extension}`, config)\n result.deps?.forEach(addWatchFile)\n return { attrs: attrs.replace(LANG_ATTR_RE, \"\").trimEnd(), css: result.code }\n })\n )\n\n let out = \"\"\n let last = 0\n blocks.forEach((block, i) => {\n const done = compiled[i]\n if (!done) return\n out += source.slice(last, block.index) + `<style${done.attrs}>${done.css}</style>`\n last = block.index + block[0].length\n })\n return out + source.slice(last)\n}\n\n// the emitted module. Literal import(\"...\") specifiers found in the\n// component's scripts become real module imports, handed to Component79 as a\n// resolution map: at runtime $__import checks the map before falling back to\n// fetch, so bundled components ship with their imports and nothing changes\n// for unbundled ones. Claimed components import as their default (a\n// Component79, matching what runtime fetch resolves to); everything else as\n// a namespace (matching native import()).\n//\n// In dev, `hot.data` carries the exported instance across updates: importers\n// hold a reference to the *first* module evaluation's instance, so later\n// evaluations patch that same instance in place instead of exporting a new one\n// nobody sees. The patching itself is the runtime's `hotReplace` - the same\n// swap the jq79/dev server drives, from the one place that can reach a\n// component's markers. An instance only used as a definition has nothing to\n// re-render (nested clones can't be reached from this module), so it falls\n// back to a full reload.\nconst componentModule = (source: string, include: RegExp, filename: string): string => {\n const hoisted = hoistableImports(source, include)\n const imports = hoisted\n .map((spec, i) =>\n include.test(spec)\n ? `import __jq79_${i} from ${JSON.stringify(spec)}`\n : `import * as __jq79_${i} from ${JSON.stringify(spec)}`\n )\n .join(\"\\n\")\n const modulesMap = `{ ${hoisted.map((spec, i) => `${JSON.stringify(spec)}: __jq79_${i}`).join(\", \")} }`\n\n return `\nimport { Component79 } from \"jq79\"\n${imports}\n\nconst src = ${JSON.stringify(source)}\nconst modules = ${modulesMap}\nconst filename = ${JSON.stringify(filename)}\n\nlet component\n\nif (import.meta.hot && import.meta.hot.data.component) {\n const prior = import.meta.hot.data.component\n prior.modules = modules\n prior.filename = filename\n // re-renders it where it stands, keeping its data. false means it was never\n // rendered - a definition used only as a nested component - and a reload is\n // the only way to reach the clones made from it\n if (!prior.hotReplace(src) && !prior.data) import.meta.hot.invalidate()\n component = prior\n} else {\n component = new Component79(src, { modules, filename })\n}\n\nif (import.meta.hot) {\n import.meta.hot.data.component = component\n import.meta.hot.accept()\n}\n\nexport default component\n`\n}\n\nexport function jq79(options: Jq79PluginOptions = {}): Plugin {\n const include = options.include ?? /\\.html$/\n const { exclude } = options\n\n let config: ResolvedConfig | null = null\n\n return {\n name: \"jq79\",\n enforce: \"pre\",\n\n configResolved(resolved) {\n config = resolved\n },\n\n async resolveId(source, importer) {\n if (!importer) return null // entry points are never components\n if (source.includes(\"?\")) return null // ?raw, ?url, ... keep their meaning\n if (!include.test(source)) return null\n\n const resolved = await this.resolve(source, importer, { skipSelf: true })\n if (!resolved || resolved.external) return null\n if (exclude?.test(resolved.id)) return null\n return resolved.id + COMPONENT_QUERY\n },\n\n async load(id) {\n if (!id.endsWith(COMPONENT_QUERY)) return null\n const file = id.slice(0, -COMPONENT_QUERY.length)\n\n let source = await readFile(file, \"utf8\")\n if (config) source = await compileStyleBlocks(source, file, config, dep => this.addWatchFile(dep))\n\n // the runtime names the component's setup scripts after this, so devtools\n // shows a path the user recognizes instead of an anonymous VM script\n const filename = config ? relative(config.root, file) : file\n\n return { code: componentModule(source, include, filename), map: null }\n },\n }\n}\n\nexport default jq79\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../dev/vite.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\"\nimport { relative } from \"node:path\"\nimport { preprocessCSS } from \"vite\"\nimport type { Plugin, ResolvedConfig } from \"vite\"\n\n// Vite plugin: import .html single-file components as modules.\n//\n// import { jq79 } from \"jq79/vite\" // vite.config\n// import UserCard from \"./UserCard.html\" // app code\n//\n// The imported value is a Component79 built from the file's source - the same\n// thing `await Component79.fetch(url)` resolves to, but bundled at build time\n// instead of fetched at runtime. The component source is inlined verbatim, so\n// a file keeps working unchanged if it's ever served from public/ and loaded\n// with fetch instead - with one deliberate exception: <style lang=\"scss\"> (or\n// less/stylus/sass) is compiled to plain CSS here. A component using `lang`\n// therefore only works through the bundler; loaded with fetch() it would\n// reach the runtime uncompiled, which the runtime warns about.\n//\n// Only .html files imported from other modules are claimed; entry points\n// (index.html) have no importer and imports carrying an explicit query\n// (?raw, ?url) keep their built-in Vite meaning.\n\nexport interface Jq79PluginOptions {\n // which import specifiers are treated as components (default: any .html)\n include?: RegExp\n // resolved absolute paths to skip even when `include` matches\n exclude?: RegExp\n}\n\n// claimed modules get this suffix so their id no longer ends in \".html\" and\n// Vite's own html handling (entries, asset pipeline) leaves them alone\nconst COMPONENT_QUERY = \"?jq79\"\n\nconst SCRIPT_BLOCK_RE = /<script\\b[^>]*>([\\s\\S]*?)<\\/script\\s*>/gi\n// import(\"...\") with a literal specifier, tried at word boundaries the\n// scanner below reaches (which is what skips $__import and foo.import(...))\nconst IMPORT_CALL_RE = /import\\s*\\(\\s*([\"'])([^\"'\\n]+?)\\1\\s*\\)/y\n// static import statements (factory scripts): optional clause + literal\n// specifier. The clause can't contain parens/quotes, so dynamic import()\n// and import.meta never match\nconst STATIC_IMPORT_RE = /import\\s*(?:[\\w$\\s,{}*]+?\\s*from\\s*)?([\"'])([^\"'\\n]+)\\1/y\n\nconst skipString = (src: string, start: number): number => {\n const quote = src[start]\n let i = start + 1\n while (i < src.length) {\n if (src[i] === \"\\\\\") { i += 2; continue }\n if (src[i] === quote) return i + 1\n i++\n }\n return src.length\n}\n\n// the literal import specifiers in one script body. A scanner rather than a\n// bare matchAll, because a specifier mentioned in a comment or a string is\n// not an import: hoisting a commented-out `import(\"./old.html\")` would pull\n// dead files into the bundle - or break the build once the file is gone\nconst importSpecifiers = (script: string): string[] => {\n const specs: string[] = []\n let i = 0\n while (i < script.length) {\n const ch = script[i]\n if (ch === \"'\" || ch === '\"' || ch === \"`\") { i = skipString(script, i); continue }\n if (ch === \"/\" && script[i + 1] === \"/\") {\n const end = script.indexOf(\"\\n\", i)\n i = end === -1 ? script.length : end + 1\n continue\n }\n if (ch === \"/\" && script[i + 1] === \"*\") {\n const end = script.indexOf(\"*/\", i + 2)\n i = end === -1 ? script.length : end + 2\n continue\n }\n if (ch === \"i\" && (i === 0 || !/[\\w$.]/.test(script[i - 1]))) {\n IMPORT_CALL_RE.lastIndex = i\n const call = IMPORT_CALL_RE.exec(script)\n if (call) { specs.push(call[2]); i = IMPORT_CALL_RE.lastIndex; continue }\n STATIC_IMPORT_RE.lastIndex = i\n const staticImport = STATIC_IMPORT_RE.exec(script)\n if (staticImport) { specs.push(staticImport[2]); i = STATIC_IMPORT_RE.lastIndex; continue }\n }\n i++\n }\n return specs\n}\n\nconst isHtmlUrl = (spec: string) => /\\.html?([?#]|$)/.test(spec)\nconst isExternalUrl = (spec: string) => /^[a-z][a-z0-9+.-]*:/i.test(spec) || spec.startsWith(\"/\")\n\n// literal import specifiers in the component's script blocks - dynamic\n// `import(\"...\")` calls and static factory-script imports - that should\n// resolve from the bundle instead of at runtime. Absolute paths and full\n// URLs are left alone (they point at served files, e.g. public/), and so\n// are .html specifiers the plugin wouldn't claim as components\nconst hoistableImports = (source: string, include: RegExp): string[] => {\n const specifiers = new Set<string>()\n for (const [, script] of source.matchAll(SCRIPT_BLOCK_RE)) {\n for (const spec of importSpecifiers(script)) {\n if (isExternalUrl(spec)) continue\n if (isHtmlUrl(spec) && !include.test(spec)) continue // html left to runtime fetch\n specifiers.add(spec) // a claimed component, a source file or an npm package\n }\n }\n return [...specifiers]\n}\n\n// a <style> block with its attribute string, so `lang` can be read and the\n// content replaced. Attribute values are matched as quoted chunks so a \">\"\n// inside one doesn't end the tag early\nconst STYLE_BLOCK_RE = /<style((?:\"[^\"]*\"|'[^']*'|[^>\"'])*)>([\\s\\S]*?)<\\/style\\s*>/gi\nconst LANG_ATTR_RE = /\\blang\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/i\n\n// compiles <style lang=\"scss|less|styl|sass\"> blocks to plain CSS with Vite's\n// own preprocessing (the same call @vitejs/plugin-vue makes), so the runtime\n// only ever sees CSS. The preprocessor picks its parser from the extension,\n// and resolving relative @use/@import against a filename in the component's\n// own directory is what makes `@use \"./vars\"` work. Files the preprocessor\n// pulls in are registered as watch deps, so editing a partial re-runs HMR for\n// every component that uses it. `lang` is dropped from the emitted tag: what\n// the runtime parses is a plain <style> (with `scoped` and the rest intact)\nconst compileStyleBlocks = async (\n source: string,\n file: string,\n config: ResolvedConfig,\n addWatchFile: (id: string) => void\n): Promise<string> => {\n const blocks = [...source.matchAll(STYLE_BLOCK_RE)]\n const compiled = await Promise.all(\n blocks.map(async ([, attrs, content]) => {\n const lang = attrs.match(LANG_ATTR_RE)\n if (!lang) return null\n const extension = lang[1] ?? lang[2] ?? lang[3]\n const result = await preprocessCSS(content, `${file}.${extension}`, config)\n result.deps?.forEach(addWatchFile)\n return { attrs: attrs.replace(LANG_ATTR_RE, \"\").trimEnd(), css: result.code }\n })\n )\n\n let out = \"\"\n let last = 0\n blocks.forEach((block, i) => {\n const done = compiled[i]\n if (!done) return\n out += source.slice(last, block.index) + `<style${done.attrs}>${done.css}</style>`\n last = block.index + block[0].length\n })\n return out + source.slice(last)\n}\n\n// the emitted module. Literal import(\"...\") specifiers found in the\n// component's scripts become real module imports, handed to Component79 as a\n// resolution map: at runtime $__import checks the map before falling back to\n// fetch, so bundled components ship with their imports and nothing changes\n// for unbundled ones. Claimed components import as their default (a\n// Component79, matching what runtime fetch resolves to); everything else as\n// a namespace (matching native import()).\n//\n// In dev, `hot.data` carries the exported instance across updates: importers\n// hold a reference to the *first* module evaluation's instance, so later\n// evaluations patch that same instance in place instead of exporting a new one\n// nobody sees. The patching itself is the runtime's `hotReplace` - the same\n// swap the jq79/dev server drives, from the one place that can reach a\n// component's markers. An instance only used as a definition has nothing to\n// re-render (nested clones can't be reached from this module), so it falls\n// back to a full reload.\nconst componentModule = (source: string, include: RegExp, filename: string): string => {\n const hoisted = hoistableImports(source, include)\n const imports = hoisted\n .map((spec, i) =>\n include.test(spec)\n ? `import __jq79_${i} from ${JSON.stringify(spec)}`\n : `import * as __jq79_${i} from ${JSON.stringify(spec)}`\n )\n .join(\"\\n\")\n const modulesMap = `{ ${hoisted.map((spec, i) => `${JSON.stringify(spec)}: __jq79_${i}`).join(\", \")} }`\n\n return `\nimport { Component79 } from \"jq79\"\n${imports}\n\nconst src = ${JSON.stringify(source)}\nconst modules = ${modulesMap}\nconst filename = ${JSON.stringify(filename)}\n\nlet component\n\nif (import.meta.hot && import.meta.hot.data.component) {\n const prior = import.meta.hot.data.component\n prior.modules = modules\n prior.filename = filename\n // re-renders it where it stands, keeping its data. false means it was never\n // rendered - a definition used only as a nested component - and a reload is\n // the only way to reach the clones made from it\n if (!prior.hotReplace(src) && !prior.data) import.meta.hot.invalidate()\n component = prior\n} else {\n component = new Component79(src, { modules, filename })\n}\n\nif (import.meta.hot) {\n import.meta.hot.data.component = component\n import.meta.hot.accept()\n}\n\nexport default component\n`\n}\n\nexport function jq79(options: Jq79PluginOptions = {}): Plugin {\n const include = options.include ?? /\\.html$/\n const { exclude } = options\n\n let config: ResolvedConfig | null = null\n\n return {\n name: \"jq79\",\n enforce: \"pre\",\n\n configResolved(resolved) {\n config = resolved\n },\n\n async resolveId(source, importer) {\n if (!importer) return null // entry points are never components\n if (source.includes(\"?\")) return null // ?raw, ?url, ... keep their meaning\n if (!include.test(source)) return null\n\n const resolved = await this.resolve(source, importer, { skipSelf: true })\n if (!resolved || resolved.external) return null\n if (exclude?.test(resolved.id)) return null\n return resolved.id + COMPONENT_QUERY\n },\n\n async load(id) {\n if (!id.endsWith(COMPONENT_QUERY)) return null\n const file = id.slice(0, -COMPONENT_QUERY.length)\n\n let source = await readFile(file, \"utf8\")\n if (config) source = await compileStyleBlocks(source, file, config, dep => this.addWatchFile(dep))\n\n // the runtime names the component's setup scripts after this, so devtools\n // shows a path the user recognizes instead of an anonymous VM script\n const filename = config ? relative(config.root, file) : file\n\n return { code: componentModule(source, include, filename), map: null }\n },\n }\n}\n\nexport default jq79\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jq79",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Mini reactive component library: single-file components, Svelte-style setup scripts, fine-grained proxy reactivity. Single-file build, zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"reactive",
|
package/src/jq79.ts
CHANGED
|
@@ -92,8 +92,10 @@ const interpolate = (template: string, scope: Record<string, any>): string =>
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
const CONTROL_ATTRS = new Set([":attrs", ":if", ":elseif", ":else", ":each", ":key", ":with", ":text", ":html"])
|
|
95
|
-
//
|
|
96
|
-
|
|
95
|
+
// `item in items`, `item, i in items`, `(value, key) in props` - the second
|
|
96
|
+
// binding is the array index or the object key, parens optional (Vue-style).
|
|
97
|
+
// The list expression can span lines, so it matches [\s\S] rather than `.`
|
|
98
|
+
const EACH_PATTERN = /^\s*\(?\s*(\w+)\s*(?:,\s*(\w+))?\s*\)?\s+in\s+([\s\S]+)$/
|
|
97
99
|
|
|
98
100
|
type ConditionalBranch = { expr?: string; node: TemplateNode }
|
|
99
101
|
|
|
@@ -120,6 +122,40 @@ const bindEvent = (el: Element, attr: string, expr: string, scope: Record<string
|
|
|
120
122
|
|
|
121
123
|
const kebabToCamel = (name: string) => name.replace(/-(\w)/g, (_, c: string) => c.toUpperCase())
|
|
122
124
|
|
|
125
|
+
// the stable boundaries of a rendered chunk. An element is its own handle, but
|
|
126
|
+
// a fragment (a nested component: two anchors with the instance's DOM between
|
|
127
|
+
// them) empties itself into the parent on insertion - after that its identity
|
|
128
|
+
// answers nothing, and what stays put are its first and last children. Callers
|
|
129
|
+
// that reposition or remove a chunk later (:each entries, :if branches) must
|
|
130
|
+
// capture its bounds *before* inserting it and work on the range
|
|
131
|
+
type NodeRange = { first: Node; last: Node }
|
|
132
|
+
|
|
133
|
+
const boundsOf = (node: Node): NodeRange =>
|
|
134
|
+
node instanceof DocumentFragment
|
|
135
|
+
? { first: node.firstChild!, last: node.lastChild! }
|
|
136
|
+
: { first: node, last: node }
|
|
137
|
+
|
|
138
|
+
// removes [first..last] inclusive - the range's content is dynamic (a nested
|
|
139
|
+
// component's DOM comes and goes between its anchors), so it walks siblings
|
|
140
|
+
// rather than assuming any particular nodes in between
|
|
141
|
+
const removeRange = ({ first, last }: NodeRange) => {
|
|
142
|
+
for (let node: Node | null = first; node; ) {
|
|
143
|
+
const next: Node | null = node === last ? null : node.nextSibling
|
|
144
|
+
node.parentNode?.removeChild(node)
|
|
145
|
+
node = next
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// moves [first..last] inclusive so the range starts right after `prev`
|
|
150
|
+
const moveRangeAfter = ({ first, last }: NodeRange, prev: Node) => {
|
|
151
|
+
const ref = prev.nextSibling
|
|
152
|
+
for (let node: Node | null = first; node; ) {
|
|
153
|
+
const next: Node | null = node === last ? null : node.nextSibling
|
|
154
|
+
prev.parentNode!.insertBefore(node, ref)
|
|
155
|
+
node = next
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
123
159
|
// finds the scope variable a template tag refers to. HTML parsing lowercases
|
|
124
160
|
// tag names, so <NestedComponent> arrives as "nestedcomponent" and matching is
|
|
125
161
|
// case-insensitive with dashes stripped (<nested-component> works too). Only
|
|
@@ -149,9 +185,14 @@ const findComponentKey = (scope: Record<string, any>, tag: string): string | nul
|
|
|
149
185
|
// tree, and a style that never applies to its own component would still be
|
|
150
186
|
// restyling the page around it
|
|
151
187
|
const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<string, any>, fx: EffectScope, shadow: boolean): Node => {
|
|
188
|
+
// two anchors bracketing everything this usage site ever renders: the
|
|
189
|
+
// instance's DOM is dynamic (the definition can resolve late or be swapped),
|
|
190
|
+
// so a caller that needs to move or remove this chunk later can't hold any
|
|
191
|
+
// of it - it holds the anchors, which never move on their own (see boundsOf)
|
|
152
192
|
const anchor = document.createComment(key)
|
|
193
|
+
const endAnchor = document.createComment(`/${key}`)
|
|
153
194
|
const wrapper = document.createDocumentFragment()
|
|
154
|
-
wrapper.
|
|
195
|
+
wrapper.append(anchor, endAnchor)
|
|
155
196
|
|
|
156
197
|
const props: Record<string, string> = {} // prop name -> expression in parent scope
|
|
157
198
|
Object.entries(node.attrs).forEach(([attr, value]) => {
|
|
@@ -199,7 +240,7 @@ const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<st
|
|
|
199
240
|
// they style, and the parent's shadow root is what scopes both
|
|
200
241
|
const holder = document.createDocumentFragment()
|
|
201
242
|
;(shadow ? instance.renderShadow(seed) : instance.render(seed)).mount(holder)
|
|
202
|
-
|
|
243
|
+
endAnchor.parentNode!.insertBefore(holder, endAnchor)
|
|
203
244
|
|
|
204
245
|
const syncFx = createEffectScope(scope)
|
|
205
246
|
Object.entries(props).forEach(([name, expr]) => {
|
|
@@ -283,7 +324,12 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
|
|
|
283
324
|
const key = findComponentKey(scope, node.tag)
|
|
284
325
|
if (!key) return
|
|
285
326
|
upgraded = true
|
|
286
|
-
|
|
327
|
+
const replacement = renderNestedComponent(key, node, scope, fx, shadow)
|
|
328
|
+
// whoever tears this subtree down holds `el`, which the swap detaches -
|
|
329
|
+
// so the component's anchors must remove themselves when the scope goes
|
|
330
|
+
const range = boundsOf(replacement)
|
|
331
|
+
fx.onDispose(() => removeRange(range))
|
|
332
|
+
el.replaceWith(replacement)
|
|
287
333
|
})
|
|
288
334
|
}
|
|
289
335
|
|
|
@@ -333,7 +379,7 @@ const renderConditional = (branches: ConditionalBranch[], scope: Record<string,
|
|
|
333
379
|
const wrapper = document.createDocumentFragment()
|
|
334
380
|
wrapper.appendChild(anchor)
|
|
335
381
|
|
|
336
|
-
let current:
|
|
382
|
+
let current: NodeRange | null = null
|
|
337
383
|
let activeBranch: ConditionalBranch | null = null
|
|
338
384
|
let branchFx: EffectScope | null = null
|
|
339
385
|
|
|
@@ -342,14 +388,17 @@ const renderConditional = (branches: ConditionalBranch[], scope: Record<string,
|
|
|
342
388
|
if (next === activeBranch) return
|
|
343
389
|
|
|
344
390
|
branchFx?.dispose()
|
|
345
|
-
if (current)
|
|
391
|
+
if (current) removeRange(current)
|
|
346
392
|
current = null
|
|
347
393
|
activeBranch = next
|
|
348
394
|
if (!next) return
|
|
349
395
|
|
|
350
396
|
branchFx = createEffectScope(scope)
|
|
351
|
-
|
|
352
|
-
|
|
397
|
+
// bounds captured before inserting: a component branch is a fragment, and
|
|
398
|
+
// inserting it is what empties it (see boundsOf)
|
|
399
|
+
const rendered = renderNode(next.node, scope, branchFx, shadow)
|
|
400
|
+
current = boundsOf(rendered)
|
|
401
|
+
anchor.parentNode!.insertBefore(rendered, anchor.nextSibling)
|
|
353
402
|
})
|
|
354
403
|
|
|
355
404
|
return wrapper
|
|
@@ -367,21 +416,32 @@ const defineScopeVar = (scope: Record<string, any>, key: string, value: any) =>
|
|
|
367
416
|
Object.defineProperty(scope, key, { value, writable: true, enumerable: true, configurable: true })
|
|
368
417
|
}
|
|
369
418
|
|
|
370
|
-
type EachEntry = { key: any; item: any; scope: Record<string, any>;
|
|
419
|
+
type EachEntry = { key: any; item: any; scope: Record<string, any>; range: NodeRange; fx: EffectScope }
|
|
371
420
|
|
|
372
|
-
// :each
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
421
|
+
// what :each iterates besides arrays: dictionaries, as their entries. Class
|
|
422
|
+
// instances, Maps and the rest stay out - the store doesn't wrap them
|
|
423
|
+
// (isPlainData), so their contents wouldn't be tracked and the list would go
|
|
424
|
+
// silently stale
|
|
425
|
+
const isPlainObject = (value: any): value is Record<string, any> => {
|
|
426
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
427
|
+
const proto = Object.getPrototypeOf(value)
|
|
428
|
+
return proto === Object.prototype || proto === null
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// :each="item in items" (or "item, i in items" / "(value, key) in props"),
|
|
432
|
+
// optionally keyed with :key="expr". Only depends on what the list expression
|
|
433
|
+
// reads, and on each run diffs by key: unchanged items (same key, same item
|
|
434
|
+
// reference) keep their DOM/effects, changed/added ones are (re)rendered,
|
|
435
|
+
// removed ones are disposed. Without :key, an array uses position - fine for
|
|
436
|
+
// append-only lists, wasteful for reordering - and an object uses the
|
|
437
|
+
// property key, which is already the stable identity. Each item gets its own
|
|
438
|
+
// scope via Object.create(scope), so the bindings and `$index` shadow
|
|
439
|
+
// same-named outer names without copying the parent scope's keys
|
|
380
440
|
const renderEach = (node: TemplateNode, scope: Record<string, any>, fx: EffectScope, shadow: boolean): Node => {
|
|
381
441
|
const match = node.attrs[":each"].match(EACH_PATTERN)
|
|
382
442
|
if (!match) return document.createComment(`invalid :each expression "${node.attrs[":each"]}"`)
|
|
383
443
|
|
|
384
|
-
const [, itemName, listExpr] = match
|
|
444
|
+
const [, itemName, atName, listExpr] = match
|
|
385
445
|
const keyExpr = node.attrs[":key"]
|
|
386
446
|
const { [":each"]: _each, [":key"]: _key, ...itemAttrs } = node.attrs
|
|
387
447
|
const itemNode: TemplateNode = { ...node, attrs: itemAttrs }
|
|
@@ -390,46 +450,87 @@ const renderEach = (node: TemplateNode, scope: Record<string, any>, fx: EffectSc
|
|
|
390
450
|
const wrapper = document.createDocumentFragment()
|
|
391
451
|
wrapper.appendChild(anchor)
|
|
392
452
|
|
|
453
|
+
// :if on the same element is not per-item filtering, and rendering
|
|
454
|
+
// everything in silence reads like a broken filter - say it out loud
|
|
455
|
+
if (":if" in node.attrs || ":elseif" in node.attrs || ":else" in node.attrs) {
|
|
456
|
+
console.warn("jq79: :if/:elseif/:else on a :each element is ignored; filter the list expression instead")
|
|
457
|
+
}
|
|
458
|
+
|
|
393
459
|
let entries: EachEntry[] = []
|
|
460
|
+
let warnedDuplicates = false
|
|
394
461
|
|
|
395
462
|
fx.effect(() => {
|
|
396
463
|
const list = evalExpr(listExpr, scope)
|
|
397
|
-
|
|
398
|
-
|
|
464
|
+
// both sources normalize to [at, item] pairs: the index for an array, the
|
|
465
|
+
// property key for a plain object (insertion order). Object entries are
|
|
466
|
+
// read off the store proxy, so each value is tracked under its own key -
|
|
467
|
+
// adds, deletes and changes all wake this effect
|
|
468
|
+
const pairs: [any, any][] = Array.isArray(list)
|
|
469
|
+
? list.map((item, index): [any, any] => [index, item])
|
|
470
|
+
: isPlainObject(list) ? Object.entries(list) : []
|
|
471
|
+
// buckets rather than a key->entry map: duplicate keys (a user error, but
|
|
472
|
+
// one that must degrade instead of corrupt) consume entries in order of
|
|
473
|
+
// appearance, so no entry is ever matched twice - matching one twice is
|
|
474
|
+
// how a reused row got disposed and a removed one resurrected
|
|
475
|
+
const previous = new Map<any, EachEntry[]>()
|
|
476
|
+
entries.forEach(entry => {
|
|
477
|
+
const bucket = previous.get(entry.key)
|
|
478
|
+
if (bucket) bucket.push(entry)
|
|
479
|
+
else previous.set(entry.key, [entry])
|
|
480
|
+
})
|
|
399
481
|
|
|
400
|
-
const
|
|
482
|
+
const seen = new Set<any>()
|
|
483
|
+
const moved: EachEntry[] = []
|
|
484
|
+
const nextEntries = pairs.map(([at, item], index): EachEntry => {
|
|
401
485
|
const itemScope = Object.create(scope)
|
|
402
486
|
defineScopeVar(itemScope, itemName, item)
|
|
487
|
+
if (atName) defineScopeVar(itemScope, atName, at)
|
|
403
488
|
defineScopeVar(itemScope, "$index", index)
|
|
404
|
-
const key = keyExpr !== undefined ? evalExpr(keyExpr, itemScope) :
|
|
405
|
-
|
|
489
|
+
const key = keyExpr !== undefined ? evalExpr(keyExpr, itemScope) : at
|
|
490
|
+
if (seen.has(key) && !warnedDuplicates) {
|
|
491
|
+
warnedDuplicates = true
|
|
492
|
+
console.warn(`jq79: duplicate :key in :each "${node.attrs[":each"]}"; duplicates pair up by position`)
|
|
493
|
+
}
|
|
494
|
+
seen.add(key)
|
|
495
|
+
const existing = previous.get(key)?.shift()
|
|
406
496
|
|
|
407
497
|
if (existing && Object.is(existing.item, item)) {
|
|
498
|
+
if (existing.scope.$index !== index) moved.push(existing)
|
|
408
499
|
defineScopeVar(existing.scope, "$index", index)
|
|
500
|
+
if (atName) defineScopeVar(existing.scope, atName, at)
|
|
409
501
|
return existing
|
|
410
502
|
}
|
|
411
503
|
|
|
412
|
-
existing
|
|
413
|
-
|
|
504
|
+
if (existing) {
|
|
505
|
+
existing.fx.dispose()
|
|
506
|
+
removeRange(existing.range)
|
|
507
|
+
}
|
|
414
508
|
|
|
415
509
|
const itemFx = createEffectScope(scope)
|
|
416
|
-
|
|
510
|
+
// bounds captured before the positioning pass inserts the entry: a
|
|
511
|
+
// component entry is a fragment, which empties on insertion (see boundsOf)
|
|
512
|
+
const range = boundsOf(renderNode(itemNode, itemScope, itemFx, shadow))
|
|
513
|
+
return { key, item, scope: itemScope, fx: itemFx, range }
|
|
417
514
|
})
|
|
418
515
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
})
|
|
516
|
+
// whatever no new item consumed is gone
|
|
517
|
+
previous.forEach(bucket => bucket.forEach(entry => {
|
|
518
|
+
entry.fx.dispose()
|
|
519
|
+
removeRange(entry.range)
|
|
520
|
+
}))
|
|
426
521
|
|
|
427
522
|
let prevNode: Node = anchor
|
|
428
523
|
nextEntries.forEach(entry => {
|
|
429
|
-
if (prevNode.nextSibling !== entry.
|
|
430
|
-
prevNode = entry.
|
|
524
|
+
if (prevNode.nextSibling !== entry.range.first) moveRangeAfter(entry.range, prevNode)
|
|
525
|
+
prevNode = entry.range.last
|
|
431
526
|
})
|
|
432
527
|
|
|
528
|
+
// reused entries that changed position: their tracked bindings re-run off
|
|
529
|
+
// the list notification anyway, but a binding that reads only `$index` or
|
|
530
|
+
// the named key tracked nothing - refresh them so the move reaches those
|
|
531
|
+
// too. Untracked, so these runs don't feed this list effect's own deps
|
|
532
|
+
moved.forEach(entry => untracked(() => entry.fx.refresh()))
|
|
533
|
+
|
|
433
534
|
entries = nextEntries
|
|
434
535
|
})
|
|
435
536
|
|