jq79 0.3.22 → 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/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 componentModule = (source, include) => {
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
- return { code: componentModule(await readFile(file, "utf8"), include), map: null };
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":";AAAA,SAAS,gBAAgB;AACzB,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AA8B9B,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;AAKA,IAAM,iBAAiB;AACvB,IAAM,eAAe;AAUrB,IAAM,qBAAqB,OACzB,QACA,MACA,QACA,iBACoB;AACpB,QAAM,SAAS,CAAC,GAAG,OAAO,SAAS,cAAc,CAAC;AAClD,QAAM,WAAW,MAAM,QAAQ;AAAA,IAC7B,OAAO,IAAI,OAAO,CAAC,EAAE,OAAO,OAAO,MAAM;AACvC,YAAM,OAAO,MAAM,MAAM,YAAY;AACrC,UAAI,CAAC,KAAM,QAAO;AAClB,YAAM,YAAY,KAAK,CAAC,KAAK,KAAK,CAAC,KAAK,KAAK,CAAC;AAC9C,YAAM,SAAS,MAAM,cAAc,SAAS,GAAG,IAAI,IAAI,SAAS,IAAI,MAAM;AAC1E,aAAO,MAAM,QAAQ,YAAY;AACjC,aAAO,EAAE,OAAO,MAAM,QAAQ,cAAc,EAAE,EAAE,QAAQ,GAAG,KAAK,OAAO,KAAK;AAAA,IAC9E,CAAC;AAAA,EACH;AAEA,MAAI,MAAM;AACV,MAAI,OAAO;AACX,SAAO,QAAQ,CAAC,OAAO,MAAM;AAC3B,UAAM,OAAO,SAAS,CAAC;AACvB,QAAI,CAAC,KAAM;AACX,WAAO,OAAO,MAAM,MAAM,MAAM,KAAK,IAAI,SAAS,KAAK,KAAK,IAAI,KAAK,GAAG;AACxE,WAAO,MAAM,QAAQ,MAAM,CAAC,EAAE;AAAA,EAChC,CAAC;AACD,SAAO,MAAM,OAAO,MAAM,IAAI;AAChC;AAkBA,IAAM,kBAAkB,CAAC,QAAgB,SAAiB,aAA6B;AACrF,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,mBACT,KAAK,UAAU,QAAQ,CAAC;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;AAAA;AA8B3C;AAEO,SAAS,KAAK,UAA6B,CAAC,GAAW;AAC5D,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,EAAE,QAAQ,IAAI;AAEpB,MAAI,SAAgC;AAEpC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IAET,eAAe,UAAU;AACvB,eAAS;AAAA,IACX;AAAA,IAEA,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;AAEhD,UAAI,SAAS,MAAM,SAAS,MAAM,MAAM;AACxC,UAAI,OAAQ,UAAS,MAAM,mBAAmB,QAAQ,MAAM,QAAQ,SAAO,KAAK,aAAa,GAAG,CAAC;AAIjG,YAAM,WAAW,SAAS,SAAS,OAAO,MAAM,IAAI,IAAI;AAExD,aAAO,EAAE,MAAM,gBAAgB,QAAQ,SAAS,QAAQ,GAAG,KAAK,KAAK;AAAA,IACvE;AAAA,EACF;AACF;AAEA,IAAO,eAAQ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jq79",
3
- "version": "0.3.22",
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",
@@ -70,6 +70,7 @@
70
70
  "jsdom": "^29.1.1",
71
71
  "marked": "^18.0.6",
72
72
  "marked-highlight": "^2.2.4",
73
+ "sass": "^1.101.0",
73
74
  "tsup": "^8.5.1",
74
75
  "typescript": "^7.0.2",
75
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*(.+?)\s*}}/g, (_, expr) => evalExpr(expr, scope) ?? "")
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
- const EACH_PATTERN = /^\s*(\w+)\s+in\s+(.+)$/
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
- if (CONTROL_ATTRS.has(attr) || attr.startsWith("@")) return
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({ template: nextDef.template, scripts: nextDef.scripts, styles: nextDef.styles, modules: nextDef.modules })
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
- return new Component79(await response.text())
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
- this.scripts.forEach(script => {
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 ? `await $mounted();\n${factoryCode}` : factoryCode
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 ? `await $mounted();\n${code}` : code
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.content))
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.content))
1011
+ this.styles.forEach(style => releaseStyle(headStyle(style)))
862
1012
  this.ownsSharedStyles = false
863
1013
  }
864
1014
  this.content = null