@nifrajs/web 5.1.0 → 5.3.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/CHANGELOG.md +3042 -0
- package/dist/build-plan.d.ts +7 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +19 -0
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +1 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +51 -3
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +23 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +224 -16
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -7
- package/dist/client.js.map +1 -1
- package/dist/csp.d.ts +2 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +2 -0
- package/dist/csp.js.map +1 -1
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +23 -3
- package/dist/deferred.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/codegen.d.ts +6 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +23 -5
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/copy-split.d.ts +27 -0
- package/dist/internal/copy-split.d.ts.map +1 -0
- package/dist/internal/copy-split.js +173 -0
- package/dist/internal/copy-split.js.map +1 -0
- package/dist/internal/deferred-free.d.ts +3 -0
- package/dist/internal/deferred-free.d.ts.map +1 -0
- package/dist/internal/deferred-free.js +15 -0
- package/dist/internal/deferred-free.js.map +1 -0
- package/dist/internal/module-preparation.d.ts +7 -0
- package/dist/internal/module-preparation.d.ts.map +1 -0
- package/dist/internal/module-preparation.js +24 -0
- package/dist/internal/module-preparation.js.map +1 -0
- package/dist/internal/output-guard.d.ts +2 -0
- package/dist/internal/output-guard.d.ts.map +1 -1
- package/dist/internal/output-guard.js +51 -9
- package/dist/internal/output-guard.js.map +1 -1
- package/dist/internal/page-execution.d.ts +20 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +115 -50
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +3 -1
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/prerendered-pages.d.ts +42 -0
- package/dist/internal/prerendered-pages.d.ts.map +1 -0
- package/dist/internal/prerendered-pages.js +92 -0
- package/dist/internal/prerendered-pages.js.map +1 -0
- package/dist/internal/public-url.d.ts +7 -0
- package/dist/internal/public-url.d.ts.map +1 -1
- package/dist/internal/public-url.js +13 -0
- package/dist/internal/public-url.js.map +1 -1
- package/dist/internal/render-document.d.ts +11 -2
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +131 -41
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-url.d.ts +4 -0
- package/dist/internal/request-url.d.ts.map +1 -1
- package/dist/internal/request-url.js +24 -0
- package/dist/internal/request-url.js.map +1 -1
- package/dist/internal/secret-scan.d.ts +4 -0
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +8 -1
- package/dist/internal/secret-scan.js.map +1 -1
- package/dist/internal/typescript-api.d.ts +4 -0
- package/dist/internal/typescript-api.d.ts.map +1 -0
- package/dist/internal/typescript-api.js +8 -0
- package/dist/internal/typescript-api.js.map +1 -0
- package/dist/internal/vite-plugin-compat.d.ts +28 -0
- package/dist/internal/vite-plugin-compat.d.ts.map +1 -0
- package/dist/internal/vite-plugin-compat.js +82 -0
- package/dist/internal/vite-plugin-compat.js.map +1 -0
- package/dist/internal/web-app.d.ts +45 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +32 -1
- package/dist/internal/web-app.js.map +1 -1
- package/dist/isr.d.ts +1 -0
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +7 -4
- package/dist/isr.js.map +1 -1
- package/dist/plugins/import-glob.d.ts +20 -0
- package/dist/plugins/import-glob.d.ts.map +1 -0
- package/dist/plugins/import-glob.js +401 -0
- package/dist/plugins/import-glob.js.map +1 -0
- package/dist/plugins/kit.d.ts +11 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +32 -0
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/stylex.d.ts.map +1 -1
- package/dist/plugins/stylex.js +2 -2
- package/dist/plugins/stylex.js.map +1 -1
- package/dist/prerender.d.ts +4 -0
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +9 -1
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +15 -0
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +91 -12
- package/dist/public-dir.js.map +1 -1
- package/dist/render-seam.d.ts +42 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +33 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/router.d.ts +3 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +4 -4
- package/dist/router.js.map +1 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +2 -0
- package/dist/search.js.map +1 -1
- package/dist/server-components/typescript.d.ts.map +1 -1
- package/dist/server-components/typescript.js +2 -7
- package/dist/server-components/typescript.js.map +1 -1
- package/dist/vite.d.ts +7 -28
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +130 -123
- package/dist/vite.js.map +1 -1
- package/package.json +5 -4
- package/src/build-plan.ts +21 -0
- package/src/build-vite.ts +60 -3
- package/src/build.ts +283 -39
- package/src/client.ts +15 -7
- package/src/csp.ts +4 -0
- package/src/deferred.ts +20 -3
- package/src/index.ts +9 -0
- package/src/internal/codegen.ts +32 -3
- package/src/internal/copy-split.ts +220 -0
- package/src/internal/deferred-free.ts +15 -0
- package/src/internal/module-preparation.ts +30 -0
- package/src/internal/output-guard.ts +51 -9
- package/src/internal/page-execution.ts +137 -53
- package/src/internal/parity.ts +2 -1
- package/src/internal/prerendered-pages.ts +124 -0
- package/src/internal/public-url.ts +13 -0
- package/src/internal/render-document.ts +179 -58
- package/src/internal/request-url.ts +24 -0
- package/src/internal/secret-scan.ts +9 -1
- package/src/internal/typescript-api.ts +11 -0
- package/src/internal/vite-plugin-compat.ts +81 -0
- package/src/internal/web-app.ts +82 -2
- package/src/isr.ts +7 -4
- package/src/plugins/import-glob.ts +392 -0
- package/src/plugins/kit.ts +36 -0
- package/src/plugins/stylex.ts +2 -1
- package/src/prerender.ts +13 -1
- package/src/public-dir.ts +107 -12
- package/src/render-seam.ts +65 -2
- package/src/router.ts +7 -6
- package/src/search.ts +1 -0
- package/src/server-components/typescript.ts +2 -10
- package/src/vite.ts +133 -135
package/src/isr.ts
CHANGED
|
@@ -161,6 +161,8 @@ export class MemoryCacheStore implements CacheStore {
|
|
|
161
161
|
private order = 0
|
|
162
162
|
private readonly purgedKeys = new Map<string, number>()
|
|
163
163
|
private readonly purgedTags = new Map<string, number>()
|
|
164
|
+
// The newest purge order whose mark was evicted. A render fenced before it may predate that purge.
|
|
165
|
+
private purgeFloor = 0
|
|
164
166
|
|
|
165
167
|
constructor(options: MemoryCacheStoreOptions = {}) {
|
|
166
168
|
const isProd = typeof process !== "undefined" && process.env?.NODE_ENV === "production"
|
|
@@ -246,8 +248,8 @@ export class MemoryCacheStore implements CacheStore {
|
|
|
246
248
|
}
|
|
247
249
|
|
|
248
250
|
private purgedAfter(key: string, tags: readonly string[], fence: number): boolean {
|
|
249
|
-
if ((this.purgedKeys.get(key) ??
|
|
250
|
-
return tags.some((tag) => (this.purgedTags.get(tag) ??
|
|
251
|
+
if ((this.purgedKeys.get(key) ?? this.purgeFloor) > fence) return true
|
|
252
|
+
return tags.some((tag) => (this.purgedTags.get(tag) ?? this.purgeFloor) > fence)
|
|
251
253
|
}
|
|
252
254
|
|
|
253
255
|
private markPurged(marks: Map<string, number>, name: string): void {
|
|
@@ -255,9 +257,10 @@ export class MemoryCacheStore implements CacheStore {
|
|
|
255
257
|
marks.set(name, ++this.order)
|
|
256
258
|
// Bounded like the entries: a mark matters only to a render still in flight.
|
|
257
259
|
while (marks.size > this.max) {
|
|
258
|
-
const oldest = marks.
|
|
260
|
+
const oldest = marks.entries().next().value
|
|
259
261
|
if (oldest === undefined) break
|
|
260
|
-
marks.delete(oldest)
|
|
262
|
+
marks.delete(oldest[0])
|
|
263
|
+
this.purgeFloor = Math.max(this.purgeFloor, oldest[1])
|
|
261
264
|
}
|
|
262
265
|
}
|
|
263
266
|
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `import.meta.glob` for the Bun build. Vite expands it at build time and Bun has no equivalent, so
|
|
3
|
+
* without this a module that loads its siblings through a glob bundles with the call left in place:
|
|
4
|
+
* `import.meta.glob` is `undefined` in the browser and every file it named is missing from the build.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs"
|
|
7
|
+
import { dirname, extname, relative, resolve, sep } from "node:path"
|
|
8
|
+
import type { BunPlugin } from "bun"
|
|
9
|
+
import { readSource } from "./kit.ts"
|
|
10
|
+
|
|
11
|
+
const SOURCE = /\.[cm]?[jt]sx?$/
|
|
12
|
+
const CALL = /\bimport\.meta\.glob\b/g
|
|
13
|
+
const LOADERS: Readonly<Record<string, "ts" | "tsx" | "jsx" | "js">> = {
|
|
14
|
+
".ts": "ts",
|
|
15
|
+
".mts": "ts",
|
|
16
|
+
".cts": "ts",
|
|
17
|
+
".tsx": "tsx",
|
|
18
|
+
".jsx": "jsx",
|
|
19
|
+
// Bun's own default: a `.js` module may hold JSX, a `.mjs` or `.cjs` one may not.
|
|
20
|
+
".js": "jsx",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface GlobOptions {
|
|
24
|
+
readonly eager: boolean
|
|
25
|
+
/** The export each entry takes: `"default"`, a name, or the whole module when unset. */
|
|
26
|
+
readonly pick: string | undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A literal the call's arguments may hold. */
|
|
30
|
+
type Literal = string | boolean | readonly string[] | Readonly<Record<string, string | boolean>>
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `source` with every comment, string body and template text blanked to spaces, offsets kept, so a
|
|
34
|
+
* glob named in prose or inside a string is never read as a call. A template's `${}` stays code.
|
|
35
|
+
*/
|
|
36
|
+
function codeOnly(source: string): string {
|
|
37
|
+
const out = source.split("")
|
|
38
|
+
const blank = (from: number, to: number): void => {
|
|
39
|
+
for (let index = from; index < to; index++) if (out[index] !== "\n") out[index] = " "
|
|
40
|
+
}
|
|
41
|
+
// Reads code from `start`; inside a template's `${`, returns past the `}` that closes it.
|
|
42
|
+
const code = (start: number, nested: boolean): number => {
|
|
43
|
+
let index = start
|
|
44
|
+
let depth = 0
|
|
45
|
+
while (index < source.length) {
|
|
46
|
+
const char = source[index]
|
|
47
|
+
if (source.startsWith("//", index)) {
|
|
48
|
+
const end = source.indexOf("\n", index)
|
|
49
|
+
const stop = end === -1 ? source.length : end
|
|
50
|
+
blank(index, stop)
|
|
51
|
+
index = stop
|
|
52
|
+
} else if (source.startsWith("/*", index)) {
|
|
53
|
+
const end = source.indexOf("*/", index + 2)
|
|
54
|
+
const stop = end === -1 ? source.length : end + 2
|
|
55
|
+
blank(index, stop)
|
|
56
|
+
index = stop
|
|
57
|
+
} else if (char === '"' || char === "'") {
|
|
58
|
+
let end = index + 1
|
|
59
|
+
while (end < source.length && source[end] !== char) end += source[end] === "\\" ? 2 : 1
|
|
60
|
+
blank(index + 1, Math.min(end, source.length))
|
|
61
|
+
index = end + 1
|
|
62
|
+
} else if (char === "`") {
|
|
63
|
+
index = template(index + 1)
|
|
64
|
+
} else if (nested && char === "}" && depth === 0) {
|
|
65
|
+
return index + 1
|
|
66
|
+
} else {
|
|
67
|
+
if (nested && char === "{") depth += 1
|
|
68
|
+
if (nested && char === "}") depth -= 1
|
|
69
|
+
index += 1
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return index
|
|
73
|
+
}
|
|
74
|
+
const template = (start: number): number => {
|
|
75
|
+
let index = start
|
|
76
|
+
while (index < source.length && source[index] !== "`") {
|
|
77
|
+
if (source.startsWith("${", index)) {
|
|
78
|
+
index = code(index + 2, true)
|
|
79
|
+
} else {
|
|
80
|
+
const step = source[index] === "\\" ? 2 : 1
|
|
81
|
+
blank(index, index + step)
|
|
82
|
+
index += step
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return index + 1
|
|
86
|
+
}
|
|
87
|
+
code(0, false)
|
|
88
|
+
return out.join("")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A reader over the call's argument text: string, array of strings, `{ key: literal }`, booleans. */
|
|
92
|
+
function argumentReader(text: string, start: number) {
|
|
93
|
+
let at = start
|
|
94
|
+
const space = (): void => {
|
|
95
|
+
while (at < text.length && /\s/.test(text[at] ?? "")) at += 1
|
|
96
|
+
}
|
|
97
|
+
const fail = (): never => {
|
|
98
|
+
throw new SyntaxError("not a literal")
|
|
99
|
+
}
|
|
100
|
+
const string = (): string => {
|
|
101
|
+
const quote = text[at]
|
|
102
|
+
if (quote !== '"' && quote !== "'" && quote !== "`") return fail()
|
|
103
|
+
let value = ""
|
|
104
|
+
at += 1
|
|
105
|
+
while (at < text.length && text[at] !== quote) {
|
|
106
|
+
const char = text[at] ?? ""
|
|
107
|
+
if (quote === "`" && char === "$" && text[at + 1] === "{") return fail()
|
|
108
|
+
if (char === "\\") {
|
|
109
|
+
value += text[at + 1] ?? ""
|
|
110
|
+
at += 2
|
|
111
|
+
} else {
|
|
112
|
+
value += char
|
|
113
|
+
at += 1
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
at += 1
|
|
117
|
+
return value
|
|
118
|
+
}
|
|
119
|
+
const literal = (): Literal => {
|
|
120
|
+
space()
|
|
121
|
+
const char = text[at]
|
|
122
|
+
if (char === "[") {
|
|
123
|
+
at += 1
|
|
124
|
+
const items: string[] = []
|
|
125
|
+
for (;;) {
|
|
126
|
+
space()
|
|
127
|
+
if (text[at] === "]") break
|
|
128
|
+
items.push(string())
|
|
129
|
+
space()
|
|
130
|
+
if (text[at] === ",") at += 1
|
|
131
|
+
else if (text[at] !== "]") return fail()
|
|
132
|
+
}
|
|
133
|
+
at += 1
|
|
134
|
+
return items
|
|
135
|
+
}
|
|
136
|
+
if (char === "{") {
|
|
137
|
+
at += 1
|
|
138
|
+
const entries: Record<string, string | boolean> = {}
|
|
139
|
+
for (;;) {
|
|
140
|
+
space()
|
|
141
|
+
if (text[at] === "}") break
|
|
142
|
+
const key = /^[A-Za-z_$][\w$]*/.exec(text.slice(at))?.[0]
|
|
143
|
+
const name = key ?? string()
|
|
144
|
+
if (key !== undefined) at += key.length
|
|
145
|
+
space()
|
|
146
|
+
if (text[at] !== ":") return fail()
|
|
147
|
+
at += 1
|
|
148
|
+
const value = literal()
|
|
149
|
+
if (typeof value !== "string" && typeof value !== "boolean") return fail()
|
|
150
|
+
entries[name] = value
|
|
151
|
+
space()
|
|
152
|
+
if (text[at] === ",") at += 1
|
|
153
|
+
else if (text[at] !== "}") return fail()
|
|
154
|
+
}
|
|
155
|
+
at += 1
|
|
156
|
+
return entries
|
|
157
|
+
}
|
|
158
|
+
if (text.startsWith("true", at) || text.startsWith("false", at)) {
|
|
159
|
+
const value = text.startsWith("true", at)
|
|
160
|
+
at += value ? 4 : 5
|
|
161
|
+
return value
|
|
162
|
+
}
|
|
163
|
+
return string()
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
literal,
|
|
167
|
+
space,
|
|
168
|
+
get at() {
|
|
169
|
+
return at
|
|
170
|
+
},
|
|
171
|
+
expect(char: string): void {
|
|
172
|
+
space()
|
|
173
|
+
if (text[at] !== char) fail()
|
|
174
|
+
at += 1
|
|
175
|
+
},
|
|
176
|
+
peek(): string | undefined {
|
|
177
|
+
space()
|
|
178
|
+
return text[at]
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const OPTION_NAMES = new Set(["eager", "import"])
|
|
184
|
+
|
|
185
|
+
// `Array.isArray` does not narrow a readonly array out of a union.
|
|
186
|
+
const isList = (value: Literal): value is readonly string[] => Array.isArray(value)
|
|
187
|
+
|
|
188
|
+
function readOptions(value: Literal | undefined): GlobOptions {
|
|
189
|
+
if (value === undefined) return { eager: false, pick: undefined }
|
|
190
|
+
if (typeof value !== "object" || isList(value)) throw new SyntaxError("options")
|
|
191
|
+
const options: Readonly<Record<string, string | boolean>> = value
|
|
192
|
+
for (const name of Object.keys(options))
|
|
193
|
+
if (!OPTION_NAMES.has(name)) throw new SyntaxError(`option ${name}`)
|
|
194
|
+
const eager = options.eager ?? false
|
|
195
|
+
const pick = options.import
|
|
196
|
+
if (typeof eager !== "boolean" || (pick !== undefined && typeof pick !== "string"))
|
|
197
|
+
throw new SyntaxError("option types")
|
|
198
|
+
return { eager, pick }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The files a set of patterns matches from `importer`, by Vite's rules, sorted; `!` excludes. */
|
|
202
|
+
function matchGlob(importer: string, root: string, patterns: readonly string[]): string[] {
|
|
203
|
+
// Globs read `\` as an escape, so on Windows every path is matched with forward slashes.
|
|
204
|
+
const slash = (path: string): string => (sep === "\\" ? path.replaceAll("\\", "/") : path)
|
|
205
|
+
const absolute = (pattern: string): string => {
|
|
206
|
+
if (pattern.startsWith("/")) return slash(resolve(root, `.${pattern}`))
|
|
207
|
+
if (pattern.startsWith("./") || pattern.startsWith("../"))
|
|
208
|
+
return slash(resolve(dirname(importer), pattern))
|
|
209
|
+
throw new SyntaxError(`pattern ${pattern}`)
|
|
210
|
+
}
|
|
211
|
+
const include = patterns.filter((pattern) => !pattern.startsWith("!")).map(absolute)
|
|
212
|
+
const exclude = patterns
|
|
213
|
+
.filter((pattern) => pattern.startsWith("!"))
|
|
214
|
+
.map((pattern) => new Bun.Glob(absolute(pattern.slice(1))))
|
|
215
|
+
const found = new Set<string>()
|
|
216
|
+
for (const pattern of include) {
|
|
217
|
+
const segments = pattern.split("/")
|
|
218
|
+
const wild = segments.findIndex((segment) => /[*?[\]{}]/.test(segment))
|
|
219
|
+
if (wild === -1) {
|
|
220
|
+
if (existsSync(pattern)) found.add(pattern)
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
const base = segments.slice(0, wild).join("/") || "/"
|
|
224
|
+
const rest = segments.slice(wild).join("/")
|
|
225
|
+
for (const file of new Bun.Glob(rest).scanSync({ cwd: base, onlyFiles: true })) {
|
|
226
|
+
const path = slash(resolve(base, file))
|
|
227
|
+
if (!rest.includes("node_modules") && path.split("/").includes("node_modules")) continue
|
|
228
|
+
found.add(path)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const self = slash(importer)
|
|
232
|
+
return [...found]
|
|
233
|
+
.filter((path) => path !== self && !exclude.some((glob) => glob.match(path)))
|
|
234
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** `value` as a JS string literal that stays on one line for every parser: before ES2019, U+2028 and
|
|
238
|
+
* U+2029 end a line even inside a string. */
|
|
239
|
+
const stringLiteral = (value: string): string =>
|
|
240
|
+
JSON.stringify(value).replace(
|
|
241
|
+
/[\u2028\u2029]/g,
|
|
242
|
+
(char) => `\\u${char.charCodeAt(0).toString(16)}`,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
/** How a matched file is keyed, as Vite keys it: from the importer (`./x.ts`) or the root (`/x.ts`). */
|
|
246
|
+
function keyOf(importer: string, root: string, file: string, rooted: boolean): string {
|
|
247
|
+
if (rooted) return `/${relative(root, file).replaceAll("\\", "/")}`
|
|
248
|
+
const path = relative(dirname(importer), file).replaceAll("\\", "/")
|
|
249
|
+
return path.startsWith(".") ? path : `./${path}`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* How often `import.meta.glob` appears in the module's code as Bun reads it: types stripped and JSX
|
|
254
|
+
* text turned into strings, neither of which {@link codeOnly} can see in the source.
|
|
255
|
+
*/
|
|
256
|
+
function globsInCode(source: string, file: string): number {
|
|
257
|
+
const js = new Bun.Transpiler({ loader: LOADERS[extname(file)] ?? "js" }).transformSync(source)
|
|
258
|
+
return codeOnly(js).match(CALL)?.length ?? 0
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const unclear = (file: string): Error =>
|
|
262
|
+
new Error(
|
|
263
|
+
`[nifra/web] ${file}: the Bun build cannot tell this module's import.meta.glob calls from text that names it (JSX text, or a quote inside a regular expression). Move the calls into a .ts module, or build with --vite.`,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* `source` (the module at `file`) with each `import.meta.glob(...)` call expanded into an object of
|
|
268
|
+
* the files it matches. Throws, naming the line, on a call this cannot read.
|
|
269
|
+
*/
|
|
270
|
+
export function expandImportGlobs(source: string, file: string, root: string): string {
|
|
271
|
+
const real = globsInCode(source, file)
|
|
272
|
+
// A docs page that shows the call in JSX text has no call to expand, and rewriting its text is wrong.
|
|
273
|
+
if (real === 0) return source
|
|
274
|
+
// JSX text is not a string to `codeOnly`: an apostrophe there flips what it reads as code.
|
|
275
|
+
const code = codeOnly(source)
|
|
276
|
+
if ((code.match(CALL)?.length ?? 0) !== real) throw unclear(file)
|
|
277
|
+
const edits: Array<{ start: number; end: number; text: string }> = []
|
|
278
|
+
const hoisted: string[] = []
|
|
279
|
+
let call = 0
|
|
280
|
+
for (const match of code.matchAll(CALL)) {
|
|
281
|
+
let start = match.index
|
|
282
|
+
const line = source.slice(0, start).split("\n").length
|
|
283
|
+
// A bundle that expands the call has the function: a `typeof` guard around it reads so.
|
|
284
|
+
const guard = /\btypeof\s+$/.exec(code.slice(Math.max(0, start - 32), start))
|
|
285
|
+
if (guard !== null) {
|
|
286
|
+
start -= guard[0].length
|
|
287
|
+
edits.push({ start, end: match.index + match[0].length, text: '"function"' })
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
const refuse = (why: string): never => {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`[nifra/web] ${file}:${line}: import.meta.glob ${why}. The Bun build reads a literal pattern (a string or an array of strings starting with ./, ../ or /, "!" excluding) and the options eager and import; write it that way, or build with --vite.`,
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
let after = start + match[0].length
|
|
296
|
+
if (!/^\s*[(<]/.test(code.slice(after))) refuse("is read as a value, not called")
|
|
297
|
+
// `import.meta.glob<T>(...)`: TypeScript's type argument, dropped with the call.
|
|
298
|
+
if (code[after] === "<") {
|
|
299
|
+
const close = code.indexOf(">(", after)
|
|
300
|
+
if (close === -1) refuse("has a type argument this cannot read")
|
|
301
|
+
after = close + 1
|
|
302
|
+
}
|
|
303
|
+
const reader = argumentReader(source, after)
|
|
304
|
+
let patterns: readonly string[]
|
|
305
|
+
let options: GlobOptions
|
|
306
|
+
try {
|
|
307
|
+
reader.expect("(")
|
|
308
|
+
const first = reader.literal()
|
|
309
|
+
patterns = typeof first === "string" ? [first] : isList(first) ? first : []
|
|
310
|
+
if (patterns.length === 0) throw new SyntaxError("patterns")
|
|
311
|
+
let second: Literal | undefined
|
|
312
|
+
if (reader.peek() === ",") {
|
|
313
|
+
reader.expect(",")
|
|
314
|
+
if (reader.peek() !== ")") second = reader.literal()
|
|
315
|
+
}
|
|
316
|
+
options = readOptions(second)
|
|
317
|
+
if (reader.peek() === ",") reader.expect(",")
|
|
318
|
+
reader.expect(")")
|
|
319
|
+
} catch (error) {
|
|
320
|
+
return refuse(
|
|
321
|
+
error instanceof SyntaxError && error.message.startsWith("option ")
|
|
322
|
+
? `takes the ${error.message.slice(7)} option, which the Bun build does not support`
|
|
323
|
+
: "takes arguments that are not literals",
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
let files: string[]
|
|
327
|
+
try {
|
|
328
|
+
files = matchGlob(file, root, patterns)
|
|
329
|
+
} catch {
|
|
330
|
+
return refuse("takes a pattern that does not start with ./, ../ or /")
|
|
331
|
+
}
|
|
332
|
+
const rooted = patterns.some((pattern) => pattern.replace(/^!/, "").startsWith("/"))
|
|
333
|
+
const entries = files.map((target, index) => {
|
|
334
|
+
const key = stringLiteral(keyOf(file, root, target, rooted))
|
|
335
|
+
const specifier = stringLiteral(keyOf(file, root, target, false))
|
|
336
|
+
if (options.eager) {
|
|
337
|
+
const name = `__nifra_glob_${call}_${index}`
|
|
338
|
+
hoisted.push(`import * as ${name} from ${specifier}`)
|
|
339
|
+
return `${key}: ${options.pick === undefined ? name : `${name}[${stringLiteral(options.pick)}]`}`
|
|
340
|
+
}
|
|
341
|
+
const load = `import(${specifier})`
|
|
342
|
+
return `${key}: () => ${options.pick === undefined ? load : `${load}.then((m) => m[${stringLiteral(options.pick)}])`}`
|
|
343
|
+
})
|
|
344
|
+
edits.push({ start, end: reader.at, text: `({${entries.join(", ")}})` })
|
|
345
|
+
call += 1
|
|
346
|
+
}
|
|
347
|
+
let next = source
|
|
348
|
+
for (const edit of edits.reverse())
|
|
349
|
+
next = next.slice(0, edit.start) + edit.text + next.slice(edit.end)
|
|
350
|
+
// Equal counts can still pair a text mention with a missed call; one left in the code is that.
|
|
351
|
+
if (globsInCode(next, file) !== 0) throw unclear(file)
|
|
352
|
+
// Import declarations are hoisted wherever they sit, so appending keeps any directive prologue first.
|
|
353
|
+
return hoisted.length === 0 ? next : `${next}\n${hoisted.join("\n")}\n`
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Expand `import.meta.glob` in the app's own modules, for `Bun.build`. `root` is the app root, which a
|
|
358
|
+
* pattern starting with `/` is read from. Installed packages are left as published. Register it after
|
|
359
|
+
* every plugin that loads app modules itself (a server-function stub must win over it); a module such
|
|
360
|
+
* a plugin loads keeps its call, which {@link assertNoImportGlob} then refuses in the output. Register
|
|
361
|
+
* it before a plugin that rewrites source through `addSourceTransform`: it reads through that rewrite.
|
|
362
|
+
*/
|
|
363
|
+
export function importGlobPlugin(options: { readonly root: string }): BunPlugin {
|
|
364
|
+
return {
|
|
365
|
+
name: "nifra-import-glob",
|
|
366
|
+
setup(build) {
|
|
367
|
+
build.onLoad({ filter: SOURCE }, async (args) => {
|
|
368
|
+
if (args.namespace !== "file" || args.path.split("/").includes("node_modules"))
|
|
369
|
+
return undefined
|
|
370
|
+
if (!(await Bun.file(args.path).text()).includes("import.meta.glob")) return undefined
|
|
371
|
+
// Claiming the module would skip a later plugin's rewrite of it (the copy split), so read
|
|
372
|
+
// through it.
|
|
373
|
+
const source = await readSource(build, args.path)
|
|
374
|
+
const contents = expandImportGlobs(source, args.path, options.root)
|
|
375
|
+
if (contents === source) return undefined
|
|
376
|
+
return { contents, loader: LOADERS[extname(args.path)] ?? "js" }
|
|
377
|
+
})
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Refuse a bundle that still calls `import.meta.glob`: the browser has no such function, so every
|
|
383
|
+
* file the call named would be missing at run time. `outputs` are the emitted chunks, path to text. */
|
|
384
|
+
export function assertNoImportGlob(outputs: ReadonlyArray<readonly [string, string]>): void {
|
|
385
|
+
const left = outputs
|
|
386
|
+
.filter(([, text]) => /\bimport\.meta\.glob\s*\(/.test(text))
|
|
387
|
+
.map(([path]) => path)
|
|
388
|
+
if (left.length === 0) return
|
|
389
|
+
throw new Error(
|
|
390
|
+
`[nifra/web] ${left.join(", ")} still call${left.length === 1 ? "s" : ""} import.meta.glob, which no browser or server runtime defines. A plugin that loads a module itself runs before the glob expansion and keeps its call; expand it in that plugin, or build with --vite.`,
|
|
391
|
+
)
|
|
392
|
+
}
|
package/src/plugins/kit.ts
CHANGED
|
@@ -122,6 +122,42 @@ export function hash8(input: string): string {
|
|
|
122
122
|
return h.toString(16).padStart(8, "0")
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
const SOURCE_TRANSFORMS = Symbol.for("nifra.build.source-transforms")
|
|
126
|
+
|
|
127
|
+
/** A rewrite of a source file's text before any plugin compiles it; `undefined` leaves the text as is. */
|
|
128
|
+
export type SourceTransform = (path: string, source: string) => string | undefined
|
|
129
|
+
|
|
130
|
+
const isTransformList = (value: unknown): value is SourceTransform[] => Array.isArray(value)
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Register a source rewrite for one build. Bun runs only the first `onLoad` that returns contents, so
|
|
134
|
+
* a plugin that claims a file and reads it off disk would compile it without the rewrite: such a plugin
|
|
135
|
+
* reads through {@link readSource} instead. Stored on the build's config, which every plugin of one
|
|
136
|
+
* `Bun.build` shares.
|
|
137
|
+
*/
|
|
138
|
+
export function addSourceTransform(build: PluginBuilder, transform: SourceTransform): void {
|
|
139
|
+
const config: unknown = build.config
|
|
140
|
+
if (typeof config !== "object" || config === null) return
|
|
141
|
+
const existing: unknown = Reflect.get(config, SOURCE_TRANSFORMS)
|
|
142
|
+
if (isTransformList(existing)) existing.push(transform)
|
|
143
|
+
else Reflect.set(config, SOURCE_TRANSFORMS, [transform])
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A source file's text as this build compiles it: off disk, through each {@link addSourceTransform}. */
|
|
147
|
+
export async function readSource(build: PluginBuilder, path: string): Promise<string> {
|
|
148
|
+
let source = await Bun.file(path).text()
|
|
149
|
+
// A runtime plugin (`Bun.plugin`) has no build config.
|
|
150
|
+
const config: unknown = build.config
|
|
151
|
+
const transforms: unknown =
|
|
152
|
+
typeof config === "object" && config !== null
|
|
153
|
+
? Reflect.get(config, SOURCE_TRANSFORMS)
|
|
154
|
+
: undefined
|
|
155
|
+
if (isTransformList(transforms)) {
|
|
156
|
+
for (const transform of transforms) source = transform(path, source) ?? source
|
|
157
|
+
}
|
|
158
|
+
return source
|
|
159
|
+
}
|
|
160
|
+
|
|
125
161
|
/**
|
|
126
162
|
* Env flag a nifra dev server sets for its own lifetime, so a framework compiler plugin can tell a dev
|
|
127
163
|
* compile from a `nifra build` one. Exported for the dev server that sets it; plugins ask
|
package/src/plugins/stylex.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
devServerCompile,
|
|
12
12
|
hash8,
|
|
13
13
|
normalizeFilePath,
|
|
14
|
+
readSource,
|
|
14
15
|
reproduciblePath,
|
|
15
16
|
requirePeer,
|
|
16
17
|
} from "./kit.ts"
|
|
@@ -236,7 +237,7 @@ export function stylexBunPlugin(
|
|
|
236
237
|
const dependencies = dependenciesFor(options)
|
|
237
238
|
build.onLoad({ filter: JS_FILTER }, async (args) => {
|
|
238
239
|
const path = normalizeFilePath(args.path)
|
|
239
|
-
const source = await
|
|
240
|
+
const source = await readSource(build, path)
|
|
240
241
|
const transformed = await transformModule(source, path, options, dependencies())
|
|
241
242
|
if (transformed === undefined) return
|
|
242
243
|
const contents =
|
package/src/prerender.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { mkdirSync, writeFileSync } from "node:fs"
|
|
12
12
|
import { dirname, join, resolve, sep } from "node:path"
|
|
13
|
+
import { prerenderedDocumentHeaders } from "./internal/prerendered-pages.ts"
|
|
13
14
|
import { assertNoSecrets, buildEnvironment, type SecretExemption } from "./internal/secret-scan.ts"
|
|
14
15
|
import { fillRoutePattern, type RouteEntry } from "./manifest.ts"
|
|
15
16
|
import { DATA_HEADER, PRERENDERED_GLOBAL, PRERENDERED_LIST_PATH } from "./router.ts"
|
|
@@ -52,6 +53,10 @@ export interface PrerenderEntry {
|
|
|
52
53
|
/** The loader-data file written alongside (`<path>/_data.json`) so a client soft-nav INTO this
|
|
53
54
|
* route skips the worker. Absent when the data-mode response is NDJSON (a deferred loader). */
|
|
54
55
|
readonly dataFile?: string
|
|
56
|
+
/** The document headers a self-hosted server sends with this file (its `content-language` and CSP).
|
|
57
|
+
* Absent when the render was specific to its request - it set a cookie, was private or no-store, or
|
|
58
|
+
* varied on a request header - so a server keeps rendering this path on demand. */
|
|
59
|
+
readonly serveHeaders?: Readonly<Record<string, string>>
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
export interface PrerenderResult {
|
|
@@ -184,7 +189,14 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
|
|
|
184
189
|
await dataRes.body?.cancel() // not emitting (non-OK or NDJSON) - release the stream
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
|
|
192
|
+
const serveHeaders = prerenderedDocumentHeaders(res.status, res.headers)
|
|
193
|
+
prerendered.push({
|
|
194
|
+
path,
|
|
195
|
+
file,
|
|
196
|
+
bytes: html.length,
|
|
197
|
+
...(dataFile ? { dataFile } : {}),
|
|
198
|
+
...(serveHeaders ? { serveHeaders } : {}),
|
|
199
|
+
})
|
|
188
200
|
}
|
|
189
201
|
|
|
190
202
|
for (const route of options.routes) {
|