jq79 0.7.2 → 0.7.4
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 +3 -2
- package/dev/vite.ts +152 -10
- package/dist/dom.d.ts +22 -4
- package/dist/html.d.ts +9 -0
- package/dist/jq79-sw.js +32 -0
- package/dist/jq79.cjs +18 -14
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +27 -13
- package/dist/jq79.global.js +18 -14
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +18 -14
- package/dist/jq79.js.map +1 -1
- package/dist/precompile.cjs +24 -0
- package/dist/precompile.cjs.map +1 -0
- package/dist/precompile.d.ts +3 -0
- package/dist/precompile.js +24 -0
- package/dist/precompile.js.map +1 -0
- package/dist/reactive.d.ts +10 -0
- package/dist/source.d.ts +65 -0
- package/dist/vite.cjs +85 -4
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.d.ts +3 -0
- package/dist/vite.js +86 -5
- package/dist/vite.js.map +1 -1
- package/package.json +12 -2
- package/src/dom.ts +28 -4
- package/src/html.ts +289 -0
- package/src/jq79.ts +705 -363
- package/src/precompile.ts +238 -0
- package/src/reactive.ts +178 -9
- package/src/source.ts +389 -0
- package/src/sw.ts +87 -0
- package/src/transform.ts +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ npm install jq79
|
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
```js
|
|
52
|
-
import { Component79, C79, $, $$, $reactive, $toRaw, parseComponent } from "jq79"
|
|
52
|
+
import { Component79, C79, $, $$, $reactive, $computed, $toRaw, parseComponent } from "jq79"
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
### Vite
|
|
@@ -175,9 +175,10 @@ When the fetch resolves, the assignments to `firstName`/`lastName` re-run the `$
|
|
|
175
175
|
- [Components](docs/components.md) — lifecycle (`mount`, `mountShadow`, `detach`, `destroy`), instance events (`on`/`off`), `<style scoped>`, several components in one file with `<template name>`, loading remote components with `Component79.fetch` (chainable — fetch and mount in one expression) and `fetchAll`, and `Component79.version`.
|
|
176
176
|
- [Template syntax](docs/template-syntax.md) — `{{ }}` interpolation, `:name` attribute bindings, `:text`/`:html`, `:if`/`:elseif`/`:else`, `:each`/`:key`, `:with`, `@event` listeners and modifiers, nested components.
|
|
177
177
|
- [Setup scripts](docs/setup-scripts.md) — `<script :setup>` reactive scripts, `$:` declarations, `$emit`, `await $mounted()`, `$self`/`$$self`, and `export default` factory scripts (plain-JS alternative).
|
|
178
|
-
- [Reactive data](docs/reactive-data.md) — the standalone `$reactive` store: `$on`, `$onAny`, `$effect
|
|
178
|
+
- [Reactive data](docs/reactive-data.md) — the standalone `$reactive` store: `$on`, `$onAny`, `$effect`, and `$computed` for derived values.
|
|
179
179
|
- [DOM helpers](docs/dom-helpers.md) — `$`, `$$` and `$create`.
|
|
180
180
|
- [Vite plugin](docs/vite-plugin.md) — importing `.html` components as bundled modules, HMR, options.
|
|
181
|
+
- [Content Security Policy](docs/csp.md) — `safeEval`: running without `'unsafe-eval'`, with Vite or with no bundler at all.
|
|
181
182
|
- [Dev server](docs/dev-server.md) — `npx jq79 dev`: serve and hot-reload components with no build step.
|
|
182
183
|
- [Development](docs/development.md) — running tests, building, publishing releases.
|
|
183
184
|
|
package/dev/vite.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises"
|
|
2
|
-
import { relative } from "node:path"
|
|
2
|
+
import { basename, relative } from "node:path"
|
|
3
3
|
import { preprocessCSS, transformWithEsbuild } from "vite"
|
|
4
4
|
import type { Plugin, ResolvedConfig } from "vite"
|
|
5
|
+
// by package name rather than ../src: this file's types are emitted with dev/
|
|
6
|
+
// as their root, and the generator is the runtime's own code - resolved through
|
|
7
|
+
// the package's exports (external in the build, aliased to src in the tests)
|
|
8
|
+
import { precompile, precompiledScript } from "jq79/precompile"
|
|
5
9
|
|
|
6
10
|
// Vite plugin: import .html single-file components as modules.
|
|
7
11
|
//
|
|
@@ -15,7 +19,7 @@ import type { Plugin, ResolvedConfig } from "vite"
|
|
|
15
19
|
// with fetch instead - with one deliberate exception, a block that says it is
|
|
16
20
|
// written in something else: <style lang="scss"> (or less/stylus/sass) is
|
|
17
21
|
// compiled to plain CSS here, and a TypeScript script - <script lang="ts">, or
|
|
18
|
-
// the <script type="text/typescript">
|
|
22
|
+
// the same mark spelled <script type="text/typescript"> - to plain JS. Such a
|
|
19
23
|
// component only works through the bundler; loaded with fetch() it would reach
|
|
20
24
|
// the runtime uncompiled, which the runtime warns about.
|
|
21
25
|
//
|
|
@@ -28,12 +32,105 @@ export interface Jq79PluginOptions {
|
|
|
28
32
|
include?: RegExp
|
|
29
33
|
// resolved absolute paths to skip even when `include` matches
|
|
30
34
|
exclude?: RegExp
|
|
35
|
+
// run the app without eval, for a CSP with no 'unsafe-eval' - the value
|
|
36
|
+
// Component79.safeEval() takes, and meaning the same thing
|
|
37
|
+
// (RECORD/2026-09-23.no-unsafe-eval.md). Every component is precompiled, and
|
|
38
|
+
// its functions ship as a script of their own. `true`: those and nothing
|
|
39
|
+
// else - whatever the build couldn't see is reported, never evaluated.
|
|
40
|
+
// `{ nonce: true }`: whatever it couldn't see (a component built from a
|
|
41
|
+
// string, an edit during HMR) is built as a <script> carrying the page's nonce
|
|
42
|
+
safeEval?: boolean | { nonce?: boolean }
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
// claimed modules get this suffix so their id no longer ends in ".html" and
|
|
34
46
|
// Vite's own html handling (entries, asset pipeline) leaves them alone
|
|
35
47
|
const COMPONENT_QUERY = "?jq79"
|
|
36
48
|
|
|
49
|
+
// safeEval. A component's precompiled functions travel as a classic script of
|
|
50
|
+
// their own - Card.html.jq79.js - and not inside its module: they compile
|
|
51
|
+
// under `with`, which is a SyntaxError in strict code, and every module is
|
|
52
|
+
// strict. In a build the script is an asset beside the chunk; in dev the
|
|
53
|
+
// server hands it out (PRECOMPILED_PATH). Either way it pushes its functions
|
|
54
|
+
// onto the queue the runtime drains (see "safe eval" in src/jq79.ts).
|
|
55
|
+
//
|
|
56
|
+
// The component's module waits for it - a top-level await - before it builds
|
|
57
|
+
// anything, through a module every component module imports. That module
|
|
58
|
+
// evaluates once, so safe mode is turned on once per app, and a page with no
|
|
59
|
+
// nonce is reported once rather than once per component; and it loads each
|
|
60
|
+
// script once, however many modules ask. The <script> carries the page's
|
|
61
|
+
// nonce when there is one, so a CSP that works by nonce admits it; under
|
|
62
|
+
// `script-src 'self'` the URL is what admits it.
|
|
63
|
+
//
|
|
64
|
+
// Component79 is passed in rather than imported, so the module resolves
|
|
65
|
+
// nothing: a bare "jq79" from a virtual importer is the one resolution that
|
|
66
|
+
// would depend on how the app is laid out.
|
|
67
|
+
//
|
|
68
|
+
// A component built from a string *before* any .html module is imported runs
|
|
69
|
+
// in the default mode; such an app calls Component79.safeEval itself. The
|
|
70
|
+
// await needs a build target with top-level await in it - Vite 7 and later
|
|
71
|
+
// have one by default; on Vite 5 or 6, raise build.target to es2022
|
|
72
|
+
const SAFE_EVAL_ID = "virtual:jq79/safe-eval"
|
|
73
|
+
const RESOLVED_SAFE_EVAL_ID = `\0${SAFE_EVAL_ID}`
|
|
74
|
+
const safeEvalModule = (options: { nonce?: boolean; worker: false }) => `
|
|
75
|
+
let started = null
|
|
76
|
+
const loading = new Map()
|
|
77
|
+
const pageNonce = () => {
|
|
78
|
+
for (const script of document.querySelectorAll("script[nonce]")) {
|
|
79
|
+
const nonce = script.nonce || script.getAttribute("nonce")
|
|
80
|
+
if (nonce) return nonce
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export const safeEval = (Component79, url) => {
|
|
84
|
+
if (!started) started = Component79.safeEval(${JSON.stringify(options)}).catch(error => console.error(error))
|
|
85
|
+
let loaded = loading.get(url)
|
|
86
|
+
if (!loaded) {
|
|
87
|
+
loaded = new Promise((resolve, reject) => {
|
|
88
|
+
const script = document.createElement("script")
|
|
89
|
+
const nonce = pageNonce()
|
|
90
|
+
if (nonce) script.setAttribute("nonce", nonce)
|
|
91
|
+
script.src = url
|
|
92
|
+
script.onload = () => { script.remove(); resolve() }
|
|
93
|
+
script.onerror = () => {
|
|
94
|
+
script.remove()
|
|
95
|
+
reject(new Error("jq79: the precompiled functions at " + url + " did not load, and safeEval can't render without them"))
|
|
96
|
+
}
|
|
97
|
+
document.head.append(script)
|
|
98
|
+
})
|
|
99
|
+
loading.set(url, loaded)
|
|
100
|
+
}
|
|
101
|
+
return loaded
|
|
102
|
+
}
|
|
103
|
+
`
|
|
104
|
+
|
|
105
|
+
// where the dev server hands out a component's precompiled script
|
|
106
|
+
const PRECOMPILED_PATH = "/@jq79/precompiled.js"
|
|
107
|
+
|
|
108
|
+
// the plugin's safeEval option, as what the modules pass the runtime: null
|
|
109
|
+
// for off, or Component79.safeEval's own options
|
|
110
|
+
const safeEvalOptions = (option: Jq79PluginOptions["safeEval"]): { nonce?: boolean } | null => {
|
|
111
|
+
if (option === undefined || option === false) return null
|
|
112
|
+
return typeof option === "object" && option !== null && option.nonce === true ? { nonce: true } : {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// a scoped-form body: the runtime falls back to the `with` form when one
|
|
116
|
+
// doesn't compile, so only the `with` form's failure is worth a word
|
|
117
|
+
const SCOPED_BODY_RE = /^(?:let \$t;| return \()/
|
|
118
|
+
|
|
119
|
+
// a component's precompiled functions, as the classic script that registers
|
|
120
|
+
// them (precompiledScript, in jq79/precompile). Each is compiled here first -
|
|
121
|
+
// node can, where the page may not - so the script holds only functions that
|
|
122
|
+
// parse; one that doesn't ships as null, and the build says so
|
|
123
|
+
const componentScript = (source: string, warn: (message: string) => void): string =>
|
|
124
|
+
precompiledScript(precompile(source), (params, body) => {
|
|
125
|
+
try {
|
|
126
|
+
new Function(...params, body)
|
|
127
|
+
return true
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (!SCOPED_BODY_RE.test(body)) warn(`${(error as Error).message}, in: ${body.length > 120 ? `${body.slice(0, 120)}…` : body}`)
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
37
134
|
// a <script> block with its attribute string, so `lang` can be read and the
|
|
38
135
|
// body replaced - the same shape as STYLE_BLOCK_RE below, quote-aware so a
|
|
39
136
|
// ">" inside an attribute value (`:setup="{ n = a > 1 }"`) doesn't end the tag
|
|
@@ -201,10 +298,12 @@ const compileStyleBlocks = async (
|
|
|
201
298
|
// languages a <script lang> is compiled from. Anything else is left as written
|
|
202
299
|
// for the runtime to warn about, rather than guessed at
|
|
203
300
|
const TS_LANGS = new Set(["ts", "typescript"])
|
|
204
|
-
// the other spelling of the same mark
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
301
|
+
// the other spelling of the same mark. A component is a plain .html file, not
|
|
302
|
+
// an SFC, so nothing in an editor knows what `lang` means there, and each mark
|
|
303
|
+
// gets half of one: VS Code's HTML language service checks a `type` of
|
|
304
|
+
// text/typescript as TypeScript (and `lang="ts"` as JavaScript), while its
|
|
305
|
+
// grammar colors that `type` not at all (and `lang="ts"` as JavaScript). See
|
|
306
|
+
// docs/vite-plugin.md#which-mark-in-vs-code. The `x-` forms are the historical
|
|
208
307
|
// spelling of the same two media types
|
|
209
308
|
const TS_TYPE_RE = /^(?:text|application)\/(?:x-)?typescript$/
|
|
210
309
|
|
|
@@ -350,7 +449,9 @@ const compileScriptBlocks = async (source: string, file: string): Promise<string
|
|
|
350
449
|
// component's markers. An instance only used as a definition has nothing to
|
|
351
450
|
// re-render (nested clones can't be reached from this module), so it falls
|
|
352
451
|
// back to a full reload.
|
|
353
|
-
|
|
452
|
+
// `precompiled` is the precompiled script's URL, as a JS expression - present
|
|
453
|
+
// only in safeEval mode
|
|
454
|
+
const componentModule = (source: string, include: RegExp, filename: string, precompiled?: string): string => {
|
|
354
455
|
const hoisted = hoistableImports(source, include)
|
|
355
456
|
const imports = hoisted
|
|
356
457
|
.map((spec, i) =>
|
|
@@ -361,9 +462,13 @@ const componentModule = (source: string, include: RegExp, filename: string): str
|
|
|
361
462
|
.join("\n")
|
|
362
463
|
const modulesMap = `{ ${hoisted.map((spec, i) => `${JSON.stringify(spec)}: __jq79_${i}`).join(", ")} }`
|
|
363
464
|
|
|
465
|
+
// without safeEval, the module is exactly what it always was
|
|
466
|
+
const safeEvalImport = precompiled ? `\nimport { safeEval } from "${SAFE_EVAL_ID}"` : ""
|
|
467
|
+
const safeEvalCall = precompiled ? `\nawait safeEval(Component79, ${precompiled})` : ""
|
|
468
|
+
|
|
364
469
|
return `
|
|
365
|
-
import { Component79 } from "jq79"
|
|
366
|
-
${imports}
|
|
470
|
+
import { Component79 } from "jq79"${safeEvalImport}
|
|
471
|
+
${imports}${safeEvalCall}
|
|
367
472
|
|
|
368
473
|
const src = ${JSON.stringify(source)}
|
|
369
474
|
const modules = ${modulesMap}
|
|
@@ -397,6 +502,11 @@ ${declaredComponents(source).map(name => `export const ${name} = component.${nam
|
|
|
397
502
|
export function jq79(options: Jq79PluginOptions = {}): Plugin {
|
|
398
503
|
const include = options.include ?? /\.html$/
|
|
399
504
|
const { exclude } = options
|
|
505
|
+
const safeEval = safeEvalOptions(options.safeEval)
|
|
506
|
+
// dev only: each component's precompiled script, by the id its module asks
|
|
507
|
+
// for - so the server hands out what the plugin compiled and nothing else
|
|
508
|
+
const devScripts = new Map<string, string>()
|
|
509
|
+
const devIds = new Map<string, number>()
|
|
400
510
|
|
|
401
511
|
let config: ResolvedConfig | null = null
|
|
402
512
|
|
|
@@ -408,7 +518,21 @@ export function jq79(options: Jq79PluginOptions = {}): Plugin {
|
|
|
408
518
|
config = resolved
|
|
409
519
|
},
|
|
410
520
|
|
|
521
|
+
configureServer(server) {
|
|
522
|
+
if (!safeEval) return
|
|
523
|
+
server.middlewares.use((req, res, next) => {
|
|
524
|
+
const url = new URL(req.url ?? "/", "http://localhost")
|
|
525
|
+
if (!url.pathname.endsWith(PRECOMPILED_PATH)) return next()
|
|
526
|
+
const script = devScripts.get(url.searchParams.get("id") ?? "")
|
|
527
|
+
if (script === undefined) return next()
|
|
528
|
+
res.setHeader("Content-Type", "text/javascript")
|
|
529
|
+
res.setHeader("Cache-Control", "no-cache")
|
|
530
|
+
res.end(script)
|
|
531
|
+
})
|
|
532
|
+
},
|
|
533
|
+
|
|
411
534
|
async resolveId(source, importer) {
|
|
535
|
+
if (source === SAFE_EVAL_ID) return RESOLVED_SAFE_EVAL_ID
|
|
412
536
|
if (!importer) return null // entry points are never components
|
|
413
537
|
if (source.includes("?")) return null // ?raw, ?url, ... keep their meaning
|
|
414
538
|
if (!include.test(source)) return null
|
|
@@ -420,6 +544,8 @@ export function jq79(options: Jq79PluginOptions = {}): Plugin {
|
|
|
420
544
|
},
|
|
421
545
|
|
|
422
546
|
async load(id) {
|
|
547
|
+
// a Vite page's functions come from the build, so it never registers the worker
|
|
548
|
+
if (id === RESOLVED_SAFE_EVAL_ID) return safeEvalModule({ ...safeEval, worker: false })
|
|
423
549
|
if (!id.endsWith(COMPONENT_QUERY)) return null
|
|
424
550
|
const file = id.slice(0, -COMPONENT_QUERY.length)
|
|
425
551
|
|
|
@@ -431,7 +557,23 @@ export function jq79(options: Jq79PluginOptions = {}): Plugin {
|
|
|
431
557
|
// shows a path the user recognizes instead of an anonymous VM script
|
|
432
558
|
const filename = config ? relative(config.root, file) : file
|
|
433
559
|
|
|
434
|
-
return { code: componentModule(source, include, filename), map: null }
|
|
560
|
+
if (!safeEval) return { code: componentModule(source, include, filename), map: null }
|
|
561
|
+
|
|
562
|
+
// the component's functions, compiled after its TypeScript and styles are:
|
|
563
|
+
// the runtime compiles what reaches it, and this is what reaches it
|
|
564
|
+
const script = componentScript(source, message => this.warn(`jq79: ${filename}: ${message}`))
|
|
565
|
+
let url: string
|
|
566
|
+
if (config?.command === "serve") {
|
|
567
|
+
let devId = devIds.get(file)
|
|
568
|
+
if (devId === undefined) devIds.set(file, devId = devIds.size)
|
|
569
|
+
devScripts.set(String(devId), script)
|
|
570
|
+
// versioned, so an edit loads the new functions rather than a cached copy
|
|
571
|
+
url = JSON.stringify(`${config.base}${PRECOMPILED_PATH.slice(1)}?id=${devId}&v=${Date.now()}`)
|
|
572
|
+
} else {
|
|
573
|
+
const ref = this.emitFile({ type: "asset", name: `${basename(file)}.jq79.js`, source: script })
|
|
574
|
+
url = `import.meta.ROLLUP_FILE_URL_${ref}`
|
|
575
|
+
}
|
|
576
|
+
return { code: componentModule(source, include, filename, url), map: null }
|
|
435
577
|
},
|
|
436
578
|
}
|
|
437
579
|
}
|
package/dist/dom.d.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export type QueryOne = {
|
|
2
|
+
<K extends keyof HTMLElementTagNameMap>(selector: K): HTMLElementTagNameMap[K] | null;
|
|
3
|
+
<K extends keyof SVGElementTagNameMap>(selector: K): SVGElementTagNameMap[K] | null;
|
|
4
|
+
<E extends Element = HTMLElement>(selector: string): E | null;
|
|
5
|
+
};
|
|
6
|
+
export type QueryAll = {
|
|
7
|
+
<K extends keyof HTMLElementTagNameMap>(selector: K): HTMLElementTagNameMap[K][];
|
|
8
|
+
<K extends keyof SVGElementTagNameMap>(selector: K): SVGElementTagNameMap[K][];
|
|
9
|
+
<E extends Element = HTMLElement>(selector: string): E[];
|
|
10
|
+
};
|
|
11
|
+
export declare function $<K extends keyof HTMLElementTagNameMap>(selector: K): HTMLElementTagNameMap[K] | null;
|
|
12
|
+
export declare function $<K extends keyof SVGElementTagNameMap>(selector: K): SVGElementTagNameMap[K] | null;
|
|
13
|
+
export declare function $<E extends Element = HTMLElement>(selector: string): E | null;
|
|
14
|
+
export declare function $<K extends keyof HTMLElementTagNameMap>(el: Element, selector: K): HTMLElementTagNameMap[K] | null;
|
|
15
|
+
export declare function $<K extends keyof SVGElementTagNameMap>(el: Element, selector: K): SVGElementTagNameMap[K] | null;
|
|
16
|
+
export declare function $<E extends Element = HTMLElement>(el: Element, selector: string): E | null;
|
|
17
|
+
export declare function $$<K extends keyof HTMLElementTagNameMap>(selector: K): HTMLElementTagNameMap[K][];
|
|
18
|
+
export declare function $$<K extends keyof SVGElementTagNameMap>(selector: K): SVGElementTagNameMap[K][];
|
|
19
|
+
export declare function $$<E extends Element = HTMLElement>(selector: string): E[];
|
|
20
|
+
export declare function $$<K extends keyof HTMLElementTagNameMap>(el: Element, selector: K): HTMLElementTagNameMap[K][];
|
|
21
|
+
export declare function $$<K extends keyof SVGElementTagNameMap>(el: Element, selector: K): SVGElementTagNameMap[K][];
|
|
22
|
+
export declare function $$<E extends Element = HTMLElement>(el: Element, selector: string): E[];
|
|
5
23
|
export declare const $create: (tag: string, attrs?: Record<string, any>) => HTMLElement;
|
|
6
24
|
export declare function isSafeUrl(value: string): boolean;
|
|
7
25
|
export type AllowUrl = (url: URL, tag: string, attr: string) => boolean;
|
package/dist/html.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type HTMLElementNode = {
|
|
2
|
+
tag: string;
|
|
3
|
+
attrs: Record<string, string>;
|
|
4
|
+
children: HTMLNode[];
|
|
5
|
+
};
|
|
6
|
+
export type HTMLNode = HTMLElementNode | string;
|
|
7
|
+
export declare const CHARACTER_REFERENCE_RE: RegExp;
|
|
8
|
+
export declare const decodeReference: (text: string, whole: string, ref: string, offset: number, inAttribute: boolean) => string;
|
|
9
|
+
export declare const parseHTML: (input: string) => HTMLNode[];
|