@nifrajs/web 2.14.1 → 3.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/dist/boundary.d.ts +158 -0
- package/dist/boundary.d.ts.map +1 -0
- package/dist/boundary.js +237 -0
- package/dist/boundary.js.map +1 -0
- package/dist/build-plan.d.ts +228 -0
- package/dist/build-plan.d.ts.map +1 -0
- package/dist/build-plan.js +514 -0
- package/dist/build-plan.js.map +1 -0
- package/dist/build-vite.d.ts +2 -1
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +5 -3
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +2 -197
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +92 -515
- package/dist/build.js.map +1 -1
- package/dist/chunk-names.d.ts.map +1 -1
- package/dist/chunk-names.js +3 -2
- package/dist/chunk-names.js.map +1 -1
- package/dist/dev-ssr-graph.d.ts.map +1 -1
- package/dist/dev-ssr-graph.js +63 -28
- package/dist/dev-ssr-graph.js.map +1 -1
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +9 -2
- package/dist/diagnostic.js.map +1 -1
- package/dist/fonts-google.d.ts.map +1 -1
- package/dist/fonts-google.js +3 -1
- package/dist/fonts-google.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +2 -1
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +16 -589
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -1824
- package/dist/index.js.map +1 -1
- package/dist/internal/codegen.d.ts +68 -0
- package/dist/internal/codegen.d.ts.map +1 -0
- package/dist/internal/codegen.js +317 -0
- package/dist/internal/codegen.js.map +1 -0
- package/dist/internal/fetcher-runtime.d.ts +19 -0
- package/dist/internal/fetcher-runtime.d.ts.map +1 -0
- package/dist/internal/fetcher-runtime.js +26 -0
- package/dist/internal/fetcher-runtime.js.map +1 -0
- package/dist/internal/head-merge.d.ts +2 -0
- package/dist/internal/head-merge.d.ts.map +1 -1
- package/dist/internal/head-merge.js +17 -1
- package/dist/internal/head-merge.js.map +1 -1
- package/dist/internal/identity-policy.d.ts +51 -0
- package/dist/internal/identity-policy.d.ts.map +1 -0
- package/dist/internal/identity-policy.js +67 -0
- package/dist/internal/identity-policy.js.map +1 -0
- package/dist/internal/parity.d.ts +104 -8
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +472 -56
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/query-runtime.d.ts +8 -0
- package/dist/internal/query-runtime.d.ts.map +1 -0
- package/dist/internal/query-runtime.js +29 -0
- package/dist/internal/query-runtime.js.map +1 -0
- package/dist/internal/render-document.d.ts +372 -0
- package/dist/internal/render-document.d.ts.map +1 -0
- package/dist/internal/render-document.js +744 -0
- package/dist/internal/render-document.js.map +1 -0
- package/dist/internal/runtime-contract.d.ts +48 -0
- package/dist/internal/runtime-contract.d.ts.map +1 -0
- package/dist/internal/runtime-contract.js +27 -0
- package/dist/internal/runtime-contract.js.map +1 -0
- package/dist/internal/web-app.d.ts +171 -0
- package/dist/internal/web-app.d.ts.map +1 -0
- package/dist/internal/web-app.js +844 -0
- package/dist/internal/web-app.js.map +1 -0
- package/dist/islands.d.ts +50 -2
- package/dist/islands.d.ts.map +1 -1
- package/dist/islands.js +96 -36
- package/dist/islands.js.map +1 -1
- package/dist/manifest.d.ts +41 -6
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts.map +1 -1
- package/dist/module-graph.js +22 -9
- package/dist/module-graph.js.map +1 -1
- package/dist/nano.d.ts +125 -0
- package/dist/nano.d.ts.map +1 -0
- package/dist/nano.js +190 -0
- package/dist/nano.js.map +1 -0
- package/dist/plugins/css-modules.js +2 -2
- package/dist/plugins/css-modules.js.map +1 -1
- package/dist/plugins/kit.d.ts +12 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +62 -12
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/postcss.js +2 -2
- package/dist/plugins/postcss.js.map +1 -1
- package/dist/plugins/scss.js +2 -2
- package/dist/plugins/scss.js.map +1 -1
- package/dist/plugins/stylex.d.ts +70 -0
- package/dist/plugins/stylex.d.ts.map +1 -0
- package/dist/plugins/stylex.js +166 -0
- package/dist/plugins/stylex.js.map +1 -0
- package/dist/plugins/svg.js +2 -2
- package/dist/plugins/svg.js.map +1 -1
- package/dist/render-seam.d.ts +5 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +10 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/router.d.ts +11 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +254 -47
- package/dist/router.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +34 -16
- package/dist/vite.js.map +1 -1
- package/package.json +49 -2
- package/src/boundary.ts +407 -0
- package/src/build-plan.ts +741 -0
- package/src/build-vite.ts +6 -7
- package/src/build.ts +120 -730
- package/src/chunk-names.ts +3 -2
- package/src/dev-ssr-graph.ts +63 -28
- package/src/diagnostic.ts +14 -2
- package/src/fonts-google.ts +3 -1
- package/src/fs.ts +2 -1
- package/src/index.ts +107 -2553
- package/src/internal/codegen.ts +400 -0
- package/src/internal/fetcher-runtime.ts +36 -0
- package/src/internal/head-merge.ts +19 -1
- package/src/internal/identity-policy.ts +94 -0
- package/src/internal/parity.ts +603 -71
- package/src/internal/query-runtime.ts +31 -0
- package/src/internal/render-document.ts +1116 -0
- package/src/internal/runtime-contract.ts +54 -0
- package/src/internal/web-app.ts +1114 -0
- package/src/islands.ts +106 -40
- package/src/manifest.ts +50 -3
- package/src/module-graph.ts +23 -9
- package/src/nano.ts +246 -0
- package/src/plugins/css-modules.ts +2 -2
- package/src/plugins/kit.ts +65 -12
- package/src/plugins/postcss.ts +2 -2
- package/src/plugins/scss.ts +2 -2
- package/src/plugins/stylex.ts +298 -0
- package/src/plugins/svg.ts +2 -2
- package/src/render-seam.ts +6 -0
- package/src/router.ts +293 -51
- package/src/vite.ts +41 -15
package/src/plugins/kit.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* `@nifrajs/web` (imported by the sibling plugin modules); not a public subpath.
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync } from "node:fs"
|
|
8
|
-
import { dirname, relative } from "node:path"
|
|
8
|
+
import { dirname, join, relative } from "node:path"
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
9
10
|
import type { BunPlugin } from "bun"
|
|
10
11
|
|
|
11
12
|
/** The argument Bun passes to a plugin's `setup` - Bun doesn't export the type, so derive it. */
|
|
@@ -23,6 +24,38 @@ export { rewriteSsrImports } from "../dev-ssr-graph.ts"
|
|
|
23
24
|
|
|
24
25
|
const packageRootCache = new Map<string, string>()
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Convert a plugin/runtime path into a path the host filesystem can open.
|
|
29
|
+
*
|
|
30
|
+
* Bun and URL-based test/build APIs can hand a Windows path to a plugin as `/D:/…` (the
|
|
31
|
+
* `pathname` form of a file URL). Node's filesystem APIs correctly treat the native form as
|
|
32
|
+
* `D:\…`, but do not reinterpret that URL pathname. Keep this conversion at the boundary so
|
|
33
|
+
* generated module specifiers can remain slash-normalized without making filesystem reads
|
|
34
|
+
* platform-dependent.
|
|
35
|
+
*/
|
|
36
|
+
export function normalizeFilePath(path: string): string {
|
|
37
|
+
let value = path.split("?", 1)[0] ?? path
|
|
38
|
+
if (value.startsWith("file://")) {
|
|
39
|
+
try {
|
|
40
|
+
value = fileURLToPath(value)
|
|
41
|
+
} catch {
|
|
42
|
+
// Preserve malformed URL-shaped input. Converting it with Windows slash rules would turn
|
|
43
|
+
// `file://%` into a plausible-looking local path (`file:\\%`) and hide the actual bad specifier.
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (process.platform === "win32") {
|
|
48
|
+
if (/^\/[A-Za-z]:[\\/]/.test(value)) value = value.slice(1)
|
|
49
|
+
return value.replaceAll("/", "\\")
|
|
50
|
+
}
|
|
51
|
+
return value
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A stable slash-separated path for generated imports, virtual module IDs, and diagnostics. */
|
|
55
|
+
export function portablePath(path: string): string {
|
|
56
|
+
return normalizeFilePath(path).replaceAll("\\", "/")
|
|
57
|
+
}
|
|
58
|
+
|
|
26
59
|
/** The nearest ancestor directory of `startDir` that holds a `package.json` (the file's package root),
|
|
27
60
|
* cached. Falls back to `startDir` if none is found, so it never throws. */
|
|
28
61
|
function packageRootOf(startDir: string): string {
|
|
@@ -30,7 +63,7 @@ function packageRootOf(startDir: string): string {
|
|
|
30
63
|
if (cached !== undefined) return cached
|
|
31
64
|
let dir = startDir
|
|
32
65
|
for (;;) {
|
|
33
|
-
if (existsSync(
|
|
66
|
+
if (existsSync(join(dir, "package.json"))) break
|
|
34
67
|
const parent = dirname(dir)
|
|
35
68
|
if (parent === dir) {
|
|
36
69
|
dir = startDir // reached the filesystem root with no package.json - anchor on the file's own dir
|
|
@@ -52,8 +85,9 @@ function packageRootOf(startDir: string): string {
|
|
|
52
85
|
* cwd-relative path would silently desync if the SSR server started from a non-project-root cwd.)
|
|
53
86
|
*/
|
|
54
87
|
export function reproduciblePath(absolutePath: string): string {
|
|
55
|
-
const
|
|
56
|
-
|
|
88
|
+
const filePath = normalizeFilePath(absolutePath)
|
|
89
|
+
const root = packageRootOf(dirname(filePath))
|
|
90
|
+
return relative(root, filePath).replaceAll("\\", "/")
|
|
57
91
|
}
|
|
58
92
|
|
|
59
93
|
/**
|
|
@@ -100,7 +134,14 @@ export const DEV_ROUTES_ENV = "NIFRA_DEV_ROUTES"
|
|
|
100
134
|
const under = (path: string, dir: string | undefined): boolean =>
|
|
101
135
|
dir !== undefined &&
|
|
102
136
|
dir !== "" &&
|
|
103
|
-
(
|
|
137
|
+
(() => {
|
|
138
|
+
const normalizedPath = portablePath(path)
|
|
139
|
+
const normalizedDir = portablePath(dir)
|
|
140
|
+
return (
|
|
141
|
+
normalizedPath === normalizedDir ||
|
|
142
|
+
normalizedPath.startsWith(normalizedDir.endsWith("/") ? normalizedDir : `${normalizedDir}/`)
|
|
143
|
+
)
|
|
144
|
+
})()
|
|
104
145
|
|
|
105
146
|
/**
|
|
106
147
|
* Whether a dev compile of `path` may wrap the module in component-level hot-patching - i.e. whether it
|
|
@@ -124,7 +165,7 @@ const under = (path: string, dir: string | undefined): boolean =>
|
|
|
124
165
|
export function devHotComponent(path: string): boolean {
|
|
125
166
|
const root = process.env[DEV_ROOT_ENV]
|
|
126
167
|
if (!under(path, root)) return false
|
|
127
|
-
if (path.includes("/node_modules/")) return false
|
|
168
|
+
if (portablePath(path).includes("/node_modules/")) return false
|
|
128
169
|
return !under(path, process.env[DEV_ROUTES_ENV])
|
|
129
170
|
}
|
|
130
171
|
|
|
@@ -151,17 +192,20 @@ export function createStylesheetEmitter(
|
|
|
151
192
|
const cssByPath = new Map<string, string>()
|
|
152
193
|
// `\?${namespace}$`: namespaces are plain identifiers, so no regex metachars to escape.
|
|
153
194
|
build.onResolve({ filter: new RegExp(`\\?${namespace}$`) }, (args) => ({
|
|
154
|
-
path
|
|
195
|
+
// Bun may hand this hook a native Windows path while the emitter stores portable module IDs.
|
|
196
|
+
// Normalize the path portion but preserve the virtual namespace suffix.
|
|
197
|
+
path: `${portablePath(args.path.slice(0, -suffix.length))}${suffix}`,
|
|
155
198
|
namespace,
|
|
156
199
|
}))
|
|
157
200
|
build.onLoad({ filter: /.*/, namespace }, (args) => ({
|
|
158
|
-
contents: cssByPath.get(args.path.slice(0, -suffix.length)) ?? "",
|
|
201
|
+
contents: cssByPath.get(portablePath(args.path.slice(0, -suffix.length))) ?? "",
|
|
159
202
|
loader: "css",
|
|
160
203
|
}))
|
|
161
204
|
return {
|
|
162
205
|
emit(path, css) {
|
|
163
|
-
|
|
164
|
-
|
|
206
|
+
const filePath = portablePath(path)
|
|
207
|
+
cssByPath.set(filePath, css)
|
|
208
|
+
return `import ${JSON.stringify(filePath + suffix)}\n`
|
|
165
209
|
},
|
|
166
210
|
}
|
|
167
211
|
}
|
|
@@ -175,13 +219,22 @@ export async function requirePeer<T>(
|
|
|
175
219
|
specifier: string,
|
|
176
220
|
hint: { readonly feature: string; readonly install: string },
|
|
177
221
|
): Promise<T> {
|
|
222
|
+
const isPathSpecifier =
|
|
223
|
+
specifier.startsWith("file://") ||
|
|
224
|
+
/^\/[A-Za-z]:[\\/]/.test(specifier) ||
|
|
225
|
+
/^[A-Za-z]:[\\/]/.test(specifier)
|
|
178
226
|
try {
|
|
179
|
-
|
|
227
|
+
// Dynamic import treats a Windows URL pathname (`/D:/…`) as a POSIX root. Convert local path
|
|
228
|
+
// seams to a file URL so the runtime, regardless of platform, receives an unambiguous module ID.
|
|
229
|
+
const importPath = isPathSpecifier
|
|
230
|
+
? pathToFileURL(normalizeFilePath(specifier)).href
|
|
231
|
+
: specifier
|
|
232
|
+
return (await import(importPath)) as T
|
|
180
233
|
} catch (err) {
|
|
181
234
|
// Only a genuine resolution failure means "not installed" - surface anything else (a corrupt
|
|
182
235
|
// install, a native-binding error, a throw at module top-level) as the real error rather than
|
|
183
236
|
// masking it behind a misleading "please install it" hint.
|
|
184
|
-
if ((err as { code?: unknown }).code === "ERR_MODULE_NOT_FOUND") {
|
|
237
|
+
if (!isPathSpecifier && (err as { code?: unknown }).code === "ERR_MODULE_NOT_FOUND") {
|
|
185
238
|
throw new Error(
|
|
186
239
|
`[nifra/web] ${hint.feature} requires the optional peer "${specifier}". Install it: ${hint.install}`,
|
|
187
240
|
)
|
package/src/plugins/postcss.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { BunPlugin } from "bun"
|
|
20
20
|
import { transformCssModule } from "./css-modules.ts"
|
|
21
|
-
import { createStylesheetEmitter, reproduciblePath, requirePeer } from "./kit.ts"
|
|
21
|
+
import { createStylesheetEmitter, normalizeFilePath, reproduciblePath, requirePeer } from "./kit.ts"
|
|
22
22
|
|
|
23
23
|
const STYLE_NS = "nifra-postcss"
|
|
24
24
|
|
|
@@ -93,7 +93,7 @@ export function postcssBunPlugin(
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
build.onLoad({ filter: CSS_FILTER }, async (args) => {
|
|
96
|
-
const path = args.path
|
|
96
|
+
const path = normalizeFilePath(args.path)
|
|
97
97
|
const source = await Bun.file(path).text()
|
|
98
98
|
const postcss = await getProcessor()
|
|
99
99
|
const plugins = await getPlugins()
|
package/src/plugins/scss.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { dirname } from "node:path"
|
|
|
17
17
|
import { pathToFileURL } from "node:url"
|
|
18
18
|
import type { BunPlugin } from "bun"
|
|
19
19
|
import { transformCssModule } from "./css-modules.ts"
|
|
20
|
-
import { createStylesheetEmitter, reproduciblePath, requirePeer } from "./kit.ts"
|
|
20
|
+
import { createStylesheetEmitter, normalizeFilePath, reproduciblePath, requirePeer } from "./kit.ts"
|
|
21
21
|
|
|
22
22
|
const STYLE_NS = "nifra-scss"
|
|
23
23
|
|
|
@@ -65,7 +65,7 @@ export function scssBunPlugin(generate: "dom" | "ssr", options: ScssPluginOption
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
build.onLoad({ filter: /\.s[ac]ss(\?|$)/ }, async (args) => {
|
|
68
|
-
const path = args.path
|
|
68
|
+
const path = normalizeFilePath(args.path)
|
|
69
69
|
const source = await Bun.file(path).text()
|
|
70
70
|
const sass = await getCompiler()
|
|
71
71
|
let css: string
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StyleX integration for Nifra's Bun and Vite pipelines.
|
|
3
|
+
*
|
|
4
|
+
* The StyleX compiler stays an optional peer owned by the StyleX project. This module owns only the
|
|
5
|
+
* Nifra boundary: transform route/component modules in both pipelines, emit client CSS through the
|
|
6
|
+
* existing virtual stylesheet seam, and run the same transform without CSS for SSR.
|
|
7
|
+
*/
|
|
8
|
+
import type { BunPlugin } from "bun"
|
|
9
|
+
import {
|
|
10
|
+
createStylesheetEmitter,
|
|
11
|
+
devServerCompile,
|
|
12
|
+
hash8,
|
|
13
|
+
normalizeFilePath,
|
|
14
|
+
reproduciblePath,
|
|
15
|
+
requirePeer,
|
|
16
|
+
} from "./kit.ts"
|
|
17
|
+
|
|
18
|
+
const STYLE_NS = "nifra-stylex"
|
|
19
|
+
const JS_FILTER = /\.[cm]?[jt]sx?(\?|$)/
|
|
20
|
+
|
|
21
|
+
/** StyleX's module resolution option, kept structural so the compiler remains an optional peer. */
|
|
22
|
+
export interface StylexModuleResolution {
|
|
23
|
+
readonly type: "commonJS" | "haste"
|
|
24
|
+
readonly rootDir: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Structural slice of `@babel/core` used by the adapter. */
|
|
28
|
+
export interface StylexBabelCompiler {
|
|
29
|
+
transformAsync(
|
|
30
|
+
source: string,
|
|
31
|
+
options: Readonly<Record<string, unknown>>,
|
|
32
|
+
): Promise<StylexBabelResult | null | undefined>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Structural slice of the StyleX Babel plugin used by the adapter. */
|
|
36
|
+
export interface StylexCompiler {
|
|
37
|
+
withOptions(options: Readonly<Record<string, unknown>>): unknown
|
|
38
|
+
processStylexRules(rules: readonly unknown[], options: Readonly<Record<string, unknown>>): string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Optional peer injection and compiler configuration shared by Bun and Vite adapters. */
|
|
42
|
+
export interface StylexPluginOptions {
|
|
43
|
+
/** Inject `@babel/core` in tests or advanced integrations. Defaults to the optional peer. */
|
|
44
|
+
readonly babel?: StylexBabelCompiler
|
|
45
|
+
/** Inject `@stylexjs/babel-plugin` in tests or advanced integrations. */
|
|
46
|
+
readonly compiler?: StylexCompiler
|
|
47
|
+
/** Inject Babel syntax plugins. Defaults to the three official syntax peers. */
|
|
48
|
+
readonly syntaxPlugins?: {
|
|
49
|
+
readonly flow: unknown
|
|
50
|
+
readonly jsx: unknown
|
|
51
|
+
readonly typescript: unknown
|
|
52
|
+
}
|
|
53
|
+
/** StyleX import sources to transform. Defaults to `stylex` and `@stylexjs/stylex`. */
|
|
54
|
+
readonly importSources?: readonly string[]
|
|
55
|
+
/** StyleX development transform mode. Defaults to Nifra's dev-server signal. */
|
|
56
|
+
readonly dev?: boolean
|
|
57
|
+
/** StyleX CSS layer output. Defaults to `true` for deterministic precedence. */
|
|
58
|
+
readonly useCSSLayers?: boolean
|
|
59
|
+
/** StyleX's module resolution policy. */
|
|
60
|
+
readonly unstable_moduleResolution?: StylexModuleResolution
|
|
61
|
+
/** Extra Babel plugins and presets merged before the StyleX plugin. */
|
|
62
|
+
readonly babelConfig?: {
|
|
63
|
+
readonly plugins?: readonly unknown[]
|
|
64
|
+
readonly presets?: readonly unknown[]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface StylexBabelResult {
|
|
69
|
+
readonly code?: unknown
|
|
70
|
+
readonly metadata?: unknown
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface StylexTransformedModule {
|
|
74
|
+
readonly code: string
|
|
75
|
+
readonly css: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface StylexDependencies {
|
|
79
|
+
readonly babel: StylexBabelCompiler
|
|
80
|
+
readonly compiler: StylexCompiler
|
|
81
|
+
readonly syntax: Required<StylexPluginOptions>["syntaxPlugins"]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface RecordLike {
|
|
85
|
+
readonly [key: string]: unknown
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isRecord = (value: unknown): value is RecordLike =>
|
|
89
|
+
(typeof value === "object" && value !== null) || typeof value === "function"
|
|
90
|
+
|
|
91
|
+
const unwrapDefault = (value: unknown): unknown => {
|
|
92
|
+
if (!isRecord(value)) return value
|
|
93
|
+
return value.default ?? value
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const isBabelCompiler = (value: unknown): value is StylexBabelCompiler =>
|
|
97
|
+
isRecord(value) && typeof value.transformAsync === "function"
|
|
98
|
+
|
|
99
|
+
const isStylexCompiler = (value: unknown): value is StylexCompiler =>
|
|
100
|
+
isRecord(value) &&
|
|
101
|
+
typeof value.withOptions === "function" &&
|
|
102
|
+
typeof value.processStylexRules === "function"
|
|
103
|
+
|
|
104
|
+
const syntaxPeer = async (specifier: string, feature: string): Promise<unknown> =>
|
|
105
|
+
unwrapDefault(
|
|
106
|
+
await requirePeer<unknown>(specifier, {
|
|
107
|
+
feature,
|
|
108
|
+
install: `bun add -d ${specifier}`,
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const loadBabelCompiler = async (options: StylexPluginOptions): Promise<StylexBabelCompiler> => {
|
|
113
|
+
if (options.babel !== undefined) return options.babel
|
|
114
|
+
const loaded = await requirePeer<unknown>("@babel/core", {
|
|
115
|
+
feature: "StyleX compilation",
|
|
116
|
+
install: "bun add -d @babel/core",
|
|
117
|
+
})
|
|
118
|
+
const compiler = unwrapDefault(loaded)
|
|
119
|
+
if (!isBabelCompiler(compiler)) {
|
|
120
|
+
throw new Error("[nifra/web] the installed @babel/core does not expose transformAsync")
|
|
121
|
+
}
|
|
122
|
+
return compiler
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const loadStylexCompiler = async (options: StylexPluginOptions): Promise<StylexCompiler> => {
|
|
126
|
+
if (options.compiler !== undefined) return options.compiler
|
|
127
|
+
const loaded = await requirePeer<unknown>("@stylexjs/babel-plugin", {
|
|
128
|
+
feature: "StyleX compilation",
|
|
129
|
+
install: "bun add -d @stylexjs/babel-plugin",
|
|
130
|
+
})
|
|
131
|
+
const compiler = unwrapDefault(loaded)
|
|
132
|
+
if (!isStylexCompiler(compiler)) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"[nifra/web] the installed @stylexjs/babel-plugin does not expose the StyleX compiler API",
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
return compiler
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const loadSyntaxPlugins = async (
|
|
141
|
+
options: StylexPluginOptions,
|
|
142
|
+
): Promise<Required<StylexPluginOptions>["syntaxPlugins"]> => {
|
|
143
|
+
if (options.syntaxPlugins !== undefined) return options.syntaxPlugins
|
|
144
|
+
const [flow, jsx, typescript] = await Promise.all([
|
|
145
|
+
syntaxPeer("@babel/plugin-syntax-flow", "StyleX Flow syntax parsing"),
|
|
146
|
+
syntaxPeer("@babel/plugin-syntax-jsx", "StyleX JSX syntax parsing"),
|
|
147
|
+
syntaxPeer("@babel/plugin-syntax-typescript", "StyleX TypeScript syntax parsing"),
|
|
148
|
+
])
|
|
149
|
+
return { flow, jsx, typescript }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
153
|
+
|
|
154
|
+
const containsStylexImport = (source: string, importSources: readonly string[]): boolean =>
|
|
155
|
+
importSources.some((specifier) => {
|
|
156
|
+
const escaped = escapeRegExp(specifier)
|
|
157
|
+
return new RegExp(
|
|
158
|
+
`(?:from\\s*['"]${escaped}['"]|import\\s*\\(\\s*['"]${escaped}['"]\\s*\\)|require\\s*\\(\\s*['"]${escaped}['"]\\s*\\))`,
|
|
159
|
+
"m",
|
|
160
|
+
).test(source)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const loaderFor = (path: string): "js" | "jsx" | "ts" | "tsx" => {
|
|
164
|
+
const clean = path.split("?", 1)[0] ?? path
|
|
165
|
+
if (clean.endsWith(".tsx")) return "tsx"
|
|
166
|
+
if (clean.endsWith(".jsx")) return "jsx"
|
|
167
|
+
if (clean.endsWith(".ts") || clean.endsWith(".mts") || clean.endsWith(".cts")) return "ts"
|
|
168
|
+
return "js"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const moduleResolution = (options: StylexPluginOptions): StylexModuleResolution =>
|
|
172
|
+
options.unstable_moduleResolution ?? { type: "commonJS", rootDir: process.cwd() }
|
|
173
|
+
|
|
174
|
+
const transformModule = async (
|
|
175
|
+
source: string,
|
|
176
|
+
filename: string,
|
|
177
|
+
options: StylexPluginOptions,
|
|
178
|
+
dependencies: Promise<StylexDependencies>,
|
|
179
|
+
): Promise<StylexTransformedModule | undefined> => {
|
|
180
|
+
if (!containsStylexImport(source, options.importSources ?? ["stylex", "@stylexjs/stylex"])) {
|
|
181
|
+
return undefined
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { babel, compiler, syntax } = await dependencies
|
|
185
|
+
const result = await babel.transformAsync(source, {
|
|
186
|
+
babelrc: false,
|
|
187
|
+
filename,
|
|
188
|
+
presets: [...(options.babelConfig?.presets ?? [])],
|
|
189
|
+
plugins: [
|
|
190
|
+
...((options.babelConfig?.plugins ?? []) as readonly unknown[]),
|
|
191
|
+
/\.jsx?$/.test(filename) ? syntax.flow : [syntax.typescript, { isTSX: true }],
|
|
192
|
+
syntax.jsx,
|
|
193
|
+
compiler.withOptions({
|
|
194
|
+
dev: options.dev ?? devServerCompile(),
|
|
195
|
+
importSources: options.importSources ?? ["stylex", "@stylexjs/stylex"],
|
|
196
|
+
treeshakeCompensation: true,
|
|
197
|
+
unstable_moduleResolution: moduleResolution(options),
|
|
198
|
+
}),
|
|
199
|
+
],
|
|
200
|
+
})
|
|
201
|
+
const code = typeof result?.code === "string" ? result.code : undefined
|
|
202
|
+
if (code === undefined) return undefined
|
|
203
|
+
|
|
204
|
+
const metadata = isRecord(result?.metadata) ? result.metadata : undefined
|
|
205
|
+
const rules = Array.isArray(metadata?.stylex) ? metadata.stylex : []
|
|
206
|
+
const css =
|
|
207
|
+
rules.length === 0
|
|
208
|
+
? ""
|
|
209
|
+
: compiler.processStylexRules(rules, {
|
|
210
|
+
useCSSLayers: options.useCSSLayers ?? true,
|
|
211
|
+
})
|
|
212
|
+
return { code, css }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const dependenciesFor = (options: StylexPluginOptions): (() => Promise<StylexDependencies>) => {
|
|
216
|
+
let dependencies: Promise<StylexDependencies> | undefined
|
|
217
|
+
return (): Promise<StylexDependencies> => {
|
|
218
|
+
dependencies ??= Promise.all([
|
|
219
|
+
loadBabelCompiler(options),
|
|
220
|
+
loadStylexCompiler(options),
|
|
221
|
+
loadSyntaxPlugins(options),
|
|
222
|
+
]).then(([babel, compiler, syntax]) => ({ babel, compiler, syntax }))
|
|
223
|
+
return dependencies
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Build a Bun plugin that transforms StyleX and emits CSS for the browser or class maps for SSR. */
|
|
228
|
+
export function stylexBunPlugin(
|
|
229
|
+
generate: "dom" | "ssr",
|
|
230
|
+
options: StylexPluginOptions = {},
|
|
231
|
+
): BunPlugin {
|
|
232
|
+
return {
|
|
233
|
+
name: `nifra-stylex-${generate}`,
|
|
234
|
+
setup(build) {
|
|
235
|
+
const stylesheet = generate === "dom" ? createStylesheetEmitter(build, STYLE_NS) : undefined
|
|
236
|
+
const dependencies = dependenciesFor(options)
|
|
237
|
+
build.onLoad({ filter: JS_FILTER }, async (args) => {
|
|
238
|
+
const path = normalizeFilePath(args.path)
|
|
239
|
+
const source = await Bun.file(path).text()
|
|
240
|
+
const transformed = await transformModule(source, path, options, dependencies())
|
|
241
|
+
if (transformed === undefined) return
|
|
242
|
+
const contents =
|
|
243
|
+
generate === "dom" && stylesheet !== undefined && transformed.css !== ""
|
|
244
|
+
? `${transformed.code}\n${stylesheet.emit(path, transformed.css)}`
|
|
245
|
+
: transformed.code
|
|
246
|
+
return { contents, loader: loaderFor(path) }
|
|
247
|
+
})
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Structural Vite plugin return type. Vite remains an optional peer of `@nifrajs/web`. */
|
|
253
|
+
export interface StylexVitePlugin {
|
|
254
|
+
readonly name: string
|
|
255
|
+
transform(
|
|
256
|
+
source: string,
|
|
257
|
+
id: string,
|
|
258
|
+
): Promise<{ readonly code: string; readonly map?: null } | undefined>
|
|
259
|
+
resolveId(id: string): string | undefined
|
|
260
|
+
load(id: string): string | undefined
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Build a Vite plugin that maps each transformed StyleX module to a virtual CSS asset. */
|
|
264
|
+
export function stylexVitePlugin(
|
|
265
|
+
generate: "dom" | "ssr",
|
|
266
|
+
options: StylexPluginOptions = {},
|
|
267
|
+
): StylexVitePlugin {
|
|
268
|
+
const cssById = new Map<string, string>()
|
|
269
|
+
const dependencies = dependenciesFor(options)
|
|
270
|
+
const virtualIdFor = (id: string): string =>
|
|
271
|
+
`virtual:nifra-stylex/${hash8(reproduciblePath(normalizeFilePath(id)))}.css`
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
name: `nifra-stylex-${generate}`,
|
|
275
|
+
async transform(source, id) {
|
|
276
|
+
const path = normalizeFilePath(id)
|
|
277
|
+
const transformed = await transformModule(source, path, options, dependencies())
|
|
278
|
+
if (transformed === undefined) return undefined
|
|
279
|
+
if (generate === "ssr" || transformed.css === "") return { code: transformed.code, map: null }
|
|
280
|
+
const cssId = virtualIdFor(path)
|
|
281
|
+
cssById.set(cssId, transformed.css)
|
|
282
|
+
return {
|
|
283
|
+
code: `${transformed.code}\nimport ${JSON.stringify(cssId)}`,
|
|
284
|
+
map: null,
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
resolveId(id) {
|
|
288
|
+
return cssById.has(id) ? id : undefined
|
|
289
|
+
},
|
|
290
|
+
load(id) {
|
|
291
|
+
return cssById.get(id)
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Convenience alias for apps that configure only a browser Vite build. */
|
|
297
|
+
export const stylexVite = (options: StylexPluginOptions = {}): StylexVitePlugin =>
|
|
298
|
+
stylexVitePlugin("dom", options)
|
package/src/plugins/svg.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* `style="..."` into an object, and drops XML declarations/comments so the result is valid JSX.
|
|
17
17
|
*/
|
|
18
18
|
import type { BunPlugin } from "bun"
|
|
19
|
-
import { requirePeer } from "./kit.ts"
|
|
19
|
+
import { normalizeFilePath, requirePeer } from "./kit.ts"
|
|
20
20
|
|
|
21
21
|
/** The subset of the `svgo` API this plugin uses (structural, so no hard dependency on its types). */
|
|
22
22
|
export interface SvgOptimizer {
|
|
@@ -168,7 +168,7 @@ export function svgComponentBunPlugin(
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
build.onLoad({ filter: SVG_FILTER }, async (args) => {
|
|
171
|
-
const path = args.path
|
|
171
|
+
const path = normalizeFilePath(args.path)
|
|
172
172
|
let xml = await Bun.file(path).text()
|
|
173
173
|
const svgo = await getOptimizer()
|
|
174
174
|
if (svgo !== undefined) {
|
package/src/render-seam.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* without importing the document renderer through the root module. The singleton is stored on
|
|
7
7
|
* globalThis because the Bun and Vite dev pipelines can evaluate two copies in one process.
|
|
8
8
|
*/
|
|
9
|
+
|
|
10
|
+
import type { BoundaryStates } from "./boundary.ts"
|
|
9
11
|
import type { Submission } from "./router.ts"
|
|
10
12
|
|
|
11
13
|
/** The data handed to a route component. */
|
|
@@ -26,6 +28,8 @@ export interface RenderProps {
|
|
|
26
28
|
readonly path?: string
|
|
27
29
|
/** The route's typed, validated search params. */
|
|
28
30
|
readonly search?: Record<string, unknown>
|
|
31
|
+
/** Neutral named-boundary states; adapters choose how each boundary's `render` UI is mounted. */
|
|
32
|
+
readonly boundaries?: BoundaryStates
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
/**
|
|
@@ -72,5 +76,7 @@ export const ROUTE_GLOBAL = "__NIFRA_ROUTE__"
|
|
|
72
76
|
/** Global the server serializes an action's data return into (absent on GETs); the client reads it
|
|
73
77
|
* so hydration after a native form POST matches the server-rendered markup. */
|
|
74
78
|
export const ACTION_GLOBAL = "__NIFRA_ACTION__"
|
|
79
|
+
/** Dynamic-boundary states for hydration; absent when a route declares no boundaries. */
|
|
80
|
+
export const BOUNDARY_GLOBAL = "__NIFRA_BOUNDARIES__"
|
|
75
81
|
/** Marker attribute used to find a non-default hydration container. */
|
|
76
82
|
export const ROOT_ATTRIBUTE = "data-nifra-root"
|