@nifrajs/web 5.0.0 → 5.2.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 +2970 -0
- package/README.md +8 -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 +12 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +43 -6
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +7 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +39 -22
- package/dist/client.js.map +1 -1
- package/dist/dev-port.d.ts +3 -2
- package/dist/dev-port.d.ts.map +1 -1
- package/dist/dev-port.js +2 -6
- package/dist/dev-port.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -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/package-entries.d.ts +34 -0
- package/dist/internal/package-entries.d.ts.map +1 -0
- package/dist/internal/package-entries.js +324 -0
- package/dist/internal/package-entries.js.map +1 -0
- package/dist/internal/page-execution.d.ts +19 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +123 -14
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +15 -2
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +8 -0
- package/dist/internal/private-env.d.ts.map +1 -1
- package/dist/internal/private-env.js +25 -10
- package/dist/internal/private-env.js.map +1 -1
- package/dist/internal/render-document.d.ts +15 -46
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +59 -62
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/route-path.d.ts +6 -0
- package/dist/internal/route-path.d.ts.map +1 -1
- package/dist/internal/route-path.js +11 -0
- package/dist/internal/route-path.js.map +1 -1
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +103 -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 +38 -2
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +30 -3
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/without-query.d.ts +4 -0
- package/dist/internal/without-query.d.ts.map +1 -0
- package/dist/internal/without-query.js +9 -0
- package/dist/internal/without-query.js.map +1 -0
- package/dist/internal/zone-graph.d.ts.map +1 -1
- package/dist/internal/zone-graph.js +17 -6
- package/dist/internal/zone-graph.js.map +1 -1
- package/dist/isr.d.ts +1 -1
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +15 -2
- package/dist/isr.js.map +1 -1
- package/dist/manifest.d.ts +9 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +1 -0
- package/dist/manifest.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/plugins/vite-leak-guard.d.ts +6 -0
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +80 -7
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +17 -21
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/plugins/zone-guard.d.ts.map +1 -1
- package/dist/plugins/zone-guard.js +44 -3
- package/dist/plugins/zone-guard.js.map +1 -1
- package/dist/prerender.d.ts +5 -1
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +41 -19
- package/dist/prerender.js.map +1 -1
- package/dist/render-seam.d.ts +60 -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 +13 -9
- package/dist/router.js.map +1 -1
- package/dist/seo.d.ts +49 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +48 -0
- package/dist/seo.js.map +1 -0
- 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 +1 -28
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +3 -69
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +30 -8
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +185 -46
- package/dist/zones.js.map +1 -1
- package/package.json +10 -4
- package/src/build-plan.ts +21 -0
- package/src/build-vite.ts +60 -3
- package/src/build.ts +57 -4
- package/src/client.ts +42 -22
- package/src/dev-port.ts +4 -10
- package/src/index.ts +8 -4
- package/src/internal/codegen.ts +32 -3
- package/src/internal/copy-split.ts +220 -0
- package/src/internal/package-entries.ts +331 -0
- package/src/internal/page-execution.ts +150 -13
- package/src/internal/parity.ts +15 -2
- package/src/internal/private-env.ts +27 -13
- package/src/internal/render-document.ts +100 -81
- package/src/internal/route-path.ts +16 -0
- package/src/internal/secret-scan.ts +101 -1
- package/src/internal/typescript-api.ts +11 -0
- package/src/internal/vite-plugin-compat.ts +81 -0
- package/src/internal/web-app.ts +71 -6
- package/src/internal/without-query.ts +8 -0
- package/src/internal/zone-graph.ts +19 -6
- package/src/isr.ts +13 -2
- package/src/manifest.ts +10 -0
- package/src/plugins/import-glob.ts +392 -0
- package/src/plugins/kit.ts +36 -0
- package/src/plugins/stylex.ts +2 -1
- package/src/plugins/vite-leak-guard.ts +99 -7
- package/src/plugins/vite-zone-guard.ts +21 -19
- package/src/plugins/zone-guard.ts +41 -2
- package/src/prerender.ts +39 -18
- package/src/render-seam.ts +88 -2
- package/src/router.ts +16 -11
- package/src/seo.ts +68 -0
- package/src/server-components/typescript.ts +2 -10
- package/src/vite.ts +3 -81
- package/src/zones.ts +237 -52
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 =
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
formatServerOnlyLeak,
|
|
47
47
|
} from "../build-plan.ts"
|
|
48
48
|
import { isBareNodeBuiltin } from "../internal/node-builtins.ts"
|
|
49
|
+
import { installedManifest } from "../internal/package-entries.ts"
|
|
49
50
|
import { privateEnvCheck } from "../internal/private-env.ts"
|
|
50
51
|
import {
|
|
51
52
|
emittedScanFiles,
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
scanForSecrets,
|
|
59
60
|
textOf,
|
|
60
61
|
} from "../internal/secret-scan.ts"
|
|
62
|
+
import { withoutQuery } from "../internal/without-query.ts"
|
|
61
63
|
import {
|
|
62
64
|
accountEmittedFiles,
|
|
63
65
|
assetDenial,
|
|
@@ -75,7 +77,7 @@ import {
|
|
|
75
77
|
type GraphModule,
|
|
76
78
|
type RollupBundleLike,
|
|
77
79
|
} from "../module-graph.ts"
|
|
78
|
-
import { createZoneClassifier, type ZoneClassifier } from "../zones.ts"
|
|
80
|
+
import { createZoneClassifier, specifierPackage, type ZoneClassifier } from "../zones.ts"
|
|
79
81
|
|
|
80
82
|
/**
|
|
81
83
|
* The Rollup plugin-context slice this uses: `getModuleInfo` for a module's resolved imports, and `error`
|
|
@@ -160,9 +162,80 @@ export interface LeakGuardPlugin {
|
|
|
160
162
|
* reached the browser.
|
|
161
163
|
*/
|
|
162
164
|
leak?: string
|
|
165
|
+
buildStart(): void
|
|
166
|
+
/** Ordered first: the bundler's own resolver answers before a plugin listed after it is asked. */
|
|
167
|
+
readonly resolveId: {
|
|
168
|
+
readonly order: "pre"
|
|
169
|
+
handler(
|
|
170
|
+
this: ResolveContext,
|
|
171
|
+
source: string,
|
|
172
|
+
importer: string | undefined,
|
|
173
|
+
options?: Readonly<Record<string, unknown>>,
|
|
174
|
+
): Promise<null>
|
|
175
|
+
}
|
|
163
176
|
generateBundle(this: RollupPluginContext, options: unknown, bundle: RollupBundleLike): void
|
|
164
177
|
}
|
|
165
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Rollup keeps no import's written specifier, yet a package that gives each entry its own side is judged
|
|
181
|
+
* by the entry an import names, since two entries may share one file. This records the specifier of
|
|
182
|
+
* each import of such a package by the edge it resolved to, and puts it back on the finished graph.
|
|
183
|
+
*/
|
|
184
|
+
function entrySpecifiers(appRoot: string, options: ServerZoneGuardOptions) {
|
|
185
|
+
// One edge may carry several specifiers: `pkg` and `pkg/react` can resolve to one shared file.
|
|
186
|
+
const written = new Map<string, Set<string>>()
|
|
187
|
+
let classifier: ZoneClassifier | undefined
|
|
188
|
+
const edge = (from: string, to: string): string =>
|
|
189
|
+
`${from.replaceAll("\\", "/")}\0${to.replaceAll("\\", "/")}`
|
|
190
|
+
return {
|
|
191
|
+
reset(): void {
|
|
192
|
+
written.clear()
|
|
193
|
+
classifier = undefined
|
|
194
|
+
},
|
|
195
|
+
async resolveId(
|
|
196
|
+
context: ResolveContext,
|
|
197
|
+
source: string,
|
|
198
|
+
importer: string | undefined,
|
|
199
|
+
resolveOptions: Readonly<Record<string, unknown>> | undefined,
|
|
200
|
+
): Promise<null> {
|
|
201
|
+
const name = specifierPackage(source)
|
|
202
|
+
const from = importer === undefined ? undefined : withoutQuery(importer)
|
|
203
|
+
if (name === undefined || importer === undefined || from === undefined || !isAbsolute(from))
|
|
204
|
+
return null
|
|
205
|
+
const manifest = installedManifest(dirname(from), name)
|
|
206
|
+
if (manifest === undefined) return null
|
|
207
|
+
classifier ??= classifierFor(appRoot, options)
|
|
208
|
+
if (classifier.classify(manifest, source).entry === undefined) return null
|
|
209
|
+
const resolved = await context.resolve(source, importer, {
|
|
210
|
+
...resolveOptions,
|
|
211
|
+
skipSelf: true,
|
|
212
|
+
})
|
|
213
|
+
if (resolved === null) return null
|
|
214
|
+
const key = edge(importer, resolved.id)
|
|
215
|
+
written.set(key, (written.get(key) ?? new Set<string>()).add(source))
|
|
216
|
+
return null
|
|
217
|
+
},
|
|
218
|
+
annotate(graph: ClientModuleGraph): ClientModuleGraph {
|
|
219
|
+
if (written.size === 0) return graph
|
|
220
|
+
const modules: Record<string, GraphModule> = {}
|
|
221
|
+
for (const [id, module] of Object.entries(graph.modules)) {
|
|
222
|
+
modules[id] = {
|
|
223
|
+
imports: module.imports.flatMap((im) => {
|
|
224
|
+
const originals =
|
|
225
|
+
im.original !== undefined || im.path === undefined
|
|
226
|
+
? undefined
|
|
227
|
+
: written.get(edge(id, im.path))
|
|
228
|
+
return originals === undefined
|
|
229
|
+
? [im]
|
|
230
|
+
: [...originals].map((original) => ({ ...im, original }))
|
|
231
|
+
}),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { ...graph, modules }
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
166
239
|
const CSS_FILE = /\.(?:css|scss|sass|less|styl)(?:$|\?)/
|
|
167
240
|
|
|
168
241
|
/**
|
|
@@ -175,8 +248,16 @@ export function viteLeakGuard(options: LeakGuardOptions = {}): LeakGuardPlugin {
|
|
|
175
248
|
const appRoot = resolve(options.appRoot ?? process.cwd())
|
|
176
249
|
const root = resolve(options.root ?? appRoot)
|
|
177
250
|
const outDir = resolve(root, options.outDir ?? "dist")
|
|
251
|
+
const specifiers = entrySpecifiers(appRoot, options)
|
|
178
252
|
const plugin: LeakGuardPlugin = {
|
|
179
253
|
name: "nifra:leak-guard",
|
|
254
|
+
buildStart: () => specifiers.reset(),
|
|
255
|
+
resolveId: {
|
|
256
|
+
order: "pre",
|
|
257
|
+
handler(source, importer, resolveOptions) {
|
|
258
|
+
return specifiers.resolveId(this, source, importer, resolveOptions)
|
|
259
|
+
},
|
|
260
|
+
},
|
|
180
261
|
generateBundle(_options, bundle) {
|
|
181
262
|
const classifier = classifierFor(appRoot, options)
|
|
182
263
|
// Resolved import edges per module, straight from Rollup's graph. Dynamic imports count too: a
|
|
@@ -186,7 +267,9 @@ export function viteLeakGuard(options: LeakGuardOptions = {}): LeakGuardPlugin {
|
|
|
186
267
|
if (info === null) return []
|
|
187
268
|
return [...(info.importedIds ?? []), ...(info.dynamicallyImportedIds ?? [])]
|
|
188
269
|
}
|
|
189
|
-
const graph =
|
|
270
|
+
const graph = specifiers.annotate(
|
|
271
|
+
completeGraph(fromRollupBundle(bundle, importsOf), this, importsOf),
|
|
272
|
+
)
|
|
190
273
|
const sources = { classifier, sourceOf: rollupModuleSource }
|
|
191
274
|
const privateEnv = privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_")
|
|
192
275
|
const outputs = Object.values(bundle as Readonly<Record<string, RollupOutputLike>>)
|
|
@@ -271,8 +354,16 @@ export type ServerZoneGuardOptions = Pick<
|
|
|
271
354
|
*/
|
|
272
355
|
export function viteServerZoneGuard(options: ServerZoneGuardOptions = {}): LeakGuardPlugin {
|
|
273
356
|
const appRoot = resolve(options.appRoot ?? process.cwd())
|
|
357
|
+
const specifiers = entrySpecifiers(appRoot, options)
|
|
274
358
|
const plugin: LeakGuardPlugin = {
|
|
275
359
|
name: "nifra:server-zone-guard",
|
|
360
|
+
buildStart: () => specifiers.reset(),
|
|
361
|
+
resolveId: {
|
|
362
|
+
order: "pre",
|
|
363
|
+
handler(source, importer, resolveOptions) {
|
|
364
|
+
return specifiers.resolveId(this, source, importer, resolveOptions)
|
|
365
|
+
},
|
|
366
|
+
},
|
|
276
367
|
generateBundle(_options, bundle) {
|
|
277
368
|
const classifier = classifierFor(appRoot, options)
|
|
278
369
|
const importsOf = (id: string): readonly string[] => {
|
|
@@ -280,7 +371,9 @@ export function viteServerZoneGuard(options: ServerZoneGuardOptions = {}): LeakG
|
|
|
280
371
|
if (info === null) return []
|
|
281
372
|
return [...(info.importedIds ?? []), ...(info.dynamicallyImportedIds ?? [])]
|
|
282
373
|
}
|
|
283
|
-
const graph =
|
|
374
|
+
const graph = specifiers.annotate(
|
|
375
|
+
completeGraph(fromRollupBundle(bundle, importsOf), this, importsOf),
|
|
376
|
+
)
|
|
284
377
|
const leak = formatServerGraphVerdict(
|
|
285
378
|
verifyServerGraph(graph, {
|
|
286
379
|
classifier,
|
|
@@ -465,7 +558,6 @@ export interface AssetUrlGuardPlugin {
|
|
|
465
558
|
): Promise<null>
|
|
466
559
|
}
|
|
467
560
|
|
|
468
|
-
const POSTFIX = /[?#].*$/s
|
|
469
561
|
const URL_SCHEME = /^(?:[a-z][a-z\d+.-]*:|\/\/)/i
|
|
470
562
|
|
|
471
563
|
/**
|
|
@@ -507,7 +599,7 @@ export function viteAssetUrlGuard(options: ServerZoneGuardOptions = {}): AssetUr
|
|
|
507
599
|
},
|
|
508
600
|
async transform(code, id, transformOptions) {
|
|
509
601
|
if (transformOptions?.ssr === true || !code.includes("import.meta.url")) return null
|
|
510
|
-
const importer = id
|
|
602
|
+
const importer = withoutQuery(id)
|
|
511
603
|
if (!isAbsolute(importer) || importer.includes("/node_modules/") || CSS_FILE.test(importer))
|
|
512
604
|
return null
|
|
513
605
|
let program: unknown
|
|
@@ -553,11 +645,11 @@ async function assetUrlFiles(
|
|
|
553
645
|
): Promise<string[]> {
|
|
554
646
|
const files = new Set<string>()
|
|
555
647
|
const add = (path: string | undefined): void => {
|
|
556
|
-
const file = path
|
|
648
|
+
const file = path === undefined ? undefined : withoutQuery(path)
|
|
557
649
|
if (file === undefined || !isAbsolute(file) || file.includes("\0")) return
|
|
558
650
|
if (statSync(file, { throwIfNoEntry: false })?.isFile() === true) files.add(file)
|
|
559
651
|
}
|
|
560
|
-
if (!url.startsWith("/")) add(resolve(dirname(importer), url
|
|
652
|
+
if (!url.startsWith("/")) add(resolve(dirname(importer), withoutQuery(url)))
|
|
561
653
|
const sources = url.startsWith(".") || url.startsWith("/") ? [url] : [url, `./${url}`]
|
|
562
654
|
for (const source of sources) {
|
|
563
655
|
try {
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The SSR environment loads the same files legitimately and is never checked here.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { readFileSync, statSync } from "node:fs"
|
|
14
14
|
import type { IncomingMessage, ServerResponse } from "node:http"
|
|
15
15
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
|
16
|
+
import { installedManifest } from "../internal/package-entries.ts"
|
|
16
17
|
import { privateEnvDenial } from "../internal/private-env.ts"
|
|
18
|
+
import { withoutQuery } from "../internal/without-query.ts"
|
|
17
19
|
import {
|
|
18
20
|
browserDenial,
|
|
19
21
|
type Classification,
|
|
@@ -78,7 +80,6 @@ export interface ViteZoneGuardPlugin {
|
|
|
78
80
|
fileForUrl(url: string): string | undefined
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
const stripQuery = (id: string): string => id.replace(/[?#].*$/, "")
|
|
82
83
|
/** Query forms that hand a module's source, or a URL to it, to the browser untransformed. */
|
|
83
84
|
const RAW_QUERY = /(?:^|&)(?:raw|url|inline)(?:[&=]|$)/
|
|
84
85
|
|
|
@@ -136,7 +137,7 @@ export function viteZoneGuard(options: ViteZoneGuardOptions): ViteZoneGuardPlugi
|
|
|
136
137
|
const fileForUrl = (url: string): string | undefined => {
|
|
137
138
|
let path: string
|
|
138
139
|
try {
|
|
139
|
-
path = decodeURIComponent(
|
|
140
|
+
path = decodeURIComponent(withoutQuery(url))
|
|
140
141
|
} catch {
|
|
141
142
|
return undefined
|
|
142
143
|
}
|
|
@@ -187,16 +188,17 @@ export function viteZoneGuard(options: ViteZoneGuardOptions): ViteZoneGuardPlugi
|
|
|
187
188
|
return context.error(new Error(message))
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
/** A bare
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
191
|
+
/** A bare import's package, classified through its manifest so a workspace link is first-party,
|
|
192
|
+
* and by the entry it names when the package declares a side per entry. */
|
|
193
|
+
const packageClassification = (
|
|
194
|
+
name: string,
|
|
195
|
+
importer: string,
|
|
196
|
+
specifier: string,
|
|
197
|
+
): Classification => {
|
|
198
|
+
const manifest = installedManifest(dirname(importer), name)
|
|
199
|
+
return manifest === undefined
|
|
200
|
+
? { zone: "library", packageName: name }
|
|
201
|
+
: classifier.classify(manifest, specifier)
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
return {
|
|
@@ -219,10 +221,10 @@ export function viteZoneGuard(options: ViteZoneGuardOptions): ViteZoneGuardPlugi
|
|
|
219
221
|
async resolveId(source, importer, resolveOptions) {
|
|
220
222
|
if (importer === undefined || resolveOptions?.scan === true) return null
|
|
221
223
|
if (!isClient(this, resolveOptions?.ssr)) return null
|
|
222
|
-
const importerFile =
|
|
224
|
+
const importerFile = withoutQuery(importer)
|
|
223
225
|
if (!isAbsolute(importerFile)) return null
|
|
224
226
|
if (source.startsWith(".")) {
|
|
225
|
-
const target = resolve(dirname(importerFile),
|
|
227
|
+
const target = resolve(dirname(importerFile), withoutQuery(source))
|
|
226
228
|
if (isFile(target)) {
|
|
227
229
|
const reason = browserDenial(classifier.classify(target))
|
|
228
230
|
if (reason !== undefined) refuse(this, target, reason, importerFile)
|
|
@@ -231,21 +233,21 @@ export function viteZoneGuard(options: ViteZoneGuardOptions): ViteZoneGuardPlugi
|
|
|
231
233
|
}
|
|
232
234
|
const name = specifierPackage(source)
|
|
233
235
|
if (name !== undefined) {
|
|
234
|
-
const reason = browserDenial(packageClassification(name, importerFile), source)
|
|
236
|
+
const reason = browserDenial(packageClassification(name, importerFile, source), source)
|
|
235
237
|
if (reason !== undefined) refuse(this, source, reason, importerFile)
|
|
236
238
|
}
|
|
237
239
|
// An alias, a tsconfig path or a package export: what the specifier resolves to decides.
|
|
238
240
|
const resolved = await this.resolve(source, importer, { ...resolveOptions, skipSelf: true })
|
|
239
|
-
const target = resolved === null || resolved.external ? undefined :
|
|
241
|
+
const target = resolved === null || resolved.external ? undefined : withoutQuery(resolved.id)
|
|
240
242
|
if (target !== undefined && isAbsolute(target) && isFile(target)) {
|
|
241
|
-
const reason = browserDenial(classifier.classify(target), source)
|
|
243
|
+
const reason = browserDenial(classifier.classify(target, source), source)
|
|
242
244
|
if (reason !== undefined) refuse(this, target, reason, importerFile)
|
|
243
245
|
}
|
|
244
246
|
return null
|
|
245
247
|
},
|
|
246
248
|
transform(code, id, transformOptions) {
|
|
247
249
|
if (!isClient(this, transformOptions?.ssr)) return null
|
|
248
|
-
const file =
|
|
250
|
+
const file = withoutQuery(id)
|
|
249
251
|
if (!isAbsolute(file) || !isFile(file)) return null
|
|
250
252
|
// A fresh transform re-resolves every import, so an earlier refusal is stale until it recurs.
|
|
251
253
|
refusals.delete(resolve(file))
|
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
import { statSync } from "node:fs"
|
|
11
11
|
import { dirname, isAbsolute, relative, resolve } from "node:path"
|
|
12
12
|
import type { BunPlugin } from "bun"
|
|
13
|
+
import { installedManifest } from "../internal/package-entries.ts"
|
|
13
14
|
import { privateEnvDenial } from "../internal/private-env.ts"
|
|
15
|
+
import { withoutQuery } from "../internal/without-query.ts"
|
|
14
16
|
import {
|
|
15
17
|
BACKEND_ONLY_MARKER,
|
|
16
18
|
browserDenial,
|
|
17
19
|
createZoneClassifier,
|
|
20
|
+
specifierPackage,
|
|
18
21
|
type ZoneClassifier,
|
|
19
22
|
} from "../zones.ts"
|
|
20
23
|
|
|
@@ -58,6 +61,40 @@ export interface ZoneGuardOptions {
|
|
|
58
61
|
/** Zones whose code runs in a browser, so may read only public environment variables. */
|
|
59
62
|
const BROWSER_CODE = new Set<string>(["route-frontend", "frontend", "shared"])
|
|
60
63
|
|
|
64
|
+
const loaderFor = (file: string): "ts" | "tsx" | "js" | "jsx" | undefined => {
|
|
65
|
+
const extension = /\.[cm]?([jt]sx?)$/.exec(file)?.[1]
|
|
66
|
+
return extension === "ts" || extension === "tsx" || extension === "js" || extension === "jsx"
|
|
67
|
+
? extension
|
|
68
|
+
: undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Why browser code at `file` may not make one of its package imports: one naming an entry its package
|
|
73
|
+
* declares backend-only, or one its per-entry map leaves out. A path cannot tell two entries that share
|
|
74
|
+
* a file apart, so the dev server judges the specifier here, where it is written.
|
|
75
|
+
*/
|
|
76
|
+
function entryDenial(classifier: ZoneClassifier, file: string, source: string): string | undefined {
|
|
77
|
+
const loader = loaderFor(file)
|
|
78
|
+
if (loader === undefined) return undefined
|
|
79
|
+
let imports: ReadonlyArray<{ readonly path: string }>
|
|
80
|
+
try {
|
|
81
|
+
imports = new Bun.Transpiler({ loader }).scanImports(source)
|
|
82
|
+
} catch {
|
|
83
|
+
// Source Bun cannot read fails its own load with the parse error.
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
for (const { path } of imports) {
|
|
87
|
+
const name = specifierPackage(path)
|
|
88
|
+
const manifest = name === undefined ? undefined : installedManifest(dirname(file), name)
|
|
89
|
+
if (manifest === undefined) continue
|
|
90
|
+
const classification = classifier.classify(manifest, path)
|
|
91
|
+
if (classification.entry === undefined) continue
|
|
92
|
+
const reason = browserDenial(classification, path)
|
|
93
|
+
if (reason !== undefined) return `it imports "${path}": ${reason}`
|
|
94
|
+
}
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
61
98
|
export function zoneGuardPlugin(options: ZoneGuardOptions): BunPlugin {
|
|
62
99
|
const classifier = options.classifier ?? createZoneClassifier(options)
|
|
63
100
|
const display = (file: string): string => {
|
|
@@ -86,14 +123,16 @@ export function zoneGuardPlugin(options: ZoneGuardOptions): BunPlugin {
|
|
|
86
123
|
BROWSER_CODE.has(classification.zone)
|
|
87
124
|
) {
|
|
88
125
|
const source = await Bun.file(args.path).text()
|
|
89
|
-
reason =
|
|
126
|
+
reason =
|
|
127
|
+
privateEnvDenial(args.path, source, options.publicEnvPrefix ?? "PUBLIC_") ??
|
|
128
|
+
entryDenial(classifier, args.path, source)
|
|
90
129
|
}
|
|
91
130
|
return reason === undefined ? undefined : refuse(args.path, reason)
|
|
92
131
|
})
|
|
93
132
|
build.onResolve({ filter: NON_SOURCE_PATH }, (args) => {
|
|
94
133
|
// An entry point (a dev server's HTML page) is the build's own choice, not an import.
|
|
95
134
|
if (args.importer === "") return undefined
|
|
96
|
-
const file = resolve(dirname(args.importer), args.path
|
|
135
|
+
const file = resolve(dirname(args.importer), withoutQuery(args.path))
|
|
97
136
|
// No such file means an extensionless import of a dotted source name (`./date.utils`): Bun resolves
|
|
98
137
|
// it to the source file, which onLoad judges. Judging the bare path would refuse a file never loaded.
|
|
99
138
|
if (!isFile(file)) return undefined
|
package/src/prerender.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* routes are not supported.
|
|
10
10
|
*/
|
|
11
11
|
import { mkdirSync, writeFileSync } from "node:fs"
|
|
12
|
-
import { dirname, resolve, sep } from "node:path"
|
|
12
|
+
import { dirname, join, resolve, sep } from "node:path"
|
|
13
13
|
import { assertNoSecrets, buildEnvironment, type SecretExemption } from "./internal/secret-scan.ts"
|
|
14
14
|
import { fillRoutePattern, type RouteEntry } from "./manifest.ts"
|
|
15
15
|
import { DATA_HEADER, PRERENDERED_GLOBAL, PRERENDERED_LIST_PATH } from "./router.ts"
|
|
@@ -70,10 +70,32 @@ function trimTrailingSlashes(path: string): string {
|
|
|
70
70
|
return path.slice(0, end)
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* One request-path segment as the file name a static host looks up, which is the segment decoded, or
|
|
75
|
+
* `undefined` when no file name maps back to it: malformed percent-encoding, `.` or `..`, or a decoded
|
|
76
|
+
* `/`, `\` or NUL.
|
|
77
|
+
*/
|
|
78
|
+
function fileSegment(segment: string): string | undefined {
|
|
79
|
+
let decoded: string
|
|
80
|
+
try {
|
|
81
|
+
decoded = decodeURIComponent(segment)
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
if (decoded === "." || decoded === ".." || /[/\\\0]/.test(decoded)) return undefined
|
|
86
|
+
return decoded
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Map a route path to its output file: `/` → `index.html`, `/a/b` → `a/b/index.html`. Segments are
|
|
91
|
+
* written decoded (`/caf%C3%A9` → `café/index.html`) because a static host decodes the request path
|
|
92
|
+
* before it looks for a file; a segment with no such file name stays as written.
|
|
93
|
+
*/
|
|
74
94
|
export function htmlFileFor(pattern: string): string {
|
|
75
95
|
const trimmed = trimTrailingSlashes(pattern.replace(/^\/+/, ""))
|
|
76
|
-
|
|
96
|
+
if (trimmed === "") return "index.html"
|
|
97
|
+
const segments = trimmed.split("/").map((segment) => fileSegment(segment) ?? segment)
|
|
98
|
+
return `${segments.join("/")}/index.html`
|
|
77
99
|
}
|
|
78
100
|
|
|
79
101
|
// The handover of a page whose prerendered set is served as a list rather than inlined.
|
|
@@ -119,6 +141,16 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
|
|
|
119
141
|
// static (one path) and dynamic (one path per getStaticPaths entry) routes. A failed render is a skip;
|
|
120
142
|
// a credential in the output fails the whole prerender.
|
|
121
143
|
const renderPath = async (path: string): Promise<void> => {
|
|
144
|
+
// Refused before rendering: a segment no file name maps back to, or a file outside `outDir`.
|
|
145
|
+
const file = htmlFileFor(path)
|
|
146
|
+
const segments = trimTrailingSlashes(path.replace(/^\/+/, "")).split("/")
|
|
147
|
+
const abs = segments.every((s) => s === "" || fileSegment(s) !== undefined)
|
|
148
|
+
? outputPath(outRoot, file)
|
|
149
|
+
: undefined
|
|
150
|
+
if (abs === undefined) {
|
|
151
|
+
skipped.push({ path, reason: "unsafe output path" })
|
|
152
|
+
return
|
|
153
|
+
}
|
|
122
154
|
const res = await options.app.fetch(new Request(`${origin}${path}`))
|
|
123
155
|
if (!res.ok) {
|
|
124
156
|
// A failed render is a per-route skip, not a build failure - surfaced for the caller to log.
|
|
@@ -127,12 +159,6 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
|
|
|
127
159
|
}
|
|
128
160
|
const html = await res.text()
|
|
129
161
|
listUrl ??= PRERENDERED_LIST_URL.exec(html)?.[1]
|
|
130
|
-
const file = htmlFileFor(path)
|
|
131
|
-
const abs = outputPath(outRoot, file)
|
|
132
|
-
if (abs === undefined) {
|
|
133
|
-
skipped.push({ path, reason: "unsafe output path" })
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
162
|
assertClean(file, html)
|
|
137
163
|
mkdirSync(dirname(abs), { recursive: true })
|
|
138
164
|
writeFileSync(abs, html)
|
|
@@ -150,15 +176,10 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
|
|
|
150
176
|
!(dataRes.headers.get("content-type") ?? "").includes("application/x-ndjson")
|
|
151
177
|
) {
|
|
152
178
|
dataFile = dataFileFor(path)
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
} else {
|
|
158
|
-
const data = await dataRes.text()
|
|
159
|
-
assertClean(dataFile, data)
|
|
160
|
-
writeFileSync(dataAbs, data)
|
|
161
|
-
}
|
|
179
|
+
const data = await dataRes.text()
|
|
180
|
+
assertClean(dataFile, data)
|
|
181
|
+
// Beside `index.html`, so inside `outDir` whenever it is.
|
|
182
|
+
writeFileSync(join(dirname(abs), "_data.json"), data)
|
|
162
183
|
} else {
|
|
163
184
|
await dataRes.body?.cancel() // not emitting (non-OK or NDJSON) - release the stream
|
|
164
185
|
}
|
package/src/render-seam.ts
CHANGED
|
@@ -33,6 +33,61 @@ export interface RenderProps {
|
|
|
33
33
|
readonly boundaries?: BoundaryStates
|
|
34
34
|
/** The rendered chain's module ids and `handle` exports, for the adapter's `useMatches`. */
|
|
35
35
|
readonly matchChain?: MatchChain
|
|
36
|
+
/** The page's locale, when the app registers its locale routing (`createWebApp`'s `i18n`). Set on
|
|
37
|
+
* SSR only; the client names it from `path` (see {@link localeOfProps}). */
|
|
38
|
+
readonly locale?: PageLocale
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A served locale as a page renders in it: from the app's locale registry. */
|
|
42
|
+
export interface PageLocale {
|
|
43
|
+
/** The registry key, which is also the URL segment (`ur`). */
|
|
44
|
+
readonly key: string
|
|
45
|
+
/** The BCP-47 tag `<html lang>` and `Content-Language` carry (`ur-PK`). */
|
|
46
|
+
readonly lang: string
|
|
47
|
+
readonly dir: "ltr" | "rtl"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The served locales, handed to the client so it names a page's locale from its path the way the
|
|
51
|
+
* server does. */
|
|
52
|
+
export interface PageLocales {
|
|
53
|
+
/** The default locale's key. */
|
|
54
|
+
readonly default: string
|
|
55
|
+
/** Every served locale, in registry order. */
|
|
56
|
+
readonly locales: readonly PageLocale[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The handover global carrying {@link PageLocales}. */
|
|
60
|
+
export const LOCALES_GLOBAL = "__NIFRA_LOCALES__"
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The locale a path is in, by the rule `@nifrajs/i18n/routing`'s `localeOf` follows: a first path
|
|
64
|
+
* segment that names a served locale, ignoring ASCII case, or else the default.
|
|
65
|
+
*/
|
|
66
|
+
export function pageLocaleIn(config: PageLocales, path: string): PageLocale | undefined {
|
|
67
|
+
// One linear pass: separators, then the segment up to the next separator, query or hash.
|
|
68
|
+
const first = /^[/\\]*([^/\\?#]*)/.exec(path.replace(/[\t\n\r]/g, ""))?.[1]?.toLowerCase()
|
|
69
|
+
return (
|
|
70
|
+
config.locales.find((locale) => first !== "" && locale.key.toLowerCase() === first) ??
|
|
71
|
+
config.locales.find((locale) => locale.key === config.default)
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isPageLocales = (value: unknown): value is PageLocales =>
|
|
76
|
+
typeof value === "object" && value !== null && "locales" in value && Array.isArray(value.locales)
|
|
77
|
+
|
|
78
|
+
/** The locale of the page at `path`, from the served locales the server handed over; `undefined`
|
|
79
|
+
* outside a browser page, or when the app registers no locale routing. */
|
|
80
|
+
export function handoverLocale(path: string): PageLocale | undefined {
|
|
81
|
+
const config: unknown = Reflect.get(globalThis, LOCALES_GLOBAL)
|
|
82
|
+
return isPageLocales(config) ? pageLocaleIn(config, path) : undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The locale of the page `props` render: the server's answer on SSR, else the one the client names
|
|
86
|
+
* from `props.path`. What every adapter's `useLocale` reads. */
|
|
87
|
+
export function localeOfProps(props: RenderProps | undefined): PageLocale | undefined {
|
|
88
|
+
if (props === undefined) return undefined
|
|
89
|
+
if (props.locale !== undefined) return props.locale
|
|
90
|
+
return props.path === undefined ? undefined : handoverLocale(props.path)
|
|
36
91
|
}
|
|
37
92
|
|
|
38
93
|
/**
|
|
@@ -62,10 +117,24 @@ export interface UIMatch {
|
|
|
62
117
|
readonly handle: unknown
|
|
63
118
|
}
|
|
64
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Markup a render produced for outside the app root, which an adapter that sets
|
|
122
|
+
* {@link RenderAdapter.documentParts} appends to. Each part must be complete before the render
|
|
123
|
+
* settles: `renderToString` resolving, or `renderToStream` returning its stream.
|
|
124
|
+
*/
|
|
125
|
+
export interface DocumentParts {
|
|
126
|
+
/** Placed at the end of `<head>`. Svelte's `<svelte:head>` lands here. */
|
|
127
|
+
head: string
|
|
128
|
+
/** Placed first in `<body>`, before the app root. Vue's `<Teleport to="body">` lands here. */
|
|
129
|
+
body: string
|
|
130
|
+
}
|
|
131
|
+
|
|
65
132
|
/** Per-document options for {@link RenderAdapter.renderToStream}. */
|
|
66
133
|
export interface RenderStreamOptions {
|
|
67
134
|
/** The document's CSP nonce, for every executable `<script>` the framework streams. */
|
|
68
135
|
readonly nonce?: string
|
|
136
|
+
/** Present only for an adapter that sets {@link RenderAdapter.documentParts}. */
|
|
137
|
+
readonly parts?: DocumentParts
|
|
69
138
|
}
|
|
70
139
|
|
|
71
140
|
/**
|
|
@@ -84,10 +153,22 @@ export interface RenderAdapter {
|
|
|
84
153
|
props: RenderProps,
|
|
85
154
|
options?: RenderStreamOptions,
|
|
86
155
|
): ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>
|
|
87
|
-
/**
|
|
88
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Render the chain to a complete HTML string when no content is deferred. `options.nonce` is the
|
|
158
|
+
* document's CSP nonce, for any inline script the renderer emits (Solid's hydration data).
|
|
159
|
+
*/
|
|
160
|
+
renderToString?(
|
|
161
|
+
chain: readonly unknown[],
|
|
162
|
+
props: RenderProps,
|
|
163
|
+
options?: RenderStreamOptions,
|
|
164
|
+
): string | Promise<string>
|
|
89
165
|
/** Per-document bootstrap markup injected into `<head>` for client hydration. */
|
|
90
166
|
hydrationHead(nonce?: string): string
|
|
167
|
+
/**
|
|
168
|
+
* Set when a render can produce markup that belongs outside the app root. Every render is then
|
|
169
|
+
* passed `options.parts` to fill; an adapter without it never is, and pays nothing for the seam.
|
|
170
|
+
*/
|
|
171
|
+
readonly documentParts?: boolean
|
|
91
172
|
}
|
|
92
173
|
|
|
93
174
|
/** Loads a module through the dev server's module graph rather than the runtime's resolver. */
|
|
@@ -125,5 +206,10 @@ export const ACTION_GLOBAL = "__NIFRA_ACTION__"
|
|
|
125
206
|
/** Dynamic-boundary states for hydration; absent when a route declares no boundaries. */
|
|
126
207
|
export const BOUNDARY_GLOBAL = "__NIFRA_BOUNDARIES__"
|
|
127
208
|
export const COMPONENT_GLOBAL = "__NIFRA_COMPONENTS__"
|
|
209
|
+
/** Locale key -> copy chunk URL, in a production build that split copy tables by locale. The name
|
|
210
|
+
* `@nifrajs/i18n/copy` reads. */
|
|
211
|
+
export const COPY_CHUNKS_GLOBAL = "__NIFRA_COPY__"
|
|
212
|
+
/** The registry `@nifrajs/i18n/copy` keeps its loaded chunks and definitions in. */
|
|
213
|
+
export const COPY_REALM = "nifra.copy"
|
|
128
214
|
/** Marker attribute used to find a non-default hydration container. */
|
|
129
215
|
export const ROOT_ATTRIBUTE = "data-nifra-root"
|