jq79 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -271
- package/assets/Component79.svg +8 -0
- package/dist/dom.d.ts +3 -0
- package/dist/jq79.cjs +9 -5
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +8 -17
- package/dist/jq79.global.js +9 -5
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +9 -5
- package/dist/jq79.js.map +1 -1
- package/dist/reactive.d.ts +20 -0
- package/dist/transform.d.ts +7 -0
- package/dist/vite.cjs +116 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +92 -0
- package/dist/vite.js.map +1 -0
- package/package.json +24 -4
- package/src/dom.ts +33 -0
- package/src/jq79.ts +95 -367
- package/src/reactive.ts +187 -0
- package/src/transform.ts +282 -0
- package/src/vite.ts +154 -0
package/dist/vite.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/vite.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
var COMPONENT_QUERY = "?jq79";
|
|
4
|
+
var SCRIPT_BLOCK_RE = /<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi;
|
|
5
|
+
var IMPORT_LITERAL_RE = /(?<![\w$.])import\s*\(\s*(["'])([^"'\n]+?)\1\s*\)/g;
|
|
6
|
+
var STATIC_IMPORT_LITERAL_RE = /(?<![\w$.])import\s*(?:[\w$\s,{}*]+?\s*from\s*)?(["'])([^"'\n]+)\1/g;
|
|
7
|
+
var isHtmlUrl = (spec) => /\.html?([?#]|$)/.test(spec);
|
|
8
|
+
var isExternalUrl = (spec) => /^[a-z][a-z0-9+.-]*:/i.test(spec) || spec.startsWith("/");
|
|
9
|
+
var hoistableImports = (source, include) => {
|
|
10
|
+
const specifiers = /* @__PURE__ */ new Set();
|
|
11
|
+
for (const [, script] of source.matchAll(SCRIPT_BLOCK_RE)) {
|
|
12
|
+
const specs = [
|
|
13
|
+
...[...script.matchAll(IMPORT_LITERAL_RE)].map((match) => match[2]),
|
|
14
|
+
...[...script.matchAll(STATIC_IMPORT_LITERAL_RE)].map((match) => match[2])
|
|
15
|
+
];
|
|
16
|
+
for (const spec of specs) {
|
|
17
|
+
if (isExternalUrl(spec)) continue;
|
|
18
|
+
if (isHtmlUrl(spec) && !include.test(spec)) continue;
|
|
19
|
+
specifiers.add(spec);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return [...specifiers];
|
|
23
|
+
};
|
|
24
|
+
var componentModule = (source, include) => {
|
|
25
|
+
const hoisted = hoistableImports(source, include);
|
|
26
|
+
const imports = hoisted.map(
|
|
27
|
+
(spec, i) => include.test(spec) ? `import __jq79_${i} from ${JSON.stringify(spec)}` : `import * as __jq79_${i} from ${JSON.stringify(spec)}`
|
|
28
|
+
).join("\n");
|
|
29
|
+
const modulesMap = `{ ${hoisted.map((spec, i) => `${JSON.stringify(spec)}: __jq79_${i}`).join(", ")} }`;
|
|
30
|
+
return `
|
|
31
|
+
import { Component79 } from "jq79"
|
|
32
|
+
${imports}
|
|
33
|
+
|
|
34
|
+
const src = ${JSON.stringify(source)}
|
|
35
|
+
const modules = ${modulesMap}
|
|
36
|
+
|
|
37
|
+
let component
|
|
38
|
+
|
|
39
|
+
if (import.meta.hot && import.meta.hot.data.component) {
|
|
40
|
+
const prior = import.meta.hot.data.component
|
|
41
|
+
const next = new Component79(src)
|
|
42
|
+
prior.template = next.template
|
|
43
|
+
prior.scripts = next.scripts
|
|
44
|
+
prior.styles = next.styles
|
|
45
|
+
prior.modules = modules
|
|
46
|
+
const root = prior.mountRoot
|
|
47
|
+
if (root) {
|
|
48
|
+
prior.mount(root, { ...prior.data })
|
|
49
|
+
} else if (!prior.data) {
|
|
50
|
+
import.meta.hot.invalidate()
|
|
51
|
+
}
|
|
52
|
+
component = prior
|
|
53
|
+
} else {
|
|
54
|
+
component = new Component79(src, { modules })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (import.meta.hot) {
|
|
58
|
+
import.meta.hot.data.component = component
|
|
59
|
+
import.meta.hot.accept()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default component
|
|
63
|
+
`;
|
|
64
|
+
};
|
|
65
|
+
function jq79(options = {}) {
|
|
66
|
+
const include = options.include ?? /\.html$/;
|
|
67
|
+
const { exclude } = options;
|
|
68
|
+
return {
|
|
69
|
+
name: "jq79",
|
|
70
|
+
enforce: "pre",
|
|
71
|
+
async resolveId(source, importer) {
|
|
72
|
+
if (!importer) return null;
|
|
73
|
+
if (source.includes("?")) return null;
|
|
74
|
+
if (!include.test(source)) return null;
|
|
75
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
76
|
+
if (!resolved || resolved.external) return null;
|
|
77
|
+
if (exclude?.test(resolved.id)) return null;
|
|
78
|
+
return resolved.id + COMPONENT_QUERY;
|
|
79
|
+
},
|
|
80
|
+
async load(id) {
|
|
81
|
+
if (!id.endsWith(COMPONENT_QUERY)) return null;
|
|
82
|
+
const file = id.slice(0, -COMPONENT_QUERY.length);
|
|
83
|
+
return { code: componentModule(await readFile(file, "utf8"), include), map: null };
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
var vite_default = jq79;
|
|
88
|
+
export {
|
|
89
|
+
vite_default as default,
|
|
90
|
+
jq79
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=vite.js.map
|
package/dist/vite.js.map
ADDED
|
@@ -0,0 +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 isRelative = (spec: string) => spec.startsWith(\"./\") || spec.startsWith(\"../\")\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;AAE/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":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jq79",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Mini reactive component library: single-file components, Svelte-style setup scripts, fine-grained proxy reactivity.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
7
7
|
"components",
|
|
@@ -30,25 +30,45 @@
|
|
|
30
30
|
"import": "./dist/jq79.js",
|
|
31
31
|
"require": "./dist/jq79.cjs",
|
|
32
32
|
"default": "./dist/jq79.js"
|
|
33
|
+
},
|
|
34
|
+
"./vite": {
|
|
35
|
+
"types": "./dist/vite.d.ts",
|
|
36
|
+
"import": "./dist/vite.js",
|
|
37
|
+
"require": "./dist/vite.cjs",
|
|
38
|
+
"default": "./dist/vite.js"
|
|
33
39
|
}
|
|
34
40
|
},
|
|
35
41
|
"unpkg": "./dist/jq79.global.js",
|
|
36
42
|
"jsdelivr": "./dist/jq79.global.js",
|
|
37
43
|
"files": [
|
|
38
44
|
"dist",
|
|
39
|
-
"src
|
|
45
|
+
"src",
|
|
46
|
+
"assets"
|
|
40
47
|
],
|
|
41
48
|
"sideEffects": false,
|
|
42
49
|
"scripts": {
|
|
43
|
-
"build": "tsup && tsc src/jq79.ts --declaration --emitDeclarationOnly --strict --lib dom,es2020 --target es2020 --module es2020 --moduleResolution bundler --outDir dist",
|
|
50
|
+
"build": "tsup && tsc src/jq79.ts src/vite.ts --declaration --emitDeclarationOnly --strict --skipLibCheck --lib dom,es2020 --target es2020 --module es2020 --moduleResolution bundler --outDir dist",
|
|
44
51
|
"test": "vitest run",
|
|
52
|
+
"test:coverage": "vitest run --coverage",
|
|
53
|
+
"site": "node scripts/build-site.mjs",
|
|
45
54
|
"prepublishOnly": "npm test && npm run build"
|
|
46
55
|
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"vite": ">=5.0.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"vite": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
47
64
|
"devDependencies": {
|
|
48
65
|
"@types/node": "^26.1.1",
|
|
66
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
49
67
|
"jsdom": "^29.1.1",
|
|
68
|
+
"marked": "^18.0.6",
|
|
50
69
|
"tsup": "^8.5.1",
|
|
51
70
|
"typescript": "^7.0.2",
|
|
71
|
+
"vite": "^8.1.4",
|
|
52
72
|
"vitest": "^4.1.10"
|
|
53
73
|
}
|
|
54
74
|
}
|
package/src/dom.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// DOM helpers: tiny query/create utilities, also injected into component
|
|
2
|
+
// scripts as $, $$ and $create
|
|
3
|
+
|
|
4
|
+
export const $ = (selectorOrEl: string | Element, selector?: string) =>
|
|
5
|
+
typeof selectorOrEl === "string"
|
|
6
|
+
? document.querySelector(selectorOrEl)
|
|
7
|
+
: selectorOrEl.querySelector(selector || "")
|
|
8
|
+
|
|
9
|
+
export const $$ = (selectorOrEl: string | Element, selector?: string) => Array.from(
|
|
10
|
+
typeof selectorOrEl === "string"
|
|
11
|
+
? document.querySelectorAll(selectorOrEl)
|
|
12
|
+
: selectorOrEl.querySelectorAll(selector || "")
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// $create(tag, attrs): attrs are set as attributes, except className, which
|
|
16
|
+
// may be a string or an array of class names.
|
|
17
|
+
export const $create = (tag: string, attrs: Record<string, any> = {}): HTMLElement => {
|
|
18
|
+
const el = document.createElement(tag);
|
|
19
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
20
|
+
if (name === 'className') {
|
|
21
|
+
el.className = Array.isArray(value) ? value.join(' ') : value;
|
|
22
|
+
} else if (name === 'textContent') {
|
|
23
|
+
el.textContent = value;
|
|
24
|
+
} else if (name === 'children') {
|
|
25
|
+
for (const child of value) {
|
|
26
|
+
el.appendChild(child);
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
el.setAttribute(name, value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return el;
|
|
33
|
+
};
|