@barefootjs/jsx 0.30.6 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/interface.d.ts +61 -20
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/analyzer.d.ts +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/index.d.ts +0 -120
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +322 -263
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/generate-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/prune-unused-prop-extractions.d.ts +7 -0
- package/dist/ir-to-client-js/prune-unused-prop-extractions.d.ts.map +1 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/value-references.d.ts +7 -7
- package/dist/value-references.d.ts.map +1 -1
- package/package.json +3 -7
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +183 -312
- package/src/__tests__/bf050-single-multi-symmetry.test.ts +118 -0
- package/src/__tests__/client-js-generation.test.ts +5 -1
- package/src/__tests__/doc-examples.test.ts +5 -1
- package/src/__tests__/prune-unused-prop-extractions.test.ts +78 -0
- package/src/adapters/interface.ts +61 -20
- package/src/analyzer.ts +16 -5
- package/src/compiler.ts +41 -3
- package/src/index.ts +0 -123
- package/src/ir-to-client-js/emit-registration.ts +9 -0
- package/src/ir-to-client-js/generate-init.ts +4 -1
- package/src/ir-to-client-js/index.ts +5 -1
- package/src/ir-to-client-js/prune-unused-prop-extractions.ts +108 -0
- package/src/types.ts +25 -0
- package/src/value-references.ts +7 -7
- package/dist/import-map.d.ts +0 -56
- package/dist/import-map.d.ts.map +0 -1
- package/dist/import-map.js +0 -18
- package/src/__tests__/import-map.test.ts +0 -75
- package/src/import-map.ts +0 -72
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Final-pass removal of prop-extraction consts the emitted init body never
|
|
3
|
+
* reads (`const children = _p.children` and friends).
|
|
4
|
+
*
|
|
5
|
+
* `emitPropsExtraction` mirrors the component's destructuring for every
|
|
6
|
+
* prop the REFERENCE GRAPH marks as used — but the graph also counts
|
|
7
|
+
* template-only references (e.g. `{children}` rendered as SSR-adopted
|
|
8
|
+
* content), which the init body never touches. The stray binding is not
|
|
9
|
+
* just dead weight: props arrive as GETTERS over the parent's reactive
|
|
10
|
+
* state, and a slot-children getter (`get children() { return
|
|
11
|
+
* [createComponent('Checkbox', …)] }`) INSTANTIATES child components when
|
|
12
|
+
* read. An init that eagerly evaluates it creates a second, duplicate
|
|
13
|
+
* child instance next to the `upsertChild` wiring the compiler also
|
|
14
|
+
* emits — double event listeners, toggles that cancel themselves out.
|
|
15
|
+
* The legacy site pipeline masked this by registration order (children
|
|
16
|
+
* modules registered after the parent's init queued, so the queued init
|
|
17
|
+
* ran against an already-hydrated tree); Vite's ESM import order
|
|
18
|
+
* registers children first and made the eager read bite (#2537's
|
|
19
|
+
* migration surfaced it on site/ui's form-builder).
|
|
20
|
+
*
|
|
21
|
+
* Mirrors `resolveFinalImports`'s shape: a TS AST walk over the finished
|
|
22
|
+
* code (never a regex — see CLAUDE.md), span-based splicing, iterated to
|
|
23
|
+
* a fixpoint so an extraction referenced only by another pruned
|
|
24
|
+
* extraction's default expression is removed too.
|
|
25
|
+
*
|
|
26
|
+
* Known limits, both erring toward FALSE-KEEP (an extraction survives and
|
|
27
|
+
* the eager getter read stays), never false-prune:
|
|
28
|
+
* - The reference scan is whole-file and scope-unaware: any other
|
|
29
|
+
* occurrence of the same identifier text as a value (a module-level
|
|
30
|
+
* helper's parameter named `children`, a local in an unrelated closure)
|
|
31
|
+
* keeps the extraction alive.
|
|
32
|
+
* - Splicing removes a statement's full leading trivia, so a comment an
|
|
33
|
+
* emitter attached directly above a pruned extraction goes with it.
|
|
34
|
+
*/
|
|
35
|
+
import ts from 'typescript'
|
|
36
|
+
import { collectValueReferencedNames } from '../value-references.ts'
|
|
37
|
+
import { PROPS_PARAM } from './utils.ts'
|
|
38
|
+
|
|
39
|
+
/** Is `stmt` a single-declarator `const X = _p.X…` prop extraction whose
|
|
40
|
+
* initializer is `_p.X` or `_p.X ?? <default>`? Returns the bound name, or
|
|
41
|
+
* null when the statement is anything else. */
|
|
42
|
+
function propExtractionName(stmt: ts.Statement): string | null {
|
|
43
|
+
if (!ts.isVariableStatement(stmt)) return null
|
|
44
|
+
const decls = stmt.declarationList.declarations
|
|
45
|
+
if (decls.length !== 1) return null
|
|
46
|
+
const decl = decls[0]!
|
|
47
|
+
if (!ts.isIdentifier(decl.name) || !decl.initializer) return null
|
|
48
|
+
|
|
49
|
+
let core: ts.Expression = decl.initializer
|
|
50
|
+
if (
|
|
51
|
+
ts.isBinaryExpression(core) &&
|
|
52
|
+
core.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken
|
|
53
|
+
) {
|
|
54
|
+
core = core.left
|
|
55
|
+
}
|
|
56
|
+
if (!ts.isPropertyAccessExpression(core)) return null
|
|
57
|
+
if (!ts.isIdentifier(core.expression) || core.expression.text !== PROPS_PARAM) return null
|
|
58
|
+
if (core.name.text !== decl.name.text) return null
|
|
59
|
+
return decl.name.text
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Remove prop-extraction consts inside `init*` functions whose bound name
|
|
64
|
+
* is never referenced as a value anywhere else in `code`. Returns the code
|
|
65
|
+
* unchanged when nothing is prunable or the code doesn't parse.
|
|
66
|
+
*/
|
|
67
|
+
export function pruneUnusedPropExtractions(code: string): string {
|
|
68
|
+
for (let round = 0; round < 10; round++) {
|
|
69
|
+
const referenced = collectValueReferencedNames(code)
|
|
70
|
+
if (referenced === null) {
|
|
71
|
+
// Unlike resolveFinalImports' identical fallback (where a missed
|
|
72
|
+
// import fails the build loudly), skipping here silently re-opens
|
|
73
|
+
// the eager-getter double-instantiation this pass exists to prevent
|
|
74
|
+
// — so say so.
|
|
75
|
+
console.warn('[barefootjs] pruneUnusedPropExtractions: generated code did not parse; skipping prune')
|
|
76
|
+
return code
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const sourceFile = ts.createSourceFile(
|
|
80
|
+
'generated.js',
|
|
81
|
+
code,
|
|
82
|
+
ts.ScriptTarget.Latest,
|
|
83
|
+
/*setParentNodes*/ false,
|
|
84
|
+
ts.ScriptKind.JS,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// Spans to delete, gathered per round; spliced back-to-front so
|
|
88
|
+
// earlier offsets stay valid.
|
|
89
|
+
const spans: Array<{ start: number; end: number }> = []
|
|
90
|
+
for (const stmt of sourceFile.statements) {
|
|
91
|
+
if (!ts.isFunctionDeclaration(stmt) || !stmt.name?.text.startsWith('init') || !stmt.body) continue
|
|
92
|
+
for (const inner of stmt.body.statements) {
|
|
93
|
+
const name = propExtractionName(inner)
|
|
94
|
+
if (name !== null && !referenced.has(name)) {
|
|
95
|
+
spans.push({ start: inner.getFullStart(), end: inner.getEnd() })
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (spans.length === 0) return code
|
|
101
|
+
let next = code
|
|
102
|
+
for (const { start, end } of spans.sort((a, b) => b.start - a.start)) {
|
|
103
|
+
next = next.slice(0, start) + next.slice(end)
|
|
104
|
+
}
|
|
105
|
+
code = next
|
|
106
|
+
}
|
|
107
|
+
return code
|
|
108
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -2225,6 +2225,31 @@ export interface CompileOptions {
|
|
|
2225
2225
|
* byte-for-byte unchanged (SR8). Dev/profiling builds only.
|
|
2226
2226
|
*/
|
|
2227
2227
|
profile?: boolean
|
|
2228
|
+
/**
|
|
2229
|
+
* Forwarded verbatim to `adapter.generate(..., { scriptAssets })` — see
|
|
2230
|
+
* that field's docstring on `AdapterGenerateOptions` for the full
|
|
2231
|
+
* precedence rules (`skipScriptRegistration` still wins; `[]` means "no
|
|
2232
|
+
* scripts", distinct from `undefined`'s "use the adapter-computed path").
|
|
2233
|
+
*
|
|
2234
|
+
* This is plain resolved data (an ordered URL list), not a rewrite
|
|
2235
|
+
* callback — the caller (chiefly `@barefootjs/vite`) has already done all
|
|
2236
|
+
* the resolution (bundling, hashing, manifest lookup) before calling
|
|
2237
|
+
* `compileJSX`; the compiler only threads the list through to the
|
|
2238
|
+
* adapter unchanged.
|
|
2239
|
+
*/
|
|
2240
|
+
scriptAssets?: string[]
|
|
2241
|
+
/**
|
|
2242
|
+
* Forwarded verbatim to `adapter.generate(..., { preloadAssets })` — see
|
|
2243
|
+
* that field's docstring on `AdapterGenerateOptions` for the full
|
|
2244
|
+
* precedence rules (`skipScriptRegistration` still wins; `[]` means
|
|
2245
|
+
* "resolved, nothing to preload", distinct from `undefined`'s "no
|
|
2246
|
+
* preload information").
|
|
2247
|
+
*
|
|
2248
|
+
* Same plain-resolved-data contract as `scriptAssets`: the caller
|
|
2249
|
+
* (`@barefootjs/vite`) has already walked the manifest; the compiler
|
|
2250
|
+
* only threads the list through unchanged.
|
|
2251
|
+
*/
|
|
2252
|
+
preloadAssets?: string[]
|
|
2228
2253
|
}
|
|
2229
2254
|
|
|
2230
2255
|
export interface FileOutput {
|
package/src/value-references.ts
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* `return { … }` with no binding — `ReferenceError: Theme is not defined`
|
|
12
12
|
* at load, killing the whole page's client JS.
|
|
13
13
|
*
|
|
14
|
-
* `packages/cli`'s `detectStrippedReferences` (in
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* `packages/cli`'s `detectStrippedReferences` (in the since-deleted
|
|
15
|
+
* `resolve-imports.ts`) used to share this classifier for its own
|
|
16
|
+
* dangling-reference scan; `collectExternalImports` is the remaining
|
|
17
|
+
* caller.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import ts from 'typescript'
|
|
@@ -27,10 +28,9 @@ import ts from 'typescript'
|
|
|
27
28
|
* reference — it reads the binding, it doesn't just spell its name.
|
|
28
29
|
*
|
|
29
30
|
* CONTRACT: this classifies identifier positions in **JavaScript** source.
|
|
30
|
-
*
|
|
31
|
-
* `collectValueReferencedNames` below
|
|
32
|
-
*
|
|
33
|
-
* bundle. TypeScript-only positions are deliberately NOT handled: an
|
|
31
|
+
* The current caller parses with `ts.ScriptKind.JS` —
|
|
32
|
+
* `collectValueReferencedNames` below.
|
|
33
|
+
* TypeScript-only positions are deliberately NOT handled: an
|
|
34
34
|
* identifier in a type position (`const x: Foo = …`, a
|
|
35
35
|
* `TypeReferenceNode`) is still reported as a value reference, and so are
|
|
36
36
|
* `interface` / `type` / `enum` declaration names. Do not point this at
|
package/dist/import-map.d.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared importmap-snippet renderer.
|
|
3
|
-
*
|
|
4
|
-
* `bf build` emits `barefoot-externals.json` (the `ExternalsManifest`) whenever
|
|
5
|
-
* `externals` / `bundleEntries` are configured. Component adapters (Hono) read
|
|
6
|
-
* that manifest at render time via a JSX component (`BfImportMap`). Template-
|
|
7
|
-
* string adapters (Go html/template, Mojolicious EP) have no component layer, so
|
|
8
|
-
* `bf build` instead emits a static `barefoot-importmap.html` for them to
|
|
9
|
-
* `{{ template }}` / `%= include` into the page `<head>`.
|
|
10
|
-
*
|
|
11
|
-
* This module is the single source of truth for that snippet's HTML, so every
|
|
12
|
-
* adapter's importmap injection point stays in sync. See issue #1644.
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
* The subset of `barefoot-externals.json` needed to render the importmap
|
|
16
|
-
* snippet. All fields are optional so a partial or hand-written manifest
|
|
17
|
-
* (e.g. a Hono `BfImportMap` `externals` prop) still type-checks. The strict
|
|
18
|
-
* build-output `ExternalsManifest` is structurally assignable to this, so both
|
|
19
|
-
* the CLI's emitted manifest and a hand-written one feed `renderImportMapHtml`.
|
|
20
|
-
* This is the one shared manifest type for every importmap injection path.
|
|
21
|
-
*/
|
|
22
|
-
export interface ImportMapManifest {
|
|
23
|
-
/** Entries for `<script type="importmap">`. */
|
|
24
|
-
importmap?: {
|
|
25
|
-
imports?: Record<string, string>;
|
|
26
|
-
};
|
|
27
|
-
/** URLs to emit as `<link rel="modulepreload">`. */
|
|
28
|
-
preloads?: string[];
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Shape of `barefoot-externals.json`, written by `bf build`. This is the build
|
|
32
|
-
* output contract shared by the CLI (which writes it) and the adapters (which
|
|
33
|
-
* consume it) — the all-required superset of {@link ImportMapManifest}.
|
|
34
|
-
*/
|
|
35
|
-
export interface ExternalsManifest {
|
|
36
|
-
/** Entries for `<script type="importmap">`. */
|
|
37
|
-
importmap: {
|
|
38
|
-
imports: Record<string, string>;
|
|
39
|
-
};
|
|
40
|
-
/** URLs to emit as `<link rel="modulepreload">`. */
|
|
41
|
-
preloads: string[];
|
|
42
|
-
/** Package names to pass as `--external` to the user's bundler. */
|
|
43
|
-
externals: string[];
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Render the `<script type="importmap">` (plus `<link rel="modulepreload">`)
|
|
47
|
-
* snippet from a parsed externals manifest. Fields are read defensively so a
|
|
48
|
-
* partial or hand-written manifest still produces valid output.
|
|
49
|
-
*
|
|
50
|
-
* Inside the importmap JSON, each `<` is replaced with its JSON unicode escape
|
|
51
|
-
* for code point U+003C so a URL containing `</script>` cannot break out of the
|
|
52
|
-
* script element — the JSON parser decodes that escape back to `<`, keeping the
|
|
53
|
-
* mapping value-identical.
|
|
54
|
-
*/
|
|
55
|
-
export declare function renderImportMapHtml(manifest: ImportMapManifest): string;
|
|
56
|
-
//# sourceMappingURL=import-map.d.ts.map
|
package/dist/import-map.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"import-map.d.ts","sourceRoot":"","sources":["../src/import-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,SAAS,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAA;IAChD,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,SAAS,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAA;IAC9C,oDAAoD;IACpD,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,mEAAmE;IACnE,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAOD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,CAavE"}
|
package/dist/import-map.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// src/import-map.ts
|
|
2
|
-
function escapeHtmlAttr(value) {
|
|
3
|
-
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
4
|
-
}
|
|
5
|
-
function renderImportMapHtml(manifest) {
|
|
6
|
-
const imports = manifest.importmap?.imports ?? {};
|
|
7
|
-
const json = JSON.stringify({ imports }).replace(/</g, "\\u003c");
|
|
8
|
-
const lines = [`<script type="importmap">${json}</script>`];
|
|
9
|
-
for (const href of manifest.preloads ?? []) {
|
|
10
|
-
lines.push(`<link rel="modulepreload" href="${escapeHtmlAttr(href)}" crossorigin>`);
|
|
11
|
-
}
|
|
12
|
-
return lines.join(`
|
|
13
|
-
`) + `
|
|
14
|
-
`;
|
|
15
|
-
}
|
|
16
|
-
export {
|
|
17
|
-
renderImportMapHtml
|
|
18
|
-
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* renderImportMapHtml tests
|
|
3
|
-
*
|
|
4
|
-
* The shared importmap-snippet renderer turns a parsed `barefoot-externals.json`
|
|
5
|
-
* into the `<script type="importmap">` (+ `<link rel="modulepreload">`) HTML that
|
|
6
|
-
* `bf build` emits as `barefoot-importmap.html` for template-string adapters
|
|
7
|
-
* (issue #1644). This is the single source of truth for that snippet.
|
|
8
|
-
*/
|
|
9
|
-
import { describe, test, expect } from 'bun:test'
|
|
10
|
-
import { renderImportMapHtml } from '../import-map'
|
|
11
|
-
|
|
12
|
-
function parseImportMap(html: string): Record<string, string> {
|
|
13
|
-
const match = html.match(/<script type="importmap">(.*?)<\/script>/s)
|
|
14
|
-
if (!match) throw new Error(`no importmap in: ${html}`)
|
|
15
|
-
// Decode the < escape the renderer applies before parsing.
|
|
16
|
-
return JSON.parse(match[1]).imports
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('renderImportMapHtml', () => {
|
|
20
|
-
test('emits the manifest importmap imports verbatim', () => {
|
|
21
|
-
const html = renderImportMapHtml({
|
|
22
|
-
importmap: {
|
|
23
|
-
imports: {
|
|
24
|
-
'@barefootjs/client': '/components/barefoot.js',
|
|
25
|
-
'@barefootjs/client/runtime': '/components/barefoot.js',
|
|
26
|
-
zod: 'https://esm.sh/zod@4.4.3',
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
preloads: [],
|
|
30
|
-
})
|
|
31
|
-
expect(parseImportMap(html)).toEqual({
|
|
32
|
-
'@barefootjs/client': '/components/barefoot.js',
|
|
33
|
-
'@barefootjs/client/runtime': '/components/barefoot.js',
|
|
34
|
-
zod: 'https://esm.sh/zod@4.4.3',
|
|
35
|
-
})
|
|
36
|
-
expect(html).not.toContain('modulepreload')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test('emits modulepreload links with crossorigin for manifest preloads (#1648)', () => {
|
|
40
|
-
const html = renderImportMapHtml({
|
|
41
|
-
importmap: { imports: {} },
|
|
42
|
-
preloads: ['/components/form.js', 'https://esm.sh/zod@4.4.3'],
|
|
43
|
-
})
|
|
44
|
-
expect(html).toContain('<link rel="modulepreload" href="/components/form.js" crossorigin>')
|
|
45
|
-
expect(html).toContain('<link rel="modulepreload" href="https://esm.sh/zod@4.4.3" crossorigin>')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('reads defensively from a partial manifest', () => {
|
|
49
|
-
expect(parseImportMap(renderImportMapHtml({}))).toEqual({})
|
|
50
|
-
expect(renderImportMapHtml({})).not.toContain('modulepreload')
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('ends with a trailing newline (template-include friendly)', () => {
|
|
54
|
-
expect(renderImportMapHtml({ importmap: { imports: {} } }).endsWith('\n')).toBe(true)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('escapes < in the importmap JSON so a URL cannot break out of the script', () => {
|
|
58
|
-
const html = renderImportMapHtml({
|
|
59
|
-
importmap: { imports: { evil: 'https://x/</script><script>alert(1)</script>' } },
|
|
60
|
-
})
|
|
61
|
-
// The literal closing tag must not appear before the importmap's own.
|
|
62
|
-
const importmapClose = html.indexOf('</script>')
|
|
63
|
-
expect(html.slice(0, importmapClose)).not.toContain('</script>')
|
|
64
|
-
// But the value still round-trips through JSON.parse.
|
|
65
|
-
expect(parseImportMap(html).evil).toBe('https://x/</script><script>alert(1)</script>')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('escapes double quotes and angle brackets in preload hrefs', () => {
|
|
69
|
-
const html = renderImportMapHtml({
|
|
70
|
-
preloads: ['/components/"onerror=alert(1).js'],
|
|
71
|
-
})
|
|
72
|
-
expect(html).not.toContain('"onerror=alert(1)')
|
|
73
|
-
expect(html).toContain('"onerror=alert(1)')
|
|
74
|
-
})
|
|
75
|
-
})
|
package/src/import-map.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared importmap-snippet renderer.
|
|
3
|
-
*
|
|
4
|
-
* `bf build` emits `barefoot-externals.json` (the `ExternalsManifest`) whenever
|
|
5
|
-
* `externals` / `bundleEntries` are configured. Component adapters (Hono) read
|
|
6
|
-
* that manifest at render time via a JSX component (`BfImportMap`). Template-
|
|
7
|
-
* string adapters (Go html/template, Mojolicious EP) have no component layer, so
|
|
8
|
-
* `bf build` instead emits a static `barefoot-importmap.html` for them to
|
|
9
|
-
* `{{ template }}` / `%= include` into the page `<head>`.
|
|
10
|
-
*
|
|
11
|
-
* This module is the single source of truth for that snippet's HTML, so every
|
|
12
|
-
* adapter's importmap injection point stays in sync. See issue #1644.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The subset of `barefoot-externals.json` needed to render the importmap
|
|
17
|
-
* snippet. All fields are optional so a partial or hand-written manifest
|
|
18
|
-
* (e.g. a Hono `BfImportMap` `externals` prop) still type-checks. The strict
|
|
19
|
-
* build-output `ExternalsManifest` is structurally assignable to this, so both
|
|
20
|
-
* the CLI's emitted manifest and a hand-written one feed `renderImportMapHtml`.
|
|
21
|
-
* This is the one shared manifest type for every importmap injection path.
|
|
22
|
-
*/
|
|
23
|
-
export interface ImportMapManifest {
|
|
24
|
-
/** Entries for `<script type="importmap">`. */
|
|
25
|
-
importmap?: { imports?: Record<string, string> }
|
|
26
|
-
/** URLs to emit as `<link rel="modulepreload">`. */
|
|
27
|
-
preloads?: string[]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Shape of `barefoot-externals.json`, written by `bf build`. This is the build
|
|
32
|
-
* output contract shared by the CLI (which writes it) and the adapters (which
|
|
33
|
-
* consume it) — the all-required superset of {@link ImportMapManifest}.
|
|
34
|
-
*/
|
|
35
|
-
export interface ExternalsManifest {
|
|
36
|
-
/** Entries for `<script type="importmap">`. */
|
|
37
|
-
importmap: { imports: Record<string, string> }
|
|
38
|
-
/** URLs to emit as `<link rel="modulepreload">`. */
|
|
39
|
-
preloads: string[]
|
|
40
|
-
/** Package names to pass as `--external` to the user's bundler. */
|
|
41
|
-
externals: string[]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Escape a value for use inside a double-quoted HTML attribute. */
|
|
45
|
-
function escapeHtmlAttr(value: string): string {
|
|
46
|
-
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<')
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Render the `<script type="importmap">` (plus `<link rel="modulepreload">`)
|
|
51
|
-
* snippet from a parsed externals manifest. Fields are read defensively so a
|
|
52
|
-
* partial or hand-written manifest still produces valid output.
|
|
53
|
-
*
|
|
54
|
-
* Inside the importmap JSON, each `<` is replaced with its JSON unicode escape
|
|
55
|
-
* for code point U+003C so a URL containing `</script>` cannot break out of the
|
|
56
|
-
* script element — the JSON parser decodes that escape back to `<`, keeping the
|
|
57
|
-
* mapping value-identical.
|
|
58
|
-
*/
|
|
59
|
-
export function renderImportMapHtml(manifest: ImportMapManifest): string {
|
|
60
|
-
const imports = manifest.importmap?.imports ?? {}
|
|
61
|
-
const json = JSON.stringify({ imports }).replace(/</g, '\\u003c')
|
|
62
|
-
const lines = [`<script type="importmap">${json}</script>`]
|
|
63
|
-
for (const href of manifest.preloads ?? []) {
|
|
64
|
-
// `crossorigin` is required so a cross-origin (CDN) preload's request
|
|
65
|
-
// matches the actual module `import` (always a CORS fetch); without it
|
|
66
|
-
// the browser discards the preload and re-fetches. Harmless for
|
|
67
|
-
// same-origin preloads, which use the same credentials mode either way.
|
|
68
|
-
// See issue #1648.
|
|
69
|
-
lines.push(`<link rel="modulepreload" href="${escapeHtmlAttr(href)}" crossorigin>`)
|
|
70
|
-
}
|
|
71
|
-
return lines.join('\n') + '\n'
|
|
72
|
-
}
|