jq79 0.3.21 → 0.3.23
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 +4 -1
- package/assets/code-coverage.svg +19 -0
- package/assets/github-dark.svg +5 -0
- package/assets/github-light.svg +5 -0
- package/assets/github-white.afdesign +0 -0
- package/assets/npm-logo.svg +8 -0
- package/dist/jq79.cjs +8 -8
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +4 -0
- package/dist/jq79.global.js +8 -8
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +8 -8
- package/dist/jq79.js.map +1 -1
- package/dist/vite.cjs +38 -3
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.js +38 -3
- package/dist/vite.js.map +1 -1
- package/package.json +4 -1
- package/src/jq79.ts +169 -19
- package/src/reactive.ts +75 -29
- package/src/transform.ts +34 -4
- package/src/vite.ts +71 -7
package/dist/vite.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/vite.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\"\nimport type { Plugin } 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 plugin is a pure loader: the component\n// source is inlined verbatim (no transforms), so a file keeps working\n// unchanged if it's ever served from public/ and loaded with fetch instead.\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// 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 (the parsed parts are public\n// fields) instead of exporting a new one nobody sees. A live instance is\n// re-rendered where it stands, seeded with a snapshot of its current store;\n// an instance only used as a definition (nested component clones can't be\n// reached from here) falls back to a full reload. `mountRoot` is internal to\n// Component79, but plugin and runtime ship in lockstep from the same package.\nconst componentModule = (source: string, include: RegExp): 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}\n\nlet component\n\nif (import.meta.hot && import.meta.hot.data.component) {\n const prior = import.meta.hot.data.component\n const next = new Component79(src)\n prior.template = next.template\n prior.scripts = next.scripts\n prior.styles = next.styles\n prior.modules = modules\n const root = prior.mountRoot\n if (root) {\n prior.mount(root, { ...prior.data })\n } else if (!prior.data) {\n import.meta.hot.invalidate()\n }\n component = prior\n} else {\n component = new Component79(src, { modules })\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 return {\n name: \"jq79\",\n enforce: \"pre\",\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 return { code: componentModule(await readFile(file, \"utf8\"), include), map: null }\n },\n }\n}\n\nexport default jq79\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAAyB;AA2BzB,IAAM,kBAAkB;AAExB,IAAM,kBAAkB;AAGxB,IAAM,oBAAoB;AAI1B,IAAM,2BAA2B;AAEjC,IAAM,YAAY,CAAC,SAAiB,kBAAkB,KAAK,IAAI;AAC/D,IAAM,gBAAgB,CAAC,SAAiB,uBAAuB,KAAK,IAAI,KAAK,KAAK,WAAW,GAAG;AAOhG,IAAM,mBAAmB,CAAC,QAAgB,YAA8B;AACtE,QAAM,aAAa,oBAAI,IAAY;AACnC,aAAW,CAAC,EAAE,MAAM,KAAK,OAAO,SAAS,eAAe,GAAG;AACzD,UAAM,QAAQ;AAAA,MACZ,GAAG,CAAC,GAAG,OAAO,SAAS,iBAAiB,CAAC,EAAE,IAAI,WAAS,MAAM,CAAC,CAAC;AAAA,MAChE,GAAG,CAAC,GAAG,OAAO,SAAS,wBAAwB,CAAC,EAAE,IAAI,WAAS,MAAM,CAAC,CAAC;AAAA,IACzE;AACA,eAAW,QAAQ,OAAO;AACxB,UAAI,cAAc,IAAI,EAAG;AACzB,UAAI,UAAU,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,EAAG;AAC5C,iBAAW,IAAI,IAAI;AAAA,IACrB;AAAA,EACF;AACA,SAAO,CAAC,GAAG,UAAU;AACvB;AAkBA,IAAM,kBAAkB,CAAC,QAAgB,YAA4B;AACnE,QAAM,UAAU,iBAAiB,QAAQ,OAAO;AAChD,QAAM,UAAU,QACb;AAAA,IAAI,CAAC,MAAM,MACV,QAAQ,KAAK,IAAI,IACb,iBAAiB,CAAC,SAAS,KAAK,UAAU,IAAI,CAAC,KAC/C,sBAAsB,CAAC,SAAS,KAAK,UAAU,IAAI,CAAC;AAAA,EAC1D,EACC,KAAK,IAAI;AACZ,QAAM,aAAa,KAAK,QAAQ,IAAI,CAAC,MAAM,MAAM,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,IAAI,CAAC;AAEnG,SAAO;AAAA;AAAA,EAEP,OAAO;AAAA;AAAA,cAEK,KAAK,UAAU,MAAM,CAAC;AAAA,kBAClB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6B5B;AAEO,SAAS,KAAK,UAA6B,CAAC,GAAW;AAC5D,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,EAAE,QAAQ,IAAI;AAEpB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IAET,MAAM,UAAU,QAAQ,UAAU;AAChC,UAAI,CAAC,SAAU,QAAO;AACtB,UAAI,OAAO,SAAS,GAAG,EAAG,QAAO;AACjC,UAAI,CAAC,QAAQ,KAAK,MAAM,EAAG,QAAO;AAElC,YAAM,WAAW,MAAM,KAAK,QAAQ,QAAQ,UAAU,EAAE,UAAU,KAAK,CAAC;AACxE,UAAI,CAAC,YAAY,SAAS,SAAU,QAAO;AAC3C,UAAI,SAAS,KAAK,SAAS,EAAE,EAAG,QAAO;AACvC,aAAO,SAAS,KAAK;AAAA,IACvB;AAAA,IAEA,MAAM,KAAK,IAAI;AACb,UAAI,CAAC,GAAG,SAAS,eAAe,EAAG,QAAO;AAC1C,YAAM,OAAO,GAAG,MAAM,GAAG,CAAC,gBAAgB,MAAM;AAChD,aAAO,EAAE,MAAM,gBAAgB,UAAM,0BAAS,MAAM,MAAM,GAAG,OAAO,GAAG,KAAK,KAAK;AAAA,IACnF;AAAA,EACF;AACF;AAEA,IAAO,eAAQ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/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 (the parsed parts are public\n// fields) instead of exporting a new one nobody sees. A live instance is\n// re-rendered where it stands, seeded with a snapshot of its current store;\n// an instance only used as a definition (nested component clones can't be\n// reached from here) falls back to a full reload. `mountRoot` is internal to\n// Component79, but plugin and runtime ship in lockstep from the same package.\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 const next = new Component79(src)\n prior.template = next.template\n prior.scripts = next.scripts\n prior.styles = next.styles\n prior.modules = modules\n prior.filename = filename\n const root = prior.mountRoot\n if (root) {\n prior.mount(root, { ...prior.data })\n } else if (!prior.data) {\n import.meta.hot.invalidate()\n }\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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/vite.ts
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
|
+
import { relative } from "path";
|
|
4
|
+
import { preprocessCSS } from "vite";
|
|
3
5
|
var COMPONENT_QUERY = "?jq79";
|
|
4
6
|
var SCRIPT_BLOCK_RE = /<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi;
|
|
5
7
|
var IMPORT_LITERAL_RE = /(?<![\w$.])import\s*\(\s*(["'])([^"'\n]+?)\1\s*\)/g;
|
|
@@ -21,7 +23,31 @@ var hoistableImports = (source, include) => {
|
|
|
21
23
|
}
|
|
22
24
|
return [...specifiers];
|
|
23
25
|
};
|
|
24
|
-
var
|
|
26
|
+
var STYLE_BLOCK_RE = /<style((?:"[^"]*"|'[^']*'|[^>"'])*)>([\s\S]*?)<\/style\s*>/gi;
|
|
27
|
+
var LANG_ATTR_RE = /\blang\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
|
|
28
|
+
var compileStyleBlocks = async (source, file, config, addWatchFile) => {
|
|
29
|
+
const blocks = [...source.matchAll(STYLE_BLOCK_RE)];
|
|
30
|
+
const compiled = await Promise.all(
|
|
31
|
+
blocks.map(async ([, attrs, content]) => {
|
|
32
|
+
const lang = attrs.match(LANG_ATTR_RE);
|
|
33
|
+
if (!lang) return null;
|
|
34
|
+
const extension = lang[1] ?? lang[2] ?? lang[3];
|
|
35
|
+
const result = await preprocessCSS(content, `${file}.${extension}`, config);
|
|
36
|
+
result.deps?.forEach(addWatchFile);
|
|
37
|
+
return { attrs: attrs.replace(LANG_ATTR_RE, "").trimEnd(), css: result.code };
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
let out = "";
|
|
41
|
+
let last = 0;
|
|
42
|
+
blocks.forEach((block, i) => {
|
|
43
|
+
const done = compiled[i];
|
|
44
|
+
if (!done) return;
|
|
45
|
+
out += source.slice(last, block.index) + `<style${done.attrs}>${done.css}</style>`;
|
|
46
|
+
last = block.index + block[0].length;
|
|
47
|
+
});
|
|
48
|
+
return out + source.slice(last);
|
|
49
|
+
};
|
|
50
|
+
var componentModule = (source, include, filename) => {
|
|
25
51
|
const hoisted = hoistableImports(source, include);
|
|
26
52
|
const imports = hoisted.map(
|
|
27
53
|
(spec, i) => include.test(spec) ? `import __jq79_${i} from ${JSON.stringify(spec)}` : `import * as __jq79_${i} from ${JSON.stringify(spec)}`
|
|
@@ -33,6 +59,7 @@ ${imports}
|
|
|
33
59
|
|
|
34
60
|
const src = ${JSON.stringify(source)}
|
|
35
61
|
const modules = ${modulesMap}
|
|
62
|
+
const filename = ${JSON.stringify(filename)}
|
|
36
63
|
|
|
37
64
|
let component
|
|
38
65
|
|
|
@@ -43,6 +70,7 @@ if (import.meta.hot && import.meta.hot.data.component) {
|
|
|
43
70
|
prior.scripts = next.scripts
|
|
44
71
|
prior.styles = next.styles
|
|
45
72
|
prior.modules = modules
|
|
73
|
+
prior.filename = filename
|
|
46
74
|
const root = prior.mountRoot
|
|
47
75
|
if (root) {
|
|
48
76
|
prior.mount(root, { ...prior.data })
|
|
@@ -51,7 +79,7 @@ if (import.meta.hot && import.meta.hot.data.component) {
|
|
|
51
79
|
}
|
|
52
80
|
component = prior
|
|
53
81
|
} else {
|
|
54
|
-
component = new Component79(src, { modules })
|
|
82
|
+
component = new Component79(src, { modules, filename })
|
|
55
83
|
}
|
|
56
84
|
|
|
57
85
|
if (import.meta.hot) {
|
|
@@ -65,9 +93,13 @@ export default component
|
|
|
65
93
|
function jq79(options = {}) {
|
|
66
94
|
const include = options.include ?? /\.html$/;
|
|
67
95
|
const { exclude } = options;
|
|
96
|
+
let config = null;
|
|
68
97
|
return {
|
|
69
98
|
name: "jq79",
|
|
70
99
|
enforce: "pre",
|
|
100
|
+
configResolved(resolved) {
|
|
101
|
+
config = resolved;
|
|
102
|
+
},
|
|
71
103
|
async resolveId(source, importer) {
|
|
72
104
|
if (!importer) return null;
|
|
73
105
|
if (source.includes("?")) return null;
|
|
@@ -80,7 +112,10 @@ function jq79(options = {}) {
|
|
|
80
112
|
async load(id) {
|
|
81
113
|
if (!id.endsWith(COMPONENT_QUERY)) return null;
|
|
82
114
|
const file = id.slice(0, -COMPONENT_QUERY.length);
|
|
83
|
-
|
|
115
|
+
let source = await readFile(file, "utf8");
|
|
116
|
+
if (config) source = await compileStyleBlocks(source, file, config, (dep) => this.addWatchFile(dep));
|
|
117
|
+
const filename = config ? relative(config.root, file) : file;
|
|
118
|
+
return { code: componentModule(source, include, filename), map: null };
|
|
84
119
|
}
|
|
85
120
|
};
|
|
86
121
|
}
|
package/dist/vite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/vite.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\"\nimport type { Plugin } 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 plugin is a pure loader: the component\n// source is inlined verbatim (no transforms), so a file keeps working\n// unchanged if it's ever served from public/ and loaded with fetch instead.\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// 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 (the parsed parts are public\n// fields) instead of exporting a new one nobody sees. A live instance is\n// re-rendered where it stands, seeded with a snapshot of its current store;\n// an instance only used as a definition (nested component clones can't be\n// reached from here) falls back to a full reload. `mountRoot` is internal to\n// Component79, but plugin and runtime ship in lockstep from the same package.\nconst componentModule = (source: string, include: RegExp): 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}\n\nlet component\n\nif (import.meta.hot && import.meta.hot.data.component) {\n const prior = import.meta.hot.data.component\n const next = new Component79(src)\n prior.template = next.template\n prior.scripts = next.scripts\n prior.styles = next.styles\n prior.modules = modules\n const root = prior.mountRoot\n if (root) {\n prior.mount(root, { ...prior.data })\n } else if (!prior.data) {\n import.meta.hot.invalidate()\n }\n component = prior\n} else {\n component = new Component79(src, { modules })\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 return {\n name: \"jq79\",\n enforce: \"pre\",\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 return { code: componentModule(await readFile(file, \"utf8\"), include), map: null }\n },\n }\n}\n\nexport default jq79\n"],"mappings":";AAAA,SAAS,gBAAgB;AA2BzB,IAAM,kBAAkB;AAExB,IAAM,kBAAkB;AAGxB,IAAM,oBAAoB;AAI1B,IAAM,2BAA2B;AAEjC,IAAM,YAAY,CAAC,SAAiB,kBAAkB,KAAK,IAAI;AAC/D,IAAM,gBAAgB,CAAC,SAAiB,uBAAuB,KAAK,IAAI,KAAK,KAAK,WAAW,GAAG;AAOhG,IAAM,mBAAmB,CAAC,QAAgB,YAA8B;AACtE,QAAM,aAAa,oBAAI,IAAY;AACnC,aAAW,CAAC,EAAE,MAAM,KAAK,OAAO,SAAS,eAAe,GAAG;AACzD,UAAM,QAAQ;AAAA,MACZ,GAAG,CAAC,GAAG,OAAO,SAAS,iBAAiB,CAAC,EAAE,IAAI,WAAS,MAAM,CAAC,CAAC;AAAA,MAChE,GAAG,CAAC,GAAG,OAAO,SAAS,wBAAwB,CAAC,EAAE,IAAI,WAAS,MAAM,CAAC,CAAC;AAAA,IACzE;AACA,eAAW,QAAQ,OAAO;AACxB,UAAI,cAAc,IAAI,EAAG;AACzB,UAAI,UAAU,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,EAAG;AAC5C,iBAAW,IAAI,IAAI;AAAA,IACrB;AAAA,EACF;AACA,SAAO,CAAC,GAAG,UAAU;AACvB;AAkBA,IAAM,kBAAkB,CAAC,QAAgB,YAA4B;AACnE,QAAM,UAAU,iBAAiB,QAAQ,OAAO;AAChD,QAAM,UAAU,QACb;AAAA,IAAI,CAAC,MAAM,MACV,QAAQ,KAAK,IAAI,IACb,iBAAiB,CAAC,SAAS,KAAK,UAAU,IAAI,CAAC,KAC/C,sBAAsB,CAAC,SAAS,KAAK,UAAU,IAAI,CAAC;AAAA,EAC1D,EACC,KAAK,IAAI;AACZ,QAAM,aAAa,KAAK,QAAQ,IAAI,CAAC,MAAM,MAAM,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,IAAI,CAAC;AAEnG,SAAO;AAAA;AAAA,EAEP,OAAO;AAAA;AAAA,cAEK,KAAK,UAAU,MAAM,CAAC;AAAA,kBAClB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6B5B;AAEO,SAAS,KAAK,UAA6B,CAAC,GAAW;AAC5D,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,EAAE,QAAQ,IAAI;AAEpB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IAET,MAAM,UAAU,QAAQ,UAAU;AAChC,UAAI,CAAC,SAAU,QAAO;AACtB,UAAI,OAAO,SAAS,GAAG,EAAG,QAAO;AACjC,UAAI,CAAC,QAAQ,KAAK,MAAM,EAAG,QAAO;AAElC,YAAM,WAAW,MAAM,KAAK,QAAQ,QAAQ,UAAU,EAAE,UAAU,KAAK,CAAC;AACxE,UAAI,CAAC,YAAY,SAAS,SAAU,QAAO;AAC3C,UAAI,SAAS,KAAK,SAAS,EAAE,EAAG,QAAO;AACvC,aAAO,SAAS,KAAK;AAAA,IACvB;AAAA,IAEA,MAAM,KAAK,IAAI;AACb,UAAI,CAAC,GAAG,SAAS,eAAe,EAAG,QAAO;AAC1C,YAAM,OAAO,GAAG,MAAM,GAAG,CAAC,gBAAgB,MAAM;AAChD,aAAO,EAAE,MAAM,gBAAgB,MAAM,SAAS,MAAM,MAAM,GAAG,OAAO,GAAG,KAAK,KAAK;AAAA,IACnF;AAAA,EACF;AACF;AAEA,IAAO,eAAQ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/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 (the parsed parts are public\n// fields) instead of exporting a new one nobody sees. A live instance is\n// re-rendered where it stands, seeded with a snapshot of its current store;\n// an instance only used as a definition (nested component clones can't be\n// reached from here) falls back to a full reload. `mountRoot` is internal to\n// Component79, but plugin and runtime ship in lockstep from the same package.\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 const next = new Component79(src)\n prior.template = next.template\n prior.scripts = next.scripts\n prior.styles = next.styles\n prior.modules = modules\n prior.filename = filename\n const root = prior.mountRoot\n if (root) {\n prior.mount(root, { ...prior.data })\n } else if (!prior.data) {\n import.meta.hot.invalidate()\n }\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.
|
|
3
|
+
"version": "0.3.23",
|
|
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",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"test": "vitest run",
|
|
52
52
|
"test:coverage": "vitest run --coverage",
|
|
53
53
|
"site": "node scripts/build-site.mjs",
|
|
54
|
+
"site.dev": "node scripts/site-dev.mjs",
|
|
54
55
|
"prepublishOnly": "npm test && npm run build"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
@@ -64,10 +65,12 @@
|
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@types/node": "^26.1.1",
|
|
66
67
|
"@vitest/coverage-v8": "^4.1.10",
|
|
68
|
+
"chokidar": "^5.0.0",
|
|
67
69
|
"highlight.js": "^11.11.1",
|
|
68
70
|
"jsdom": "^29.1.1",
|
|
69
71
|
"marked": "^18.0.6",
|
|
70
72
|
"marked-highlight": "^2.2.4",
|
|
73
|
+
"sass": "^1.101.0",
|
|
71
74
|
"tsup": "^8.5.1",
|
|
72
75
|
"typescript": "^7.0.2",
|
|
73
76
|
"vite": "^8.1.4",
|
package/src/jq79.ts
CHANGED
|
@@ -15,6 +15,10 @@ type TemplateNode = {
|
|
|
15
15
|
type TagBlock = {
|
|
16
16
|
attrs: Record<string, string>
|
|
17
17
|
content: string
|
|
18
|
+
// <style scoped> only: `content` rewritten to require the component's scope
|
|
19
|
+
// attribute. Kept beside the original rather than replacing it, because a
|
|
20
|
+
// shadow root doesn't want it - see headStyle()
|
|
21
|
+
scoped?: string
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
const elementAttrs = (el: Element): Record<string, string> =>
|
|
@@ -52,12 +56,15 @@ const evalExpr = (expr: string, scope: Record<string, any>, extras?: Record<stri
|
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
// [\s\S] rather than `.` so an expression can span lines, like the ones in
|
|
60
|
+
// directive attributes (which reach evalExpr wrapped in parens either way)
|
|
55
61
|
const interpolate = (template: string, scope: Record<string, any>): string =>
|
|
56
|
-
template.replace(/{{\s*(
|
|
62
|
+
template.replace(/{{\s*([\s\S]+?)\s*}}/g, (_, expr) => evalExpr(expr, scope) ?? "")
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
const CONTROL_ATTRS = new Set([":attrs", ":if", ":elseif", ":else", ":each", ":key", ":with", ":text", ":html"])
|
|
60
|
-
|
|
66
|
+
// the list expression can span lines, so it matches [\s\S] rather than `.`
|
|
67
|
+
const EACH_PATTERN = /^\s*(\w+)\s+in\s+([\s\S]+)$/
|
|
61
68
|
|
|
62
69
|
type ConditionalBranch = { expr?: string; node: TemplateNode }
|
|
63
70
|
|
|
@@ -114,7 +121,9 @@ const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<st
|
|
|
114
121
|
|
|
115
122
|
const props: Record<string, string> = {} // prop name -> expression in parent scope
|
|
116
123
|
Object.entries(node.attrs).forEach(([attr, value]) => {
|
|
117
|
-
|
|
124
|
+
// the parent's scope stamp is stamped on every template element, this tag
|
|
125
|
+
// included - it's not a prop, and the child renders under its own scope
|
|
126
|
+
if (attr === SCOPE_ATTR || CONTROL_ATTRS.has(attr) || attr.startsWith("@")) return
|
|
118
127
|
if (attr.startsWith(":")) {
|
|
119
128
|
const name = kebabToCamel(attr.slice(1))
|
|
120
129
|
props[name] = value || name
|
|
@@ -141,7 +150,13 @@ const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<st
|
|
|
141
150
|
|
|
142
151
|
// a fresh instance per usage site: the definition's parsed parts (and
|
|
143
152
|
// pre-resolved modules) are shared, but store/effects/DOM are per instance
|
|
144
|
-
const instance = new Component79({
|
|
153
|
+
const instance = new Component79({
|
|
154
|
+
template: nextDef.template,
|
|
155
|
+
scripts: nextDef.scripts,
|
|
156
|
+
styles: nextDef.styles,
|
|
157
|
+
modules: nextDef.modules,
|
|
158
|
+
filename: nextDef.filename,
|
|
159
|
+
})
|
|
145
160
|
const seed = untracked(() =>
|
|
146
161
|
Object.fromEntries(Object.entries(props).map(([name, expr]) => [name, evalExpr(expr, scope)]))
|
|
147
162
|
)
|
|
@@ -442,6 +457,9 @@ type ComponentParts = {
|
|
|
442
457
|
// the literal specifier. Bundlers (the jq79/vite plugin) fill this so
|
|
443
458
|
// imports resolve from the bundle instead of being fetched at runtime
|
|
444
459
|
modules?: Record<string, any>
|
|
460
|
+
// where this component came from (a URL for fetch(), a path for the vite
|
|
461
|
+
// plugin). Names the setup scripts in devtools - see scriptSourceUrl
|
|
462
|
+
filename?: string
|
|
445
463
|
}
|
|
446
464
|
|
|
447
465
|
const VOID_ELEMENTS = new Set([
|
|
@@ -471,6 +489,68 @@ const expandSelfClosingTags = (src: string): string =>
|
|
|
471
489
|
)
|
|
472
490
|
.join("")
|
|
473
491
|
|
|
492
|
+
// <style scoped> support. Every element of the component's own template is
|
|
493
|
+
// stamped with data-jq79="<hash>" and the style's selectors are rewritten to
|
|
494
|
+
// require that attribute, so its rules can't reach anything the component
|
|
495
|
+
// didn't render. Purely a runtime transform (the browser parses the CSS), so
|
|
496
|
+
// it works the same for a bundled component and one loaded with fetch()
|
|
497
|
+
const SCOPE_ATTR = "data-jq79"
|
|
498
|
+
|
|
499
|
+
// FNV-1a over the source: stable per definition (not per instance), so N
|
|
500
|
+
// instances of the same component share one refcounted <style> in the head
|
|
501
|
+
const scopeHash = (src: string): string => {
|
|
502
|
+
let hash = 2166136261
|
|
503
|
+
for (let i = 0; i < src.length; i++) hash = Math.imul(hash ^ src.charCodeAt(i), 16777619)
|
|
504
|
+
return (hash >>> 0).toString(36)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const stampScope = (nodes: (TemplateNode | string)[], scope: string) => {
|
|
508
|
+
nodes.forEach(node => {
|
|
509
|
+
if (typeof node === "string") return
|
|
510
|
+
node.attrs[SCOPE_ATTR] = scope
|
|
511
|
+
stampScope(node.children, scope)
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// the scope attribute goes on the selector's last compound - the element the
|
|
516
|
+
// rule actually targets - but *before* a pseudo-element, which must stay last
|
|
517
|
+
// (".a::before" scopes to ".a[data-jq79='x']::before", not "::before[...]")
|
|
518
|
+
const scopeSelector = (selectorText: string, scope: string): string =>
|
|
519
|
+
selectorText
|
|
520
|
+
.split(",")
|
|
521
|
+
.map(part => {
|
|
522
|
+
const selector = part.trim()
|
|
523
|
+
const pseudoAt = selector.indexOf("::")
|
|
524
|
+
const target = pseudoAt === -1 ? selector : selector.slice(0, pseudoAt)
|
|
525
|
+
const pseudoElement = pseudoAt === -1 ? "" : selector.slice(pseudoAt)
|
|
526
|
+
return `${target}[${SCOPE_ATTR}="${scope}"]${pseudoElement}`
|
|
527
|
+
})
|
|
528
|
+
.join(", ")
|
|
529
|
+
|
|
530
|
+
// CSSStyleRule is scoped in place; CSSGroupingRule (@media, @supports,
|
|
531
|
+
// @container) is recursed into; everything else - notably @keyframes, whose
|
|
532
|
+
// "selectors" are percentages - is left alone
|
|
533
|
+
const scopeRules = (rules: CSSRuleList, scope: string) => {
|
|
534
|
+
Array.from(rules).forEach(rule => {
|
|
535
|
+
if (rule instanceof CSSStyleRule) rule.selectorText = scopeSelector(rule.selectorText, scope)
|
|
536
|
+
else if (rule instanceof CSSGroupingRule) scopeRules(rule.cssRules, scope)
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// the CSS parser is the browser's own (no dependency, no hand-rolled parser).
|
|
541
|
+
// Note browsers *silently drop* rules whose selector they can't parse, which
|
|
542
|
+
// is what Vue's :deep()/::v-deep/>>> escape hatches are - unsupported here,
|
|
543
|
+
// and warned about rather than left to vanish
|
|
544
|
+
const scopeCss = (css: string, scope: string): string => {
|
|
545
|
+
if (/:deep\(|::v-deep|>>>/.test(css)) {
|
|
546
|
+
console.warn("jq79: :deep()/::v-deep/>>> are not supported in <style scoped>; the rule will be dropped by the browser")
|
|
547
|
+
}
|
|
548
|
+
const sheet = new CSSStyleSheet()
|
|
549
|
+
sheet.replaceSync(css)
|
|
550
|
+
scopeRules(sheet.cssRules, scope)
|
|
551
|
+
return Array.from(sheet.cssRules).map(rule => rule.cssText).join("\n")
|
|
552
|
+
}
|
|
553
|
+
|
|
474
554
|
// converts a string of HTML into an AST representation of the component:
|
|
475
555
|
// - template: the non-script/style top-level elements, as TemplateNodes
|
|
476
556
|
// - scripts/styles: { attrs, content } blocks in source order
|
|
@@ -501,13 +581,39 @@ const parseComponentString = (component: string): ComponentParts => {
|
|
|
501
581
|
const template: TemplateNode[] = []
|
|
502
582
|
|
|
503
583
|
Array.from(root.content.children).forEach(el => {
|
|
504
|
-
const block = { attrs: elementAttrs(el), content: el.textContent ?? "" }
|
|
584
|
+
const block: TagBlock = { attrs: elementAttrs(el), content: el.textContent ?? "" }
|
|
505
585
|
|
|
506
586
|
if (el.tagName === "SCRIPT") scripts.push(block)
|
|
507
587
|
else if (el.tagName === "STYLE") styles.push(block)
|
|
508
588
|
else template.push(elementToAST(el))
|
|
509
589
|
})
|
|
510
590
|
|
|
591
|
+
// <style lang="scss"> is compiled by the jq79/vite plugin, so a `lang` still
|
|
592
|
+
// here means this component never went through it - it was fetched, loaded
|
|
593
|
+
// from a URL, or built from an inline string. The browser would drop the
|
|
594
|
+
// uncompiled source without a word, so say it out loud instead
|
|
595
|
+
styles.forEach(style => {
|
|
596
|
+
if ("lang" in style.attrs) {
|
|
597
|
+
console.warn(
|
|
598
|
+
`jq79: <style lang="${style.attrs.lang}"> needs the jq79/vite plugin to compile it. ` +
|
|
599
|
+
"This component didn't go through the bundler, so its styles were left uncompiled and the browser will ignore them."
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
// scoping is resolved once, here: the stamped template and the scoped CSS
|
|
605
|
+
// are what every instance of this definition renders and injects. An
|
|
606
|
+
// uncompiled `lang` block is left as it was written - rewriting selectors
|
|
607
|
+
// in something that isn't CSS yet would only garble what devtools shows
|
|
608
|
+
const isScoped = (style: TagBlock) => "scoped" in style.attrs && !("lang" in style.attrs)
|
|
609
|
+
if (styles.some(isScoped)) {
|
|
610
|
+
const scope = scopeHash(component)
|
|
611
|
+
stampScope(template, scope)
|
|
612
|
+
styles.forEach(style => {
|
|
613
|
+
if (isScoped(style)) style.scoped = scopeCss(style.content, scope)
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
|
|
511
617
|
return { template, scripts, styles }
|
|
512
618
|
}
|
|
513
619
|
|
|
@@ -515,6 +621,40 @@ const parseComponentString = (component: string): ComponentParts => {
|
|
|
515
621
|
const importResource = (url: string): Promise<any> =>
|
|
516
622
|
/\.html?([?#]|$)/.test(url) ? Component79.fetch(url) : import(url)
|
|
517
623
|
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
// naming scripts for devtools
|
|
626
|
+
//
|
|
627
|
+
// setup scripts are compiled with new Function (they need `with`, which is a
|
|
628
|
+
// SyntaxError in a module), so no bundler source map can reach them: they show
|
|
629
|
+
// up as an anonymous "VM1234" script, breakpoints don't survive a reload, and
|
|
630
|
+
// stack traces name nothing. A //# sourceURL comment fixes all three - the
|
|
631
|
+
// compiled script takes the component's name, so it is findable in the sources
|
|
632
|
+
// tree, keeps its breakpoints, and appears by name in stack traces.
|
|
633
|
+
//
|
|
634
|
+
// The line numbers it reports are the compiled script's own, not the .html
|
|
635
|
+
// file's: the engine wraps a Function body in a header ("function anonymous(
|
|
636
|
+
// args\n) {\n") that shifts everything down, and no amount of padding can
|
|
637
|
+
// shift code *up* to match a <script> sitting on line 1. Reporting the
|
|
638
|
+
// component's real lines would need a source map, which the runtime doesn't
|
|
639
|
+
// emit today
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
|
|
642
|
+
// where a script block came from: the component's filename, and its index
|
|
643
|
+
// among the component's scripts (two scripts in one file need distinct names,
|
|
644
|
+
// or devtools shows only one of them)
|
|
645
|
+
type ScriptLocation = { filename?: string; index?: number }
|
|
646
|
+
|
|
647
|
+
// nothing to name an inline component's scripts after, so they stay anonymous
|
|
648
|
+
const sourceUrlComment = (filename: string | undefined, index: number): string =>
|
|
649
|
+
filename ? `\n//# sourceURL=${filename}?jq79-script=${index}` : ""
|
|
650
|
+
|
|
651
|
+
// what a <style> block injects into document.head: the scoped rewrite when it
|
|
652
|
+
// has one, the source otherwise. A shadow root uses `content` directly instead
|
|
653
|
+
// - scoping is what a shadow root already does, and doing both would break the
|
|
654
|
+
// `:host` rules only shadow rendering can have (`:host[data-jq79=...]` matches
|
|
655
|
+
// nothing: the host element is outside the template, so it carries no stamp)
|
|
656
|
+
const headStyle = (style: TagBlock): string => style.scoped ?? style.content
|
|
657
|
+
|
|
518
658
|
// document.head styles are shared by content and refcounted, so N instances
|
|
519
659
|
// of the same component (e.g. one per :each item) inject a single <style> tag
|
|
520
660
|
// that goes away when the last instance is destroyed
|
|
@@ -556,7 +696,7 @@ const SETUP_HELPERS: Record<string, any> = { $, $$, $create, $reactive }
|
|
|
556
696
|
// The body is wrapped in an async IIFE so top-level `await` works: everything
|
|
557
697
|
// up to the first await runs synchronously (before the template renders), and
|
|
558
698
|
// later assignments update the DOM reactively when they happen
|
|
559
|
-
const runSetupScript = (code: string, scope: Record<string, any>, effect: (run: () => void) => void, instanceHelpers: Record<string, any> = {}, importer: (url: string) => Promise<any> = importResource) => {
|
|
699
|
+
const runSetupScript = (code: string, scope: Record<string, any>, effect: (run: () => void) => void, instanceHelpers: Record<string, any> = {}, importer: (url: string) => Promise<any> = importResource, at: ScriptLocation = {}) => {
|
|
560
700
|
// instanceHelpers are per-component-instance additions (e.g. $emit, which
|
|
561
701
|
// is bound to this instance's DOM position)
|
|
562
702
|
const helpers = { ...SETUP_HELPERS, ...instanceHelpers }
|
|
@@ -567,7 +707,7 @@ const runSetupScript = (code: string, scope: Record<string, any>, effect: (run:
|
|
|
567
707
|
})
|
|
568
708
|
const result: Promise<void> = new Function(
|
|
569
709
|
"$scope", "$__effect", "$__import", ...Object.keys(helpers),
|
|
570
|
-
`return (async () => { with ($scope) { ${code} } })()`
|
|
710
|
+
`return (async () => { with ($scope) { ${code} } })()${sourceUrlComment(at.filename, at.index ?? 0)}`
|
|
571
711
|
)(scriptScope, effect, importer, ...Object.values(helpers))
|
|
572
712
|
result.catch(error => console.error("jq79: error in :setup script", error))
|
|
573
713
|
}
|
|
@@ -583,12 +723,12 @@ const interopDefault = (mod: any) => (mod && mod.default !== undefined ? mod.def
|
|
|
583
723
|
// synchronous body invokes the factory before the first render, matching
|
|
584
724
|
// setup-script timing; bodies with top-level await (static imports included)
|
|
585
725
|
// resolve later and the template updates reactively
|
|
586
|
-
const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run: () => void) => void, instanceHelpers: Record<string, any> = {}, importer: (url: string) => Promise<any> = importResource) => {
|
|
726
|
+
const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run: () => void) => void, instanceHelpers: Record<string, any> = {}, importer: (url: string) => Promise<any> = importResource, at: ScriptLocation = {}) => {
|
|
587
727
|
const helpers = { ...SETUP_HELPERS, ...instanceHelpers }
|
|
588
728
|
const $__exports: { default?: (ctx: Record<string, any>) => any; done?: boolean } = {}
|
|
589
729
|
const result: Promise<void> = new Function(
|
|
590
730
|
"$__exports", "$__default", "$__import", ...Object.keys(helpers),
|
|
591
|
-
`return (async () => { "use strict";\n${code}\n;$__exports.done = true })()`
|
|
731
|
+
`return (async () => { "use strict";\n${code}\n;$__exports.done = true })()${sourceUrlComment(at.filename, at.index ?? 0)}`
|
|
592
732
|
)($__exports, interopDefault, importer, ...Object.values(helpers))
|
|
593
733
|
|
|
594
734
|
const logError = (error: any) => console.error("jq79: error in factory script", error)
|
|
@@ -633,6 +773,8 @@ export class Component79 {
|
|
|
633
773
|
// pre-resolved modules for setup-script `import(...)` calls (see
|
|
634
774
|
// ComponentParts.modules); checked before falling back to fetch/import
|
|
635
775
|
modules?: Record<string, any>
|
|
776
|
+
// the component's origin, used to name its scripts in devtools
|
|
777
|
+
filename?: string
|
|
636
778
|
|
|
637
779
|
data: ReactiveDeepData<Record<string, any>> | null = null
|
|
638
780
|
|
|
@@ -657,18 +799,21 @@ export class Component79 {
|
|
|
657
799
|
// outside the render generation so they survive re-render and destroy()
|
|
658
800
|
private emitListeners = new Map<string, Set<EmitListener>>()
|
|
659
801
|
|
|
660
|
-
constructor(src: string | ComponentParts, options: { modules?: Record<string, any
|
|
802
|
+
constructor(src: string | ComponentParts, options: { modules?: Record<string, any>; filename?: string } = {}) {
|
|
661
803
|
const parts = typeof src === "string" ? parseComponentString(src) : src
|
|
662
804
|
this.template = parts.template
|
|
663
805
|
this.scripts = parts.scripts
|
|
664
806
|
this.styles = parts.styles
|
|
665
807
|
this.modules = options.modules ?? (typeof src === "string" ? undefined : src.modules)
|
|
808
|
+
this.filename = options.filename ?? (typeof src === "string" ? undefined : src.filename)
|
|
666
809
|
}
|
|
667
810
|
|
|
668
811
|
static async fetch(url: string): Promise<Component79> {
|
|
669
812
|
const response = await fetch(url)
|
|
670
813
|
if (!response.ok) throw new Error(`failed to fetch component from ${url}: ${response.status}`)
|
|
671
|
-
|
|
814
|
+
// the URL names the component's scripts in devtools, and is where the
|
|
815
|
+
// browser will look for the source when a breakpoint lands in one
|
|
816
|
+
return new Component79(await response.text(), { filename: url })
|
|
672
817
|
}
|
|
673
818
|
|
|
674
819
|
// subscribes to this instance's $emit events, on top of the DOM CustomEvent
|
|
@@ -763,20 +908,25 @@ export class Component79 {
|
|
|
763
908
|
// scripts run before the template renders so `$:` values are initialized;
|
|
764
909
|
// a `:mounted` script defers entirely until mount() instead. A top-level
|
|
765
910
|
// `export default` switches the script to factory mode (plain lexical JS)
|
|
766
|
-
|
|
911
|
+
// a `:mounted` script is deferred by prepending the await on the code's own
|
|
912
|
+
// first line, so deferring doesn't shift the lines devtools reports for it
|
|
913
|
+
const defer = (code: string) => `await $mounted();${code}`
|
|
914
|
+
|
|
915
|
+
this.scripts.forEach((script, index) => {
|
|
767
916
|
const instanceHelpers = { $emit, $mounted, $self, $$self }
|
|
917
|
+
const at: ScriptLocation = { filename: this.filename, index }
|
|
768
918
|
const factoryCode = transformFactoryScript(script.content)
|
|
769
919
|
if (factoryCode !== null) {
|
|
770
|
-
const body = ":mounted" in script.attrs ?
|
|
771
|
-
runFactoryScript(body, store, fx.effect, instanceHelpers, $import)
|
|
920
|
+
const body = ":mounted" in script.attrs ? defer(factoryCode) : factoryCode
|
|
921
|
+
runFactoryScript(body, store, fx.effect, instanceHelpers, $import, at)
|
|
772
922
|
return
|
|
773
923
|
}
|
|
774
924
|
const { vars, code } = transformSetupScript(script.content)
|
|
775
925
|
// pre-declare script vars on the store so `with` resolves assignments
|
|
776
926
|
// to them (and reads of them) through the reactive proxy
|
|
777
927
|
vars.forEach(name => { if (!(name in store)) (store as any)[name] = undefined })
|
|
778
|
-
const body = ":mounted" in script.attrs ?
|
|
779
|
-
runSetupScript(body, store, fx.effect, instanceHelpers, $import)
|
|
928
|
+
const body = ":mounted" in script.attrs ? defer(code) : code
|
|
929
|
+
runSetupScript(body, store, fx.effect, instanceHelpers, $import, at)
|
|
780
930
|
})
|
|
781
931
|
|
|
782
932
|
const content = document.createDocumentFragment()
|
|
@@ -786,11 +936,11 @@ export class Component79 {
|
|
|
786
936
|
if (shadow) {
|
|
787
937
|
this.styleEls = this.styles.map(style => {
|
|
788
938
|
const el = document.createElement("style")
|
|
789
|
-
el.textContent = style.content
|
|
939
|
+
el.textContent = style.content // the source: a shadow root scopes it already
|
|
790
940
|
return el
|
|
791
941
|
})
|
|
792
942
|
} else {
|
|
793
|
-
this.styles.forEach(style => acquireStyle(style
|
|
943
|
+
this.styles.forEach(style => acquireStyle(headStyle(style)))
|
|
794
944
|
this.ownsSharedStyles = true
|
|
795
945
|
}
|
|
796
946
|
|
|
@@ -858,7 +1008,7 @@ export class Component79 {
|
|
|
858
1008
|
this.styleEls.forEach(el => el.parentNode?.removeChild(el))
|
|
859
1009
|
this.styleEls = []
|
|
860
1010
|
if (this.ownsSharedStyles) {
|
|
861
|
-
this.styles.forEach(style => releaseStyle(style
|
|
1011
|
+
this.styles.forEach(style => releaseStyle(headStyle(style)))
|
|
862
1012
|
this.ownsSharedStyles = false
|
|
863
1013
|
}
|
|
864
1014
|
this.content = null
|