@wular/pnext 0.0.24 → 0.1.1
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/bin/pnext +1 -1
- package/package.json +2 -2
- package/reference/data/bench.json +398 -223
- package/reference/getting-started.md +1 -1
- package/reference/navigation.md +2 -2
- package/src/api/client-navigation.ts +7 -5
- package/src/cli/adapters/vercel-warm.ts +120 -28
- package/src/cli/build.ts +56 -15
- package/src/cli/create.ts +1 -1
- package/src/cli/dev.ts +2 -1
- package/src/cli/index.ts +146 -10
- package/src/cli/migrate/report.ts +1 -1
- package/src/cli/serve/pipeline.ts +23 -6
- package/src/cli/serve/ui.ts +1 -1
- package/src/cli/start.ts +2 -1
- package/src/cli/typegen.ts +5 -2
- package/src/client/build.ts +217 -58
- package/src/client/chunk-fold.ts +7 -1
- package/src/client/entry.ts +44 -7
- package/src/client/router/runtime.ts +1099 -236
- package/src/client/router/types.ts +15 -0
- package/src/compat/actions/client-plugin.ts +10 -0
- package/src/compat/actions/rewrite.ts +1 -0
- package/src/compat/bundler/optimize-package-imports.ts +375 -28
- package/src/compat/client/navigation-scroll.ts +29 -3
- package/src/compat/client/optimistic-routing.ts +2 -2
- package/src/compat/client/segment-cache-policy.ts +4 -18
- package/src/compat/client/segment-cache.ts +104 -0
- package/src/compat/client/segment-prefetch.ts +6 -6
- package/src/compat/next/config-loader.ts +6 -4
- package/src/compat/next/navigation.ts +16 -2
- package/src/compat/pages/router.ts +102 -9
- package/src/compat/protocol.ts +10 -1
- package/src/compat/register/actions.ts +2 -0
- package/src/compat/register/bundler.ts +6 -6
- package/src/compat/register/routing.ts +3 -4
- package/src/compat/tsconfig-defaults.ts +3 -2
- package/src/config.ts +3 -3
- package/src/css/postcss.ts +450 -17
- package/src/dev/client-actions.ts +2 -0
- package/src/dev/server.ts +87 -14
- package/src/render/island-context.ts +3 -0
- package/src/render/ppr.ts +14 -0
- package/src/render/renderer.ts +273 -43
- package/src/request/context.ts +32 -11
- package/src/resolve/scan-facts.ts +23 -4
- package/src/routing/proxy.ts +29 -25
- package/src/routing/routes.ts +1 -1
- package/src/runtime/loader.ts +47 -1
- package/src/runtime/modules.ts +546 -65
- package/src/runtime/vendor-build.ts +109 -22
- package/src/runtime/vendor.ts +7 -3
- package/src/utils/ansi.ts +3 -1
- package/src/utils/fs.ts +17 -3
package/src/css/postcss.ts
CHANGED
|
@@ -2,8 +2,17 @@
|
|
|
2
2
|
// on the CSS worker thread (./worker.ts) rather than the event loop:
|
|
3
3
|
// Tailwind v4's postcss plugin is synchronous CPU work — its oxide scan and
|
|
4
4
|
// compile starved every concurrent request while it ran in-process.
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
5
6
|
import { createRequire } from 'node:module'
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
statSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from 'node:fs'
|
|
7
16
|
import { writeFile } from 'node:fs/promises'
|
|
8
17
|
import path from 'node:path'
|
|
9
18
|
import { pathToFileURL } from 'node:url'
|
|
@@ -30,10 +39,64 @@ interface PostcssProcessor {
|
|
|
30
39
|
interface LoadedPostcss {
|
|
31
40
|
processor: PostcssProcessor
|
|
32
41
|
sources: string
|
|
42
|
+
partitionedTailwindScan: boolean
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
type PostcssPluginFactory = (options?: unknown) => unknown
|
|
36
46
|
|
|
47
|
+
interface TailwindSource {
|
|
48
|
+
base: string
|
|
49
|
+
pattern: string
|
|
50
|
+
negated: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface TailwindScanner {
|
|
54
|
+
scan(): string[]
|
|
55
|
+
scanFiles(input: TailwindChangedContent[]): string[]
|
|
56
|
+
getCandidatesWithPositions(input: TailwindChangedContent): TailwindCandidatePosition[]
|
|
57
|
+
files: string[]
|
|
58
|
+
scannedFiles: string[]
|
|
59
|
+
globs: TailwindGlob[]
|
|
60
|
+
normalizedSources: TailwindGlob[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface TailwindChangedContent {
|
|
64
|
+
file?: string
|
|
65
|
+
content?: string
|
|
66
|
+
extension: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface TailwindCandidatePosition {
|
|
70
|
+
candidate: string
|
|
71
|
+
position: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface TailwindGlob {
|
|
75
|
+
base: string
|
|
76
|
+
pattern: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type TailwindScannerConstructor = new (options: { sources?: TailwindSource[] }) => TailwindScanner
|
|
80
|
+
|
|
81
|
+
interface TailwindScanRegistration {
|
|
82
|
+
root: string
|
|
83
|
+
cacheDir: string
|
|
84
|
+
versions: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tailwindScannerPatch = Symbol.for('pnext.tailwind-scanner-v1')
|
|
88
|
+
const tailwindCandidateSchema = 1
|
|
89
|
+
|
|
90
|
+
interface TailwindOxideModule {
|
|
91
|
+
Scanner: TailwindScannerConstructor
|
|
92
|
+
[tailwindScannerPatch]?: {
|
|
93
|
+
registrations: TailwindScanRegistration[]
|
|
94
|
+
Scanner: TailwindScannerConstructor
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const warnedTailwindScannerVersions = new Set<string>()
|
|
99
|
+
|
|
37
100
|
interface PostcssConfig {
|
|
38
101
|
plugins?: Record<string, unknown> | unknown[]
|
|
39
102
|
}
|
|
@@ -78,10 +141,10 @@ export async function runPostcss(
|
|
|
78
141
|
const css = await readText(cssFile)
|
|
79
142
|
// The scan directives go last so they don't shift the line numbers plugins
|
|
80
143
|
// report against the real source; `@source` is order-independent to Tailwind.
|
|
81
|
-
const result = await loaded.processor.process(
|
|
82
|
-
from,
|
|
83
|
-
to: cssFile,
|
|
84
|
-
|
|
144
|
+
const result = await loaded.processor.process(
|
|
145
|
+
css + keptExclusions(loaded.sources, css, from, loaded.partitionedTailwindScan),
|
|
146
|
+
{ from, to: cssFile },
|
|
147
|
+
)
|
|
85
148
|
await writeFile(cssFile, result.css)
|
|
86
149
|
}
|
|
87
150
|
|
|
@@ -91,7 +154,11 @@ async function createPostcssProcessor(root: string, options: PostcssOptions) {
|
|
|
91
154
|
const loaded = (await import(pathToFileURL(configFile).href)) as
|
|
92
155
|
PostcssConfig | { default: PostcssConfig }
|
|
93
156
|
const config = 'default' in loaded && loaded.default ? loaded.default : loaded
|
|
94
|
-
const plugins = postcssPlugins(
|
|
157
|
+
const { plugins, partitionedTailwindScan } = postcssPlugins(
|
|
158
|
+
root,
|
|
159
|
+
config as PostcssConfig,
|
|
160
|
+
options,
|
|
161
|
+
)
|
|
95
162
|
if (plugins.length === 0) return undefined
|
|
96
163
|
const appRequire = createRequire(path.join(root, 'package.json'))
|
|
97
164
|
const postcss = interopDefault(resolvePostcss(appRequire)) as (
|
|
@@ -103,6 +170,7 @@ async function createPostcssProcessor(root: string, options: PostcssOptions) {
|
|
|
103
170
|
return {
|
|
104
171
|
processor: postcss(plugins),
|
|
105
172
|
sources: tailwind ? scanExclusions(root, appRequire, options) : '',
|
|
173
|
+
partitionedTailwindScan,
|
|
106
174
|
}
|
|
107
175
|
}
|
|
108
176
|
|
|
@@ -120,23 +188,27 @@ async function createPostcssProcessor(root: string, options: PostcssOptions) {
|
|
|
120
188
|
* stylesheet compiled with or without utilities minted out of pnext's own emitted JS.
|
|
121
189
|
*/
|
|
122
190
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* exclusions that no explicit `@source` in this stylesheet reaches into.
|
|
191
|
+
* The patched scanner partitions explicit dependency sources before applying broad exclusions, so
|
|
192
|
+
* it needs the complete exclusion set. The native fallback cannot do that: remove any broad
|
|
193
|
+
* exclusion that would otherwise override an explicit `@source` and silently drop its utilities.
|
|
127
194
|
*/
|
|
128
|
-
export function keptExclusions(
|
|
129
|
-
|
|
195
|
+
export function keptExclusions(
|
|
196
|
+
exclusions: string,
|
|
197
|
+
css: string,
|
|
198
|
+
fromFile: string,
|
|
199
|
+
partitionedTailwindScan = false,
|
|
200
|
+
): string {
|
|
201
|
+
if (!exclusions || partitionedTailwindScan) return exclusions
|
|
130
202
|
const dir = path.dirname(fromFile)
|
|
131
|
-
const globs = [...css.matchAll(/@source\s+(?!not\s)['"]([^'"]+)['"]/g)].map(
|
|
132
|
-
path.resolve(dir,
|
|
203
|
+
const globs = [...css.matchAll(/@source\s+(?!not\s)['"]([^'"]+)['"]/g)].map(match =>
|
|
204
|
+
path.resolve(dir, match[1]!.replace(/[*{[].*$/, '')),
|
|
133
205
|
)
|
|
134
206
|
if (globs.length === 0) return exclusions
|
|
135
207
|
const kept = exclusions
|
|
136
208
|
.split('\n')
|
|
137
209
|
.filter(line => {
|
|
138
|
-
const
|
|
139
|
-
return !
|
|
210
|
+
const match = /@source not "([^"]+)"/.exec(line)
|
|
211
|
+
return !match || !globs.some(glob => glob === match[1] || glob.startsWith(match[1] + '/'))
|
|
140
212
|
})
|
|
141
213
|
.join('\n')
|
|
142
214
|
return kept.includes('@source not') ? kept : ''
|
|
@@ -188,6 +260,7 @@ function postcssPlugins(root: string, config: PostcssConfig, options: PostcssOpt
|
|
|
188
260
|
const entries = pluginEntries(config)
|
|
189
261
|
|
|
190
262
|
const plugins: unknown[] = []
|
|
263
|
+
let partitionedTailwindScan = false
|
|
191
264
|
for (const [specifier, pluginOptions] of entries) {
|
|
192
265
|
if (pluginOptions === false) continue
|
|
193
266
|
if (typeof specifier !== 'string') {
|
|
@@ -195,13 +268,373 @@ function postcssPlugins(root: string, config: PostcssConfig, options: PostcssOpt
|
|
|
195
268
|
if (specifier) plugins.push(specifier)
|
|
196
269
|
continue
|
|
197
270
|
}
|
|
271
|
+
const patchedScanner =
|
|
272
|
+
specifier === '@tailwindcss/postcss'
|
|
273
|
+
? configureTailwindScanner(appRequire, root, options)
|
|
274
|
+
: undefined
|
|
198
275
|
const factory = interopDefault(appRequire(specifier) as unknown)
|
|
276
|
+
if (patchedScanner) {
|
|
277
|
+
assertTailwindScannerPatch(appRequire, patchedScanner)
|
|
278
|
+
partitionedTailwindScan = true
|
|
279
|
+
}
|
|
199
280
|
const resolvedOptions = pluginOptionsFor(root, specifier, pluginOptions, options)
|
|
200
281
|
plugins.push(
|
|
201
282
|
typeof factory === 'function' ? (factory as PostcssPluginFactory)(resolvedOptions) : factory,
|
|
202
283
|
)
|
|
203
284
|
}
|
|
204
|
-
return plugins
|
|
285
|
+
return { plugins, partitionedTailwindScan }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function configureTailwindScanner(
|
|
289
|
+
appRequire: NodeJS.Require,
|
|
290
|
+
root: string,
|
|
291
|
+
options: PostcssOptions,
|
|
292
|
+
) {
|
|
293
|
+
const pluginPackage = appRequire.resolve('@tailwindcss/postcss/package.json')
|
|
294
|
+
const pluginRequire = createRequire(pluginPackage)
|
|
295
|
+
const oxide = pluginRequire('@tailwindcss/oxide') as TailwindOxideModule
|
|
296
|
+
const versions = ['@tailwindcss/postcss', 'tailwindcss', '@tailwindcss/oxide'].map(name => ({
|
|
297
|
+
name,
|
|
298
|
+
version: packageVersionText(pluginRequire, name),
|
|
299
|
+
tuple: packageVersion(pluginRequire, name),
|
|
300
|
+
}))
|
|
301
|
+
// The monkey-patch and source partitioning contract are covered against Tailwind 4.3.x.
|
|
302
|
+
if (!versions.every(({ tuple: [major, minor] }) => major === 4 && minor === 3)) {
|
|
303
|
+
const label = versions.map(({ name, version }) => `${name}@${version}`).join(', ')
|
|
304
|
+
if (!warnedTailwindScannerVersions.has(label)) {
|
|
305
|
+
warnedTailwindScannerVersions.add(label)
|
|
306
|
+
console.warn(`PNext: unsupported Tailwind scanner versions (${label}); using native Scanner.`)
|
|
307
|
+
}
|
|
308
|
+
return undefined
|
|
309
|
+
}
|
|
310
|
+
const registration = {
|
|
311
|
+
root: path.resolve(root),
|
|
312
|
+
cacheDir: path.join(
|
|
313
|
+
path.resolve(root, options.outDir ?? '.pnext'),
|
|
314
|
+
'cache/tailwind/candidates/v1',
|
|
315
|
+
),
|
|
316
|
+
versions: ['@tailwindcss/postcss', 'tailwindcss', '@tailwindcss/oxide', 'postcss']
|
|
317
|
+
.map(name => `${name}@${packageVersionText(pluginRequire, name)}`)
|
|
318
|
+
.join('\0'),
|
|
319
|
+
}
|
|
320
|
+
const installed = oxide[tailwindScannerPatch]
|
|
321
|
+
if (installed) {
|
|
322
|
+
if (oxide.Scanner !== installed.Scanner) throw tailwindScannerPatchError()
|
|
323
|
+
const index = installed.registrations.findIndex(item => item.root === registration.root)
|
|
324
|
+
if (index === -1) installed.registrations.push(registration)
|
|
325
|
+
else installed.registrations[index] = registration
|
|
326
|
+
return installed.Scanner
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const NativeScanner = oxide.Scanner
|
|
330
|
+
const registrations = [registration]
|
|
331
|
+
class PnextTailwindScanner implements TailwindScanner {
|
|
332
|
+
private readonly scanners: {
|
|
333
|
+
scanner: TailwindScanner
|
|
334
|
+
inventory: TailwindScanner
|
|
335
|
+
sources: TailwindSource[]
|
|
336
|
+
}[]
|
|
337
|
+
private readonly registration?: TailwindScanRegistration
|
|
338
|
+
|
|
339
|
+
constructor({ sources = [] }: { sources?: TailwindSource[] }) {
|
|
340
|
+
this.registration = tailwindRegistration(registrations, sources)
|
|
341
|
+
this.scanners = partitionTailwindSources(sources).map(partition => ({
|
|
342
|
+
scanner: new NativeScanner({ sources: partition }),
|
|
343
|
+
inventory: new NativeScanner({ sources: partition }),
|
|
344
|
+
sources: partition,
|
|
345
|
+
}))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
scan() {
|
|
349
|
+
return sortedUnique(
|
|
350
|
+
this.scanners.flatMap(({ scanner, inventory, sources }) =>
|
|
351
|
+
this.registration
|
|
352
|
+
? cachedTailwindCandidates(inventory, sources, this.registration, () =>
|
|
353
|
+
new NativeScanner({ sources }).scan(),
|
|
354
|
+
)
|
|
355
|
+
: scanner.scan(),
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
scanFiles(input: TailwindChangedContent[]) {
|
|
361
|
+
return sortedUnique(this.scanners.flatMap(({ scanner }) => scanner.scanFiles(input)))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
getCandidatesWithPositions(input: TailwindChangedContent) {
|
|
365
|
+
return this.scanners.flatMap(({ scanner }) => scanner.getCandidatesWithPositions(input))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
get files() {
|
|
369
|
+
return sortedUnique(this.scanners.flatMap(({ inventory }) => inventory.files))
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
get scannedFiles() {
|
|
373
|
+
return sortedUnique(this.scanners.flatMap(({ scanner }) => scanner.scannedFiles))
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
get globs() {
|
|
377
|
+
return uniqueTailwindGlobs(this.scanners.flatMap(({ scanner }) => scanner.globs))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
get normalizedSources() {
|
|
381
|
+
return uniqueTailwindGlobs(this.scanners.flatMap(({ scanner }) => scanner.normalizedSources))
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
oxide.Scanner = PnextTailwindScanner
|
|
386
|
+
oxide[tailwindScannerPatch] = { registrations, Scanner: PnextTailwindScanner }
|
|
387
|
+
return PnextTailwindScanner
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function assertTailwindScannerPatch(
|
|
391
|
+
appRequire: NodeJS.Require,
|
|
392
|
+
expected: TailwindScannerConstructor,
|
|
393
|
+
) {
|
|
394
|
+
const pluginPackage = appRequire.resolve('@tailwindcss/postcss/package.json')
|
|
395
|
+
const oxide = createRequire(pluginPackage)('@tailwindcss/oxide') as TailwindOxideModule
|
|
396
|
+
if (oxide.Scanner !== expected || oxide[tailwindScannerPatch]?.Scanner !== expected) {
|
|
397
|
+
throw tailwindScannerPatchError()
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function tailwindScannerPatchError() {
|
|
402
|
+
return new Error(
|
|
403
|
+
'PNext could not install its Tailwind Scanner integration. The loaded @tailwindcss/postcss module does not observe the patched @tailwindcss/oxide Scanner; refusing to drop explicit @source utilities.',
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function partitionTailwindSources(sources: TailwindSource[]): TailwindSource[][] {
|
|
408
|
+
const broadExclusions = sources.filter(source => source.negated && !/[*?{[]/.test(source.pattern))
|
|
409
|
+
const dependencySources = sources.filter(
|
|
410
|
+
source =>
|
|
411
|
+
!source.negated &&
|
|
412
|
+
broadExclusions.some(exclusion => isInside(sourcePrefix(exclusion), sourcePrefix(source))),
|
|
413
|
+
)
|
|
414
|
+
if (dependencySources.length === 0) return [sources]
|
|
415
|
+
const dependencySet = new Set(dependencySources)
|
|
416
|
+
const appSources = sources.filter(source => !dependencySet.has(source))
|
|
417
|
+
return [appSources].concat(
|
|
418
|
+
dependencySources.map(source =>
|
|
419
|
+
[
|
|
420
|
+
source,
|
|
421
|
+
...sources.filter(
|
|
422
|
+
exclusion =>
|
|
423
|
+
exclusion.negated &&
|
|
424
|
+
!broadExclusions.includes(exclusion) &&
|
|
425
|
+
pathsOverlap(sourcePrefix(source), sourcePrefix(exclusion)),
|
|
426
|
+
),
|
|
427
|
+
].map(resolveTailwindSource),
|
|
428
|
+
),
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function resolveTailwindSource(source: TailwindSource): TailwindSource {
|
|
433
|
+
const globAt = source.pattern.search(/[*?{[]/)
|
|
434
|
+
const literal = (globAt === -1 ? source.pattern : source.pattern.slice(0, globAt)).replace(
|
|
435
|
+
/[\\/]$/,
|
|
436
|
+
'',
|
|
437
|
+
)
|
|
438
|
+
const absolutePattern = path.resolve(source.base, source.pattern)
|
|
439
|
+
let absolute = path.resolve(source.base, literal || '.')
|
|
440
|
+
while (!existsSync(absolute)) {
|
|
441
|
+
const parent = path.dirname(absolute)
|
|
442
|
+
if (parent === absolute) return source
|
|
443
|
+
absolute = parent
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
const stat = statSync(absolute)
|
|
447
|
+
if (globAt === -1 && stat.isFile()) {
|
|
448
|
+
return {
|
|
449
|
+
...source,
|
|
450
|
+
base: realpathSync.native(path.dirname(absolute)),
|
|
451
|
+
pattern: path.basename(absolute),
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
...source,
|
|
456
|
+
base: realpathSync.native(absolute),
|
|
457
|
+
pattern:
|
|
458
|
+
globAt === -1 ? '**/*' : path.relative(absolute, absolutePattern).split(path.sep).join('/'),
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
return source
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function sourcePrefix(source: TailwindSource) {
|
|
466
|
+
const prefix = source.pattern.replace(/[*?{[].*$/, '').replace(/[\\/]$/, '') || '.'
|
|
467
|
+
return path.resolve(source.base, prefix)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function pathsOverlap(left: string, right: string) {
|
|
471
|
+
return isInside(left, right) || isInside(right, left)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function isInside(parent: string, child: string) {
|
|
475
|
+
const relative = path.relative(parent, child)
|
|
476
|
+
return (
|
|
477
|
+
relative === '' ||
|
|
478
|
+
(!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function tailwindRegistration(
|
|
483
|
+
registrations: TailwindScanRegistration[],
|
|
484
|
+
sources: TailwindSource[],
|
|
485
|
+
) {
|
|
486
|
+
return [...registrations]
|
|
487
|
+
.sort((left, right) => right.root.length - left.root.length)
|
|
488
|
+
.find(item => sources.some(source => isInside(item.root, path.resolve(source.base))))
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function cachedTailwindCandidates(
|
|
492
|
+
inventory: TailwindScanner,
|
|
493
|
+
sources: TailwindSource[],
|
|
494
|
+
registration: TailwindScanRegistration,
|
|
495
|
+
scan: () => string[],
|
|
496
|
+
) {
|
|
497
|
+
let cache: { file: string; fingerprint: string; files: number }
|
|
498
|
+
try {
|
|
499
|
+
cache = tailwindCacheIdentity(inventory, sources, registration)
|
|
500
|
+
} catch {
|
|
501
|
+
return scan()
|
|
502
|
+
}
|
|
503
|
+
const stored = readJson<{
|
|
504
|
+
schema?: number
|
|
505
|
+
fingerprint?: string
|
|
506
|
+
candidates?: unknown
|
|
507
|
+
}>(cache.file)
|
|
508
|
+
if (
|
|
509
|
+
stored?.schema === tailwindCandidateSchema &&
|
|
510
|
+
stored.fingerprint === cache.fingerprint &&
|
|
511
|
+
Array.isArray(stored.candidates) &&
|
|
512
|
+
stored.candidates.every(candidate => typeof candidate === 'string')
|
|
513
|
+
) {
|
|
514
|
+
return stored.candidates
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const candidates = scan()
|
|
518
|
+
try {
|
|
519
|
+
mkdirSync(registration.cacheDir, { recursive: true })
|
|
520
|
+
atomicJsonWrite(cache.file, {
|
|
521
|
+
schema: tailwindCandidateSchema,
|
|
522
|
+
fingerprint: cache.fingerprint,
|
|
523
|
+
candidates,
|
|
524
|
+
files: cache.files,
|
|
525
|
+
})
|
|
526
|
+
} catch {
|
|
527
|
+
// Cache writes are optional; the fresh Oxide result remains correct.
|
|
528
|
+
}
|
|
529
|
+
return candidates
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function tailwindCacheIdentity(
|
|
533
|
+
scanner: TailwindScanner,
|
|
534
|
+
sources: TailwindSource[],
|
|
535
|
+
registration: TailwindScanRegistration,
|
|
536
|
+
) {
|
|
537
|
+
const identities = sources.map(tailwindSourceIdentity)
|
|
538
|
+
const sourceKey = createHash('sha256')
|
|
539
|
+
.update(String(tailwindCandidateSchema))
|
|
540
|
+
.update('\0')
|
|
541
|
+
.update(registration.versions)
|
|
542
|
+
.update('\0')
|
|
543
|
+
.update(JSON.stringify(identities))
|
|
544
|
+
.digest('hex')
|
|
545
|
+
const files = scanner.files.map(file => path.resolve(file)).sort()
|
|
546
|
+
const fingerprint = createHash('sha256').update(sourceKey)
|
|
547
|
+
for (const file of files) {
|
|
548
|
+
fingerprint.update('\0').update(file).update('\0').update(readFileSync(file))
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
file: path.join(registration.cacheDir, `${sourceKey}.json`),
|
|
552
|
+
fingerprint: fingerprint.digest('hex'),
|
|
553
|
+
files: files.length,
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function tailwindSourceIdentity(source: TailwindSource) {
|
|
558
|
+
const prefix = sourcePrefix(source)
|
|
559
|
+
const real = realExistingPath(prefix)
|
|
560
|
+
const packageFile = nearestPackageJson(real)
|
|
561
|
+
let packageIdentity: { name?: string; version?: string } | undefined
|
|
562
|
+
if (packageFile) {
|
|
563
|
+
const pkg = readJson<{ name?: string; version?: string }>(packageFile)
|
|
564
|
+
if (pkg) {
|
|
565
|
+
packageIdentity = { name: pkg.name, version: pkg.version }
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return { ...source, base: path.resolve(source.base), real, package: packageIdentity }
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function realExistingPath(input: string) {
|
|
572
|
+
let current = input
|
|
573
|
+
while (!existsSync(current)) {
|
|
574
|
+
const parent = path.dirname(current)
|
|
575
|
+
if (parent === current) return input
|
|
576
|
+
current = parent
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
const stat = statSync(current)
|
|
580
|
+
return realpathSync.native(stat.isFile() ? path.dirname(current) : current)
|
|
581
|
+
} catch {
|
|
582
|
+
return current
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function nearestPackageJson(input: string) {
|
|
587
|
+
let current = input
|
|
588
|
+
while (true) {
|
|
589
|
+
const file = path.join(current, 'package.json')
|
|
590
|
+
if (existsSync(file)) return file
|
|
591
|
+
const parent = path.dirname(current)
|
|
592
|
+
if (parent === current) break
|
|
593
|
+
current = parent
|
|
594
|
+
}
|
|
595
|
+
return undefined
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function atomicJsonWrite(file: string, value: unknown) {
|
|
599
|
+
const temporary = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
600
|
+
writeFileSync(temporary, JSON.stringify(value))
|
|
601
|
+
renameSync(temporary, file)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function readJson<T>(file: string): T | undefined {
|
|
605
|
+
try {
|
|
606
|
+
return JSON.parse(readFileSync(file, 'utf8')) as T
|
|
607
|
+
} catch {
|
|
608
|
+
return undefined
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function sortedUnique(values: string[]) {
|
|
613
|
+
return [...new Set(values)].sort()
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function uniqueTailwindGlobs(globs: TailwindGlob[]) {
|
|
617
|
+
const seen = new Set<string>()
|
|
618
|
+
return globs.filter(glob => {
|
|
619
|
+
const key = `${glob.base}\0${glob.pattern}`
|
|
620
|
+
if (seen.has(key)) return false
|
|
621
|
+
seen.add(key)
|
|
622
|
+
return true
|
|
623
|
+
})
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function packageVersionText(req: NodeJS.Require, name: string) {
|
|
627
|
+
try {
|
|
628
|
+
return (
|
|
629
|
+
(
|
|
630
|
+
JSON.parse(readFileSync(req.resolve(`${name}/package.json`), 'utf8')) as {
|
|
631
|
+
version?: string
|
|
632
|
+
}
|
|
633
|
+
).version ?? 'unknown'
|
|
634
|
+
)
|
|
635
|
+
} catch {
|
|
636
|
+
return 'unknown'
|
|
637
|
+
}
|
|
205
638
|
}
|
|
206
639
|
|
|
207
640
|
function pluginOptionsFor(
|
|
@@ -20,6 +20,8 @@ export interface ClientActionBundler {
|
|
|
20
20
|
isClientActionModule: (file: string) => boolean
|
|
21
21
|
/** Generate the client fetch-stub source for an action module file. */
|
|
22
22
|
stubSource: (file: string) => Promise<string>
|
|
23
|
+
/** Read a client-layer source and replace an undiscovered action module with its RPC stub. */
|
|
24
|
+
loadClientSource: (file: string, root: string) => Promise<{ source: string; stubSource?: string }>
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
let bundler: ClientActionBundler | undefined
|