jq79 0.3.27 → 0.3.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dev/cli.ts +57 -0
- package/dev/dev.ts +252 -0
- package/{src → dev}/vite.ts +10 -16
- package/dist/cli.js +202 -0
- package/dist/dev.cjs +186 -0
- package/dist/dev.cjs.map +1 -0
- package/dist/dev.d.ts +12 -0
- package/dist/dev.js +162 -0
- package/dist/dev.js.map +1 -0
- package/dist/jq79.cjs +8 -8
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +3 -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/reactive.d.ts +1 -0
- package/dist/vite.cjs +5 -11
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.js +5 -11
- package/dist/vite.js.map +1 -1
- package/package.json +14 -2
- package/src/jq79.ts +142 -0
- package/src/reactive.ts +53 -1
package/dist/vite.cjs
CHANGED
|
@@ -16,7 +16,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
16
|
};
|
|
17
17
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
18
|
|
|
19
|
-
//
|
|
19
|
+
// dev/vite.ts
|
|
20
20
|
var vite_exports = {};
|
|
21
21
|
__export(vite_exports, {
|
|
22
22
|
default: () => vite_default,
|
|
@@ -89,18 +89,12 @@ let component
|
|
|
89
89
|
|
|
90
90
|
if (import.meta.hot && import.meta.hot.data.component) {
|
|
91
91
|
const prior = import.meta.hot.data.component
|
|
92
|
-
const next = new Component79(src)
|
|
93
|
-
prior.template = next.template
|
|
94
|
-
prior.scripts = next.scripts
|
|
95
|
-
prior.styles = next.styles
|
|
96
92
|
prior.modules = modules
|
|
97
93
|
prior.filename = filename
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
import.meta.hot.invalidate()
|
|
103
|
-
}
|
|
94
|
+
// re-renders it where it stands, keeping its data. false means it was never
|
|
95
|
+
// rendered - a definition used only as a nested component - and a reload is
|
|
96
|
+
// the only way to reach the clones made from it
|
|
97
|
+
if (!prior.hotReplace(src) && !prior.data) import.meta.hot.invalidate()
|
|
104
98
|
component = prior
|
|
105
99
|
} else {
|
|
106
100
|
component = new Component79(src, { modules, filename })
|
package/dist/vite.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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":[]}
|
|
1
|
+
{"version":3,"sources":["../dev/vite.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\"\nimport { relative } from \"node:path\"\nimport { preprocessCSS } from \"vite\"\nimport type { Plugin, ResolvedConfig } from \"vite\"\n\n// Vite plugin: import .html single-file components as modules.\n//\n// import { jq79 } from \"jq79/vite\" // vite.config\n// import UserCard from \"./UserCard.html\" // app code\n//\n// The imported value is a Component79 built from the file's source - the same\n// thing `await Component79.fetch(url)` resolves to, but bundled at build time\n// instead of fetched at runtime. The component source is inlined verbatim, so\n// a file keeps working unchanged if it's ever served from public/ and loaded\n// with fetch instead - with one deliberate exception: <style lang=\"scss\"> (or\n// less/stylus/sass) is compiled to plain CSS here. A component using `lang`\n// therefore only works through the bundler; loaded with fetch() it would\n// reach the runtime uncompiled, which the runtime warns about.\n//\n// Only .html files imported from other modules are claimed; entry points\n// (index.html) have no importer and imports carrying an explicit query\n// (?raw, ?url) keep their built-in Vite meaning.\n\nexport interface Jq79PluginOptions {\n // which import specifiers are treated as components (default: any .html)\n include?: RegExp\n // resolved absolute paths to skip even when `include` matches\n exclude?: RegExp\n}\n\n// claimed modules get this suffix so their id no longer ends in \".html\" and\n// Vite's own html handling (entries, asset pipeline) leaves them alone\nconst COMPONENT_QUERY = \"?jq79\"\n\nconst SCRIPT_BLOCK_RE = /<script\\b[^>]*>([\\s\\S]*?)<\\/script\\s*>/gi\n// import(\"...\") with a literal specifier; the lookbehind skips $__import and\n// property accesses like foo.import(...)\nconst IMPORT_LITERAL_RE = /(?<![\\w$.])import\\s*\\(\\s*([\"'])([^\"'\\n]+?)\\1\\s*\\)/g\n// static import statements (factory scripts): optional clause + literal\n// specifier. The clause can't contain parens/quotes, so dynamic import()\n// and import.meta never match\nconst STATIC_IMPORT_LITERAL_RE = /(?<![\\w$.])import\\s*(?:[\\w$\\s,{}*]+?\\s*from\\s*)?([\"'])([^\"'\\n]+)\\1/g\n\nconst isHtmlUrl = (spec: string) => /\\.html?([?#]|$)/.test(spec)\nconst isExternalUrl = (spec: string) => /^[a-z][a-z0-9+.-]*:/i.test(spec) || spec.startsWith(\"/\")\n\n// literal import specifiers in the component's script blocks - dynamic\n// `import(\"...\")` calls and static factory-script imports - that should\n// resolve from the bundle instead of at runtime. Absolute paths and full\n// URLs are left alone (they point at served files, e.g. public/), and so\n// are .html specifiers the plugin wouldn't claim as components\nconst hoistableImports = (source: string, include: RegExp): string[] => {\n const specifiers = new Set<string>()\n for (const [, script] of source.matchAll(SCRIPT_BLOCK_RE)) {\n const specs = [\n ...[...script.matchAll(IMPORT_LITERAL_RE)].map(match => match[2]),\n ...[...script.matchAll(STATIC_IMPORT_LITERAL_RE)].map(match => match[2]),\n ]\n for (const spec of specs) {\n if (isExternalUrl(spec)) continue\n if (isHtmlUrl(spec) && !include.test(spec)) continue // html left to runtime fetch\n specifiers.add(spec) // a claimed component, a source file or an npm package\n }\n }\n return [...specifiers]\n}\n\n// a <style> block with its attribute string, so `lang` can be read and the\n// content replaced. Attribute values are matched as quoted chunks so a \">\"\n// inside one doesn't end the tag early\nconst STYLE_BLOCK_RE = /<style((?:\"[^\"]*\"|'[^']*'|[^>\"'])*)>([\\s\\S]*?)<\\/style\\s*>/gi\nconst LANG_ATTR_RE = /\\blang\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/i\n\n// compiles <style lang=\"scss|less|styl|sass\"> blocks to plain CSS with Vite's\n// own preprocessing (the same call @vitejs/plugin-vue makes), so the runtime\n// only ever sees CSS. The preprocessor picks its parser from the extension,\n// and resolving relative @use/@import against a filename in the component's\n// own directory is what makes `@use \"./vars\"` work. Files the preprocessor\n// pulls in are registered as watch deps, so editing a partial re-runs HMR for\n// every component that uses it. `lang` is dropped from the emitted tag: what\n// the runtime parses is a plain <style> (with `scoped` and the rest intact)\nconst compileStyleBlocks = async (\n source: string,\n file: string,\n config: ResolvedConfig,\n addWatchFile: (id: string) => void\n): Promise<string> => {\n const blocks = [...source.matchAll(STYLE_BLOCK_RE)]\n const compiled = await Promise.all(\n blocks.map(async ([, attrs, content]) => {\n const lang = attrs.match(LANG_ATTR_RE)\n if (!lang) return null\n const extension = lang[1] ?? lang[2] ?? lang[3]\n const result = await preprocessCSS(content, `${file}.${extension}`, config)\n result.deps?.forEach(addWatchFile)\n return { attrs: attrs.replace(LANG_ATTR_RE, \"\").trimEnd(), css: result.code }\n })\n )\n\n let out = \"\"\n let last = 0\n blocks.forEach((block, i) => {\n const done = compiled[i]\n if (!done) return\n out += source.slice(last, block.index) + `<style${done.attrs}>${done.css}</style>`\n last = block.index + block[0].length\n })\n return out + source.slice(last)\n}\n\n// the emitted module. Literal import(\"...\") specifiers found in the\n// component's scripts become real module imports, handed to Component79 as a\n// resolution map: at runtime $__import checks the map before falling back to\n// fetch, so bundled components ship with their imports and nothing changes\n// for unbundled ones. Claimed components import as their default (a\n// Component79, matching what runtime fetch resolves to); everything else as\n// a namespace (matching native import()).\n//\n// In dev, `hot.data` carries the exported instance across updates: importers\n// hold a reference to the *first* module evaluation's instance, so later\n// evaluations patch that same instance in place instead of exporting a new one\n// nobody sees. The patching itself is the runtime's `hotReplace` - the same\n// swap the jq79/dev server drives, from the one place that can reach a\n// component's markers. An instance only used as a definition has nothing to\n// re-render (nested clones can't be reached from this module), so it falls\n// back to a full reload.\nconst componentModule = (source: string, include: RegExp, filename: string): string => {\n const hoisted = hoistableImports(source, include)\n const imports = hoisted\n .map((spec, i) =>\n include.test(spec)\n ? `import __jq79_${i} from ${JSON.stringify(spec)}`\n : `import * as __jq79_${i} from ${JSON.stringify(spec)}`\n )\n .join(\"\\n\")\n const modulesMap = `{ ${hoisted.map((spec, i) => `${JSON.stringify(spec)}: __jq79_${i}`).join(\", \")} }`\n\n return `\nimport { Component79 } from \"jq79\"\n${imports}\n\nconst src = ${JSON.stringify(source)}\nconst modules = ${modulesMap}\nconst filename = ${JSON.stringify(filename)}\n\nlet component\n\nif (import.meta.hot && import.meta.hot.data.component) {\n const prior = import.meta.hot.data.component\n prior.modules = modules\n prior.filename = filename\n // re-renders it where it stands, keeping its data. false means it was never\n // rendered - a definition used only as a nested component - and a reload is\n // the only way to reach the clones made from it\n if (!prior.hotReplace(src) && !prior.data) import.meta.hot.invalidate()\n component = prior\n} else {\n component = new Component79(src, { modules, filename })\n}\n\nif (import.meta.hot) {\n import.meta.hot.data.component = component\n import.meta.hot.accept()\n}\n\nexport default component\n`\n}\n\nexport function jq79(options: Jq79PluginOptions = {}): Plugin {\n const include = options.include ?? /\\.html$/\n const { exclude } = options\n\n let config: ResolvedConfig | null = null\n\n return {\n name: \"jq79\",\n enforce: \"pre\",\n\n configResolved(resolved) {\n config = resolved\n },\n\n async resolveId(source, importer) {\n if (!importer) return null // entry points are never components\n if (source.includes(\"?\")) return null // ?raw, ?url, ... keep their meaning\n if (!include.test(source)) return null\n\n const resolved = await this.resolve(source, importer, { skipSelf: true })\n if (!resolved || resolved.external) return null\n if (exclude?.test(resolved.id)) return null\n return resolved.id + COMPONENT_QUERY\n },\n\n async load(id) {\n if (!id.endsWith(COMPONENT_QUERY)) return null\n const file = id.slice(0, -COMPONENT_QUERY.length)\n\n let source = await readFile(file, \"utf8\")\n if (config) source = await compileStyleBlocks(source, file, config, dep => this.addWatchFile(dep))\n\n // the runtime names the component's setup scripts after this, so devtools\n // shows a path the user recognizes instead of an anonymous VM script\n const filename = config ? relative(config.root, file) : file\n\n return { code: componentModule(source, include, filename), map: null }\n },\n }\n}\n\nexport default jq79\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAAyB;AACzB,uBAAyB;AACzB,kBAA8B;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,UAAM,2BAAc,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;AAwB3C;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,UAAM,0BAAS,MAAM,MAAM;AACxC,UAAI,OAAQ,UAAS,MAAM,mBAAmB,QAAQ,MAAM,QAAQ,SAAO,KAAK,aAAa,GAAG,CAAC;AAIjG,YAAM,WAAW,aAAS,2BAAS,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/dist/vite.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// dev/vite.ts
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
3
|
import { relative } from "path";
|
|
4
4
|
import { preprocessCSS } from "vite";
|
|
@@ -65,18 +65,12 @@ let component
|
|
|
65
65
|
|
|
66
66
|
if (import.meta.hot && import.meta.hot.data.component) {
|
|
67
67
|
const prior = import.meta.hot.data.component
|
|
68
|
-
const next = new Component79(src)
|
|
69
|
-
prior.template = next.template
|
|
70
|
-
prior.scripts = next.scripts
|
|
71
|
-
prior.styles = next.styles
|
|
72
68
|
prior.modules = modules
|
|
73
69
|
prior.filename = filename
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
import.meta.hot.invalidate()
|
|
79
|
-
}
|
|
70
|
+
// re-renders it where it stands, keeping its data. false means it was never
|
|
71
|
+
// rendered - a definition used only as a nested component - and a reload is
|
|
72
|
+
// the only way to reach the clones made from it
|
|
73
|
+
if (!prior.hotReplace(src) && !prior.data) import.meta.hot.invalidate()
|
|
80
74
|
component = prior
|
|
81
75
|
} else {
|
|
82
76
|
component = new Component79(src, { modules, filename })
|
package/dist/vite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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":[]}
|
|
1
|
+
{"version":3,"sources":["../dev/vite.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\"\nimport { relative } from \"node:path\"\nimport { preprocessCSS } from \"vite\"\nimport type { Plugin, ResolvedConfig } from \"vite\"\n\n// Vite plugin: import .html single-file components as modules.\n//\n// import { jq79 } from \"jq79/vite\" // vite.config\n// import UserCard from \"./UserCard.html\" // app code\n//\n// The imported value is a Component79 built from the file's source - the same\n// thing `await Component79.fetch(url)` resolves to, but bundled at build time\n// instead of fetched at runtime. The component source is inlined verbatim, so\n// a file keeps working unchanged if it's ever served from public/ and loaded\n// with fetch instead - with one deliberate exception: <style lang=\"scss\"> (or\n// less/stylus/sass) is compiled to plain CSS here. A component using `lang`\n// therefore only works through the bundler; loaded with fetch() it would\n// reach the runtime uncompiled, which the runtime warns about.\n//\n// Only .html files imported from other modules are claimed; entry points\n// (index.html) have no importer and imports carrying an explicit query\n// (?raw, ?url) keep their built-in Vite meaning.\n\nexport interface Jq79PluginOptions {\n // which import specifiers are treated as components (default: any .html)\n include?: RegExp\n // resolved absolute paths to skip even when `include` matches\n exclude?: RegExp\n}\n\n// claimed modules get this suffix so their id no longer ends in \".html\" and\n// Vite's own html handling (entries, asset pipeline) leaves them alone\nconst COMPONENT_QUERY = \"?jq79\"\n\nconst SCRIPT_BLOCK_RE = /<script\\b[^>]*>([\\s\\S]*?)<\\/script\\s*>/gi\n// import(\"...\") with a literal specifier; the lookbehind skips $__import and\n// property accesses like foo.import(...)\nconst IMPORT_LITERAL_RE = /(?<![\\w$.])import\\s*\\(\\s*([\"'])([^\"'\\n]+?)\\1\\s*\\)/g\n// static import statements (factory scripts): optional clause + literal\n// specifier. The clause can't contain parens/quotes, so dynamic import()\n// and import.meta never match\nconst STATIC_IMPORT_LITERAL_RE = /(?<![\\w$.])import\\s*(?:[\\w$\\s,{}*]+?\\s*from\\s*)?([\"'])([^\"'\\n]+)\\1/g\n\nconst isHtmlUrl = (spec: string) => /\\.html?([?#]|$)/.test(spec)\nconst isExternalUrl = (spec: string) => /^[a-z][a-z0-9+.-]*:/i.test(spec) || spec.startsWith(\"/\")\n\n// literal import specifiers in the component's script blocks - dynamic\n// `import(\"...\")` calls and static factory-script imports - that should\n// resolve from the bundle instead of at runtime. Absolute paths and full\n// URLs are left alone (they point at served files, e.g. public/), and so\n// are .html specifiers the plugin wouldn't claim as components\nconst hoistableImports = (source: string, include: RegExp): string[] => {\n const specifiers = new Set<string>()\n for (const [, script] of source.matchAll(SCRIPT_BLOCK_RE)) {\n const specs = [\n ...[...script.matchAll(IMPORT_LITERAL_RE)].map(match => match[2]),\n ...[...script.matchAll(STATIC_IMPORT_LITERAL_RE)].map(match => match[2]),\n ]\n for (const spec of specs) {\n if (isExternalUrl(spec)) continue\n if (isHtmlUrl(spec) && !include.test(spec)) continue // html left to runtime fetch\n specifiers.add(spec) // a claimed component, a source file or an npm package\n }\n }\n return [...specifiers]\n}\n\n// a <style> block with its attribute string, so `lang` can be read and the\n// content replaced. Attribute values are matched as quoted chunks so a \">\"\n// inside one doesn't end the tag early\nconst STYLE_BLOCK_RE = /<style((?:\"[^\"]*\"|'[^']*'|[^>\"'])*)>([\\s\\S]*?)<\\/style\\s*>/gi\nconst LANG_ATTR_RE = /\\blang\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/i\n\n// compiles <style lang=\"scss|less|styl|sass\"> blocks to plain CSS with Vite's\n// own preprocessing (the same call @vitejs/plugin-vue makes), so the runtime\n// only ever sees CSS. The preprocessor picks its parser from the extension,\n// and resolving relative @use/@import against a filename in the component's\n// own directory is what makes `@use \"./vars\"` work. Files the preprocessor\n// pulls in are registered as watch deps, so editing a partial re-runs HMR for\n// every component that uses it. `lang` is dropped from the emitted tag: what\n// the runtime parses is a plain <style> (with `scoped` and the rest intact)\nconst compileStyleBlocks = async (\n source: string,\n file: string,\n config: ResolvedConfig,\n addWatchFile: (id: string) => void\n): Promise<string> => {\n const blocks = [...source.matchAll(STYLE_BLOCK_RE)]\n const compiled = await Promise.all(\n blocks.map(async ([, attrs, content]) => {\n const lang = attrs.match(LANG_ATTR_RE)\n if (!lang) return null\n const extension = lang[1] ?? lang[2] ?? lang[3]\n const result = await preprocessCSS(content, `${file}.${extension}`, config)\n result.deps?.forEach(addWatchFile)\n return { attrs: attrs.replace(LANG_ATTR_RE, \"\").trimEnd(), css: result.code }\n })\n )\n\n let out = \"\"\n let last = 0\n blocks.forEach((block, i) => {\n const done = compiled[i]\n if (!done) return\n out += source.slice(last, block.index) + `<style${done.attrs}>${done.css}</style>`\n last = block.index + block[0].length\n })\n return out + source.slice(last)\n}\n\n// the emitted module. Literal import(\"...\") specifiers found in the\n// component's scripts become real module imports, handed to Component79 as a\n// resolution map: at runtime $__import checks the map before falling back to\n// fetch, so bundled components ship with their imports and nothing changes\n// for unbundled ones. Claimed components import as their default (a\n// Component79, matching what runtime fetch resolves to); everything else as\n// a namespace (matching native import()).\n//\n// In dev, `hot.data` carries the exported instance across updates: importers\n// hold a reference to the *first* module evaluation's instance, so later\n// evaluations patch that same instance in place instead of exporting a new one\n// nobody sees. The patching itself is the runtime's `hotReplace` - the same\n// swap the jq79/dev server drives, from the one place that can reach a\n// component's markers. An instance only used as a definition has nothing to\n// re-render (nested clones can't be reached from this module), so it falls\n// back to a full reload.\nconst componentModule = (source: string, include: RegExp, filename: string): string => {\n const hoisted = hoistableImports(source, include)\n const imports = hoisted\n .map((spec, i) =>\n include.test(spec)\n ? `import __jq79_${i} from ${JSON.stringify(spec)}`\n : `import * as __jq79_${i} from ${JSON.stringify(spec)}`\n )\n .join(\"\\n\")\n const modulesMap = `{ ${hoisted.map((spec, i) => `${JSON.stringify(spec)}: __jq79_${i}`).join(\", \")} }`\n\n return `\nimport { Component79 } from \"jq79\"\n${imports}\n\nconst src = ${JSON.stringify(source)}\nconst modules = ${modulesMap}\nconst filename = ${JSON.stringify(filename)}\n\nlet component\n\nif (import.meta.hot && import.meta.hot.data.component) {\n const prior = import.meta.hot.data.component\n prior.modules = modules\n prior.filename = filename\n // re-renders it where it stands, keeping its data. false means it was never\n // rendered - a definition used only as a nested component - and a reload is\n // the only way to reach the clones made from it\n if (!prior.hotReplace(src) && !prior.data) import.meta.hot.invalidate()\n component = prior\n} else {\n component = new Component79(src, { modules, filename })\n}\n\nif (import.meta.hot) {\n import.meta.hot.data.component = component\n import.meta.hot.accept()\n}\n\nexport default component\n`\n}\n\nexport function jq79(options: Jq79PluginOptions = {}): Plugin {\n const include = options.include ?? /\\.html$/\n const { exclude } = options\n\n let config: ResolvedConfig | null = null\n\n return {\n name: \"jq79\",\n enforce: \"pre\",\n\n configResolved(resolved) {\n config = resolved\n },\n\n async resolveId(source, importer) {\n if (!importer) return null // entry points are never components\n if (source.includes(\"?\")) return null // ?raw, ?url, ... keep their meaning\n if (!include.test(source)) return null\n\n const resolved = await this.resolve(source, importer, { skipSelf: true })\n if (!resolved || resolved.external) return null\n if (exclude?.test(resolved.id)) return null\n return resolved.id + COMPONENT_QUERY\n },\n\n async load(id) {\n if (!id.endsWith(COMPONENT_QUERY)) return null\n const file = id.slice(0, -COMPONENT_QUERY.length)\n\n let source = await readFile(file, \"utf8\")\n if (config) source = await compileStyleBlocks(source, file, config, dep => this.addWatchFile(dep))\n\n // the runtime names the component's setup scripts after this, so devtools\n // shows a path the user recognizes instead of an anonymous VM script\n const filename = config ? relative(config.root, file) : file\n\n return { code: componentModule(source, include, filename), map: null }\n },\n }\n}\n\nexport default jq79\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jq79",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.29",
|
|
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",
|
|
@@ -36,18 +36,30 @@
|
|
|
36
36
|
"import": "./dist/vite.js",
|
|
37
37
|
"require": "./dist/vite.cjs",
|
|
38
38
|
"default": "./dist/vite.js"
|
|
39
|
+
},
|
|
40
|
+
"./dev": {
|
|
41
|
+
"types": "./dist/dev.d.ts",
|
|
42
|
+
"import": "./dist/dev.js",
|
|
43
|
+
"require": "./dist/dev.cjs",
|
|
44
|
+
"default": "./dist/dev.js"
|
|
39
45
|
}
|
|
40
46
|
},
|
|
47
|
+
"bin": {
|
|
48
|
+
"jq79": "./dist/cli.js"
|
|
49
|
+
},
|
|
41
50
|
"unpkg": "./dist/jq79.global.js",
|
|
42
51
|
"jsdelivr": "./dist/jq79.global.js",
|
|
43
52
|
"files": [
|
|
44
53
|
"dist",
|
|
45
54
|
"src",
|
|
55
|
+
"dev",
|
|
46
56
|
"assets"
|
|
47
57
|
],
|
|
48
58
|
"sideEffects": false,
|
|
49
59
|
"scripts": {
|
|
50
|
-
"build": "tsup &&
|
|
60
|
+
"build": "tsup && npm run types.lib && npm run types.dev",
|
|
61
|
+
"types.lib": "tsc src/jq79.ts --rootDir src --declaration --emitDeclarationOnly --strict --skipLibCheck --lib dom,es2021 --target es2020 --module es2020 --moduleResolution bundler --outDir dist",
|
|
62
|
+
"types.dev": "tsc dev/vite.ts dev/dev.ts --rootDir dev --declaration --emitDeclarationOnly --strict --skipLibCheck --lib dom,es2021 --target es2020 --module es2020 --moduleResolution bundler --outDir dist",
|
|
51
63
|
"test": "vitest run",
|
|
52
64
|
"test:coverage": "vitest run --coverage",
|
|
53
65
|
"site": "node scripts/build-site.mjs",
|
package/src/jq79.ts
CHANGED
|
@@ -784,6 +784,88 @@ const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run
|
|
|
784
784
|
if ($__exports.done) invoke() // fully-sync body: factory runs before first render
|
|
785
785
|
}
|
|
786
786
|
|
|
787
|
+
// ---------------------------------------------------------------------------
|
|
788
|
+
// hot reload
|
|
789
|
+
//
|
|
790
|
+
// Both delivery paths want the same thing when a .html file changes: reparse
|
|
791
|
+
// it, and re-render every live instance of it in place, keeping its data. The
|
|
792
|
+
// swap lives in the runtime (hotReplace, below) so jq79/dev and the Vite
|
|
793
|
+
// plugin share one implementation instead of two - and so it can reach the
|
|
794
|
+
// private fields it needs (the markers, the holding fragment) rather than
|
|
795
|
+
// poking at them from outside, which is what the plugin used to do.
|
|
796
|
+
//
|
|
797
|
+
// Finding the instances is the part only the runtime can do: a component
|
|
798
|
+
// fetched at runtime is reachable from nothing but the DOM it rendered. So
|
|
799
|
+
// instances register themselves - but only once a page opts in, before the
|
|
800
|
+
// runtime loads. Nothing here costs a bundled app anything: with the registry
|
|
801
|
+
// off, an instance is not tracked at all.
|
|
802
|
+
// ---------------------------------------------------------------------------
|
|
803
|
+
|
|
804
|
+
const HOT_FLAG = "__JQ79_HMR_ENABLED__"
|
|
805
|
+
const HOT_RUNTIME = "__JQ79_HMR__"
|
|
806
|
+
|
|
807
|
+
// live instances by filename. WeakRef because a destroyed component that the
|
|
808
|
+
// page has dropped must stay collectable: `:each` churns through clones
|
|
809
|
+
let hotRegistry: Map<string, Set<WeakRef<Component79>>> | null = null
|
|
810
|
+
|
|
811
|
+
const hotRegister = (instance: Component79) => {
|
|
812
|
+
if (!hotRegistry || !instance.filename) return
|
|
813
|
+
let refs = hotRegistry.get(instance.filename)
|
|
814
|
+
if (!refs) hotRegistry.set(instance.filename, (refs = new Set()))
|
|
815
|
+
refs.add(new WeakRef(instance))
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// the same file reaches the runtime under different names - "./card.html" from
|
|
819
|
+
// an import() in a setup script, "/cards/card.html" from a fetch, "cards/card.
|
|
820
|
+
// html" from the dev server that watched it - and they all have to land on one
|
|
821
|
+
// key. Resolving against the page is what settles them
|
|
822
|
+
const hotKey = (filename: string): string => {
|
|
823
|
+
try {
|
|
824
|
+
return new URL(filename, document.baseURI).pathname
|
|
825
|
+
} catch {
|
|
826
|
+
return filename
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// swaps the file's new source into every instance that came from `filename`,
|
|
831
|
+
// and returns how many of them were *on the page* and so re-rendered. Zero
|
|
832
|
+
// means the change is not visible anywhere - the file is a page rather than a
|
|
833
|
+
// component, or nothing has mounted it yet - and the caller (a dev server)
|
|
834
|
+
// should fall back to reloading. Definitions and instances that have been
|
|
835
|
+
// destroyed but not yet collected are patched all the same; they just don't
|
|
836
|
+
// count, because nothing on screen changed for them
|
|
837
|
+
export const hotUpdate = (filename: string, src: string): number => {
|
|
838
|
+
if (!hotRegistry) return 0
|
|
839
|
+
|
|
840
|
+
const key = hotKey(filename)
|
|
841
|
+
// parsed once and shared by every instance - which is already what a
|
|
842
|
+
// definition and the clones :component makes from it do
|
|
843
|
+
const parts = parseComponentString(src)
|
|
844
|
+
|
|
845
|
+
let rerendered = 0
|
|
846
|
+
for (const [name, refs] of hotRegistry) {
|
|
847
|
+
if (hotKey(name) !== key) continue
|
|
848
|
+
for (const ref of refs) {
|
|
849
|
+
const instance = ref.deref()
|
|
850
|
+
if (!instance) {
|
|
851
|
+
refs.delete(ref) // collected since the last update
|
|
852
|
+
continue
|
|
853
|
+
}
|
|
854
|
+
if (instance.hotReplace(parts)) rerendered++
|
|
855
|
+
}
|
|
856
|
+
if (!refs.size) hotRegistry.delete(name)
|
|
857
|
+
}
|
|
858
|
+
return rerendered
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// starts tracking instances, so hotUpdate can find them. jq79/dev's client
|
|
862
|
+
// calls this through the global handshake at the foot of this file; it is
|
|
863
|
+
// exported so a bundled app - or a test - can opt in directly
|
|
864
|
+
export const enableHotReload = (): void => {
|
|
865
|
+
hotRegistry ??= new Map()
|
|
866
|
+
;(globalThis as any)[HOT_RUNTIME] = { update: hotUpdate }
|
|
867
|
+
}
|
|
868
|
+
|
|
787
869
|
type EmitListener = (event: CustomEvent, payload: any) => void
|
|
788
870
|
|
|
789
871
|
// a parsed single-file component. Typical lifecycle:
|
|
@@ -834,6 +916,56 @@ export class Component79 {
|
|
|
834
916
|
this.styles = parts.styles
|
|
835
917
|
this.modules = options.modules ?? (typeof src === "string" ? undefined : src.modules)
|
|
836
918
|
this.filename = options.filename ?? (typeof src === "string" ? undefined : src.filename)
|
|
919
|
+
hotRegister(this) // a no-op unless the page enabled hot reload
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// swaps this component's parsed parts for `src`'s and, if it is on the page,
|
|
923
|
+
// re-renders it where it stands - seeded with a snapshot of its data, so
|
|
924
|
+
// props and store values survive (the setup script runs again, so whatever it
|
|
925
|
+
// initializes is reset). Returns whether it re-rendered: an instance that was
|
|
926
|
+
// never rendered is a *definition*, and patching its parts is all there is to
|
|
927
|
+
// do - the clones :component made from it are instances in their own right,
|
|
928
|
+
// registered under the same filename, and re-render themselves.
|
|
929
|
+
//
|
|
930
|
+
// Dev-only, and not part of the public API: jq79/dev and the Vite plugin call
|
|
931
|
+
// it when a file changes. It re-attaches against the markers rather than
|
|
932
|
+
// mountRoot on purpose - a nested clone is mounted into a fragment that is
|
|
933
|
+
// then emptied into the page, so its mountRoot is a stale, detached fragment
|
|
934
|
+
// while its markers sit where its DOM actually is
|
|
935
|
+
hotReplace(src: string | ComponentParts): boolean {
|
|
936
|
+
const parts = typeof src === "string" ? parseComponentString(src) : src
|
|
937
|
+
const marker = this.startMarker
|
|
938
|
+
const rendered = !!(marker && this.content)
|
|
939
|
+
|
|
940
|
+
// where its output sits now, if it is on the page. A rendered-but-detached
|
|
941
|
+
// instance (markers in the holding fragment) re-renders detached, and a
|
|
942
|
+
// later mount() attaches the new output - like any update it missed away
|
|
943
|
+
const live = rendered && marker!.isConnected
|
|
944
|
+
const parent = live ? (marker!.parentNode as Element | ShadowRoot | DocumentFragment) : null
|
|
945
|
+
const before = live ? this.endMarker!.nextSibling : null
|
|
946
|
+
const data = { ...this.data }
|
|
947
|
+
const shadow = this.useShadow
|
|
948
|
+
|
|
949
|
+
// destroy() releases the styles it acquired, so it has to run while
|
|
950
|
+
// this.styles is still the *old* set - swapping the parts first would leak
|
|
951
|
+
// the old stylesheet into the head and release a new one nobody holds
|
|
952
|
+
if (rendered) this.destroy()
|
|
953
|
+
|
|
954
|
+
this.template = parts.template
|
|
955
|
+
this.scripts = parts.scripts
|
|
956
|
+
this.styles = parts.styles
|
|
957
|
+
if (!rendered) return false // a definition: its clones re-render themselves
|
|
958
|
+
|
|
959
|
+
this.renderWith(data, shadow)
|
|
960
|
+
if (!parent) return false
|
|
961
|
+
|
|
962
|
+
// shadow styles live inline, right before the DOM they style (attach()
|
|
963
|
+
// appends them ahead of the content), so they go back the same way
|
|
964
|
+
if (shadow) this.styleEls.forEach(el => parent.insertBefore(el, before))
|
|
965
|
+
parent.insertBefore(this.content!, before)
|
|
966
|
+
this.mountRoot = parent
|
|
967
|
+
this.resolveMounted?.()
|
|
968
|
+
return true
|
|
837
969
|
}
|
|
838
970
|
|
|
839
971
|
static async fetch(url: string): Promise<Component79> {
|
|
@@ -1033,6 +1165,9 @@ export class Component79 {
|
|
|
1033
1165
|
this.detach()
|
|
1034
1166
|
this.fx?.dispose()
|
|
1035
1167
|
this.fx = null
|
|
1168
|
+
// a store this component was handed (a shared `$reactive`) outlives it, and
|
|
1169
|
+
// holds a listener per store that nested it - drop this instance's
|
|
1170
|
+
this.data?.$dispose()
|
|
1036
1171
|
this.styleEls.forEach(el => el.parentNode?.removeChild(el))
|
|
1037
1172
|
this.styleEls = []
|
|
1038
1173
|
if (this.ownsSharedStyles) {
|
|
@@ -1050,3 +1185,10 @@ export class Component79 {
|
|
|
1050
1185
|
|
|
1051
1186
|
export const parseComponent = (component: string): Component79 => new Component79(component)
|
|
1052
1187
|
|
|
1188
|
+
// the hot-reload handshake. jq79/dev serves a classic script that sets the flag
|
|
1189
|
+
// below; classic scripts run before deferred module ones, so the flag is always
|
|
1190
|
+
// set before this module evaluates. The page's copy of the runtime can come from
|
|
1191
|
+
// anywhere - a CDN, an import map, dist/ - and the dev client has no way to
|
|
1192
|
+
// import *that* copy, so the runtime hands itself to the client instead
|
|
1193
|
+
if (typeof globalThis !== "undefined" && (globalThis as any)[HOT_FLAG]) enableHotReload()
|
|
1194
|
+
|
package/src/reactive.ts
CHANGED
|
@@ -13,6 +13,10 @@ export type ReactiveDeepData<T> = T & {
|
|
|
13
13
|
// runs `run` immediately, recording every dotKey it reads off this store, then
|
|
14
14
|
// re-runs it whenever a changed dotKey overlaps one of those - see pathsOverlap
|
|
15
15
|
$effect: (run: () => void) => Unsubscribe
|
|
16
|
+
// drops this store's subscriptions to the stores nested inside it (see
|
|
17
|
+
// bridge). A store that outlives the one holding it - the shared-state case -
|
|
18
|
+
// would otherwise keep the dead holder's listeners on its own list forever
|
|
19
|
+
$dispose: () => void
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
const getByPath = (obj: Record<string, any>, dotKey: string): any =>
|
|
@@ -116,6 +120,33 @@ export const $reactive = <T extends Record<string, any>>(data: T): ReactiveDeepD
|
|
|
116
120
|
const isWrappable = (value: any): value is Record<string, any> =>
|
|
117
121
|
value !== null && typeof value === "object" && isPlainData(value)
|
|
118
122
|
|
|
123
|
+
// a store nested inside this one keeps its own listeners and its own effects,
|
|
124
|
+
// and this store's effects are not among them - so a write through the inner
|
|
125
|
+
// store notifies nobody out here, and a component rendering `{{ cart.items }}`
|
|
126
|
+
// off a `$reactive` it was handed would never update. The holder subscribes
|
|
127
|
+
// instead, and re-notifies the inner store's changes under the path it sits at
|
|
128
|
+
// ("items.0" -> "cart.items.0"). An effect that read through `cart` recorded
|
|
129
|
+
// exactly that path's ancestor as a dependency, so pathsOverlap wakes it.
|
|
130
|
+
// Chains compose: re-notifying runs this store's own $onAny listeners, which
|
|
131
|
+
// is how a store two levels down still reaches the top
|
|
132
|
+
const bridges = new Map<string, { store: any; unsubscribe: Unsubscribe }>()
|
|
133
|
+
|
|
134
|
+
const bridge = (store: any, path: string) => {
|
|
135
|
+
const current = bridges.get(path)
|
|
136
|
+
if (current?.store === store) return
|
|
137
|
+
current?.unsubscribe()
|
|
138
|
+
bridges.set(path, {
|
|
139
|
+
store,
|
|
140
|
+
unsubscribe: store.$onAny((dotKey: string, value: any) => notify(`${path}.${dotKey}`, value)),
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// the key no longer holds the store it held: stop listening to it
|
|
145
|
+
const unbridge = (path: string) => {
|
|
146
|
+
bridges.get(path)?.unsubscribe()
|
|
147
|
+
bridges.delete(path)
|
|
148
|
+
}
|
|
149
|
+
|
|
119
150
|
// the reactive view of `raw`, created on demand. Callers must hand it a raw
|
|
120
151
|
// object (see toRaw at both call sites): wrapping a proxy is what compounds
|
|
121
152
|
const wrap = (raw: Record<string, any>, path: string): Record<string, any> => {
|
|
@@ -135,7 +166,10 @@ export const $reactive = <T extends Record<string, any>>(data: T): ReactiveDeepD
|
|
|
135
166
|
// nested objects are wrapped here rather than up front, so the object
|
|
136
167
|
// handed to $reactive is never rewritten
|
|
137
168
|
const value = Reflect.get(target, key, receiver)
|
|
138
|
-
if (isStore(value))
|
|
169
|
+
if (isStore(value)) {
|
|
170
|
+
bridge(value, dotKey)
|
|
171
|
+
return value
|
|
172
|
+
}
|
|
139
173
|
|
|
140
174
|
const raw = toRaw(value)
|
|
141
175
|
return isWrappable(raw) ? wrap(raw, dotKey) : raw
|
|
@@ -160,6 +194,8 @@ export const $reactive = <T extends Record<string, any>>(data: T): ReactiveDeepD
|
|
|
160
194
|
const stored = isStore(value) ? value : toRaw(value)
|
|
161
195
|
const isNewKey = !Object.prototype.hasOwnProperty.call(target, key)
|
|
162
196
|
target[key] = stored
|
|
197
|
+
if (isStore(stored)) bridge(stored, dotKey)
|
|
198
|
+
else unbridge(dotKey)
|
|
163
199
|
const notified = isStore(stored) || !isWrappable(stored) ? stored : wrap(stored, dotKey)
|
|
164
200
|
notify(dotKey, notified, isNewKey)
|
|
165
201
|
return true
|
|
@@ -172,6 +208,16 @@ export const $reactive = <T extends Record<string, any>>(data: T): ReactiveDeepD
|
|
|
172
208
|
|
|
173
209
|
const reactive = wrap(toRaw(data), "") as ReactiveDeepData<T>
|
|
174
210
|
|
|
211
|
+
// a store handed in with the data (a prop, or render data) is bridged here
|
|
212
|
+
// rather than on first read, so a listener registered before anything reads
|
|
213
|
+
// the key still hears it. Only the top level is scanned: that's where a prop
|
|
214
|
+
// lands, and descending would mean walking whatever else was handed in - a
|
|
215
|
+
// highlighter, an API client - to its leaves. A store sitting deeper is
|
|
216
|
+
// bridged when the read that reaches it wraps its parent
|
|
217
|
+
Object.entries(toRaw(data)).forEach(([key, value]) => {
|
|
218
|
+
if (isStore(value)) bridge(value, key)
|
|
219
|
+
})
|
|
220
|
+
|
|
175
221
|
const $on = (dotKey: string, listener: ChangeListener, { immediate = false }: ListenerOptions = {}): Unsubscribe => {
|
|
176
222
|
if (!exactListeners.has(dotKey)) exactListeners.set(dotKey, new Set())
|
|
177
223
|
exactListeners.get(dotKey)!.add(listener)
|
|
@@ -204,9 +250,15 @@ export const $reactive = <T extends Record<string, any>>(data: T): ReactiveDeepD
|
|
|
204
250
|
return () => { effects.delete(effect) }
|
|
205
251
|
}
|
|
206
252
|
|
|
253
|
+
const $dispose = () => {
|
|
254
|
+
bridges.forEach(({ unsubscribe }) => unsubscribe())
|
|
255
|
+
bridges.clear()
|
|
256
|
+
}
|
|
257
|
+
|
|
207
258
|
storeApi.$on = $on
|
|
208
259
|
storeApi.$onAny = $onAny
|
|
209
260
|
storeApi.$effect = $effect
|
|
261
|
+
storeApi.$dispose = $dispose
|
|
210
262
|
|
|
211
263
|
return reactive
|
|
212
264
|
}
|