@wular/pnext 0.0.16 → 0.0.17
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/package.json
CHANGED
|
@@ -116,8 +116,29 @@ function packRules(config: ResolvedConfig): PackRules {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* A keep path and everything under it. Prefix, not equality: the compiled module cache mirrors the
|
|
121
|
+
* source layout, so a dependency's artifacts land in `<outDir>/cache/server/<profile>/node_modules/
|
|
122
|
+
* ...`. That segment names a source tree, not a package store, and pruning it by name ships the
|
|
123
|
+
* importer without the module it imports — the function then 500s on the first request that reaches
|
|
124
|
+
* it. Only the app's own build output is exempt; a real `node_modules` beside it still prunes.
|
|
125
|
+
*/
|
|
126
|
+
function withinKeepPath(pack: PackRules, source: string) {
|
|
127
|
+
const resolved = path.resolve(source)
|
|
128
|
+
for (const keep of pack.keepPaths) {
|
|
129
|
+
if (resolved === keep || resolved.startsWith(`${keep}${path.sep}`)) return true
|
|
130
|
+
}
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `isLink` is load-bearing, not a detail: the exemption covers real directories the build output
|
|
136
|
+
* owns, never a link out of it. The proxy shim links `node_modules` into `.pnext`, and following
|
|
137
|
+
* that would pull the whole store into the function.
|
|
138
|
+
*/
|
|
139
|
+
function shipsDir(pack: PackRules, name: string, source: string, isLink = false) {
|
|
140
|
+
if (!pack.prunedDirs.has(name)) return true
|
|
141
|
+
return !isLink && withinKeepPath(pack, source)
|
|
121
142
|
}
|
|
122
143
|
|
|
123
144
|
function shipsFile(pack: PackRules, name: string) {
|
|
@@ -466,7 +487,7 @@ async function copyTree(
|
|
|
466
487
|
// Nothing to skip at this level: the whole tree clones in one syscall.
|
|
467
488
|
if (exclude.size === 0 && !existsSync(to)) {
|
|
468
489
|
await mkdir(path.dirname(to), { recursive: true })
|
|
469
|
-
if (clone?.(from, to)) return normalizeClone(to, pack, visited)
|
|
490
|
+
if (clone?.(from, to)) return normalizeClone(to, from, pack, visited)
|
|
470
491
|
}
|
|
471
492
|
// The build output is still live while the walk runs — typegen rewrites
|
|
472
493
|
// `.pnext/types` under it — so a directory can vanish between the parent's
|
|
@@ -480,7 +501,8 @@ async function copyTree(
|
|
|
480
501
|
const target = path.join(to, entry.name)
|
|
481
502
|
// Matched by name before type: a symlinked `node_modules` must be pruned
|
|
482
503
|
// as the directory it points at, not followed.
|
|
483
|
-
if (exclude.has(entry.name) || !shipsDir(pack, entry.name, source))
|
|
504
|
+
if (exclude.has(entry.name) || !shipsDir(pack, entry.name, source, entry.isSymbolicLink()))
|
|
505
|
+
return
|
|
484
506
|
if (entry.isSymbolicLink()) {
|
|
485
507
|
const link = resolveLinkTarget(source)
|
|
486
508
|
if (!link) return
|
|
@@ -494,7 +516,7 @@ async function copyTree(
|
|
|
494
516
|
// One syscall for the whole subtree beats one per file — the excluded
|
|
495
517
|
// names are stripped from the copy afterwards.
|
|
496
518
|
if (entry.isDirectory() && clone?.(source, target)) {
|
|
497
|
-
return normalizeClone(target, pack, visited)
|
|
519
|
+
return normalizeClone(target, source, pack, visited)
|
|
498
520
|
}
|
|
499
521
|
if (entry.isDirectory()) return copyTree(source, target, pack, [], visited)
|
|
500
522
|
if (entry.isFile() && shipsFile(pack, entry.name)) return linkOrCopyFile(source, target)
|
|
@@ -555,18 +577,25 @@ async function directoryCloner() {
|
|
|
555
577
|
* both paths emit the same tree. Reading directory entries costs nothing next
|
|
556
578
|
* to the per-file copies this replaces.
|
|
557
579
|
*/
|
|
558
|
-
async function normalizeClone(
|
|
559
|
-
|
|
580
|
+
async function normalizeClone(
|
|
581
|
+
dir: string,
|
|
582
|
+
// Where `dir` was cloned from. Carried so the keep-path exemption reads the same here as in the
|
|
583
|
+
// walk: the clone's own paths are inside the function and would never match a source keep path.
|
|
584
|
+
from: string,
|
|
585
|
+
pack: PackRules,
|
|
586
|
+
visited: Set<string>,
|
|
587
|
+
) {
|
|
588
|
+
const stack: [string, string][] = [[dir, from]]
|
|
560
589
|
const pending: Promise<unknown>[] = []
|
|
561
590
|
while (stack.length > 0) {
|
|
562
|
-
const current = stack.pop()!
|
|
591
|
+
const [current, currentFrom] = stack.pop()!
|
|
563
592
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
564
593
|
const full = path.join(current, entry.name)
|
|
594
|
+
const source = path.join(currentFrom, entry.name)
|
|
565
595
|
// Matched by name before type, as the walk does: the pages-compat shim
|
|
566
596
|
// links `node_modules` at the app root, and following it would pull the
|
|
567
|
-
// whole store into the function.
|
|
568
|
-
|
|
569
|
-
if (pack.prunedDirs.has(entry.name)) {
|
|
597
|
+
// whole store into the function.
|
|
598
|
+
if (!shipsDir(pack, entry.name, source, entry.isSymbolicLink())) {
|
|
570
599
|
rmSync(full, { recursive: true, force: true })
|
|
571
600
|
} else if (entry.isSymbolicLink()) {
|
|
572
601
|
const link = resolveLinkTarget(full)
|
|
@@ -580,7 +609,7 @@ async function normalizeClone(dir: string, pack: PackRules, visited: Set<string>
|
|
|
580
609
|
pending.push(linkOrCopyFile(link.path, full))
|
|
581
610
|
}
|
|
582
611
|
} else if (entry.isDirectory()) {
|
|
583
|
-
stack.push(full)
|
|
612
|
+
stack.push([full, source])
|
|
584
613
|
} else if (!shipsFile(pack, entry.name)) {
|
|
585
614
|
rmSync(full, { force: true })
|
|
586
615
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
5
5
|
import path from 'node:path'
|
|
6
|
-
import { pnextVersionRange } from '../../utils/fs'
|
|
6
|
+
import { pnextOptionalDependencyRange, pnextVersionRange } from '../../utils/fs'
|
|
7
7
|
import type { MigrationResult } from './report'
|
|
8
8
|
|
|
9
9
|
const SEPARATORS = new Set(['&&', '||', ';', '|', '&'])
|
|
@@ -132,6 +132,26 @@ export async function migratePackageJson(
|
|
|
132
132
|
details: [`devDependencies: @wular/pnext ${versionRange}`],
|
|
133
133
|
})
|
|
134
134
|
}
|
|
135
|
+
// next/font/google resolves its metadata from `next-font`, one of pnext's optional dependencies.
|
|
136
|
+
// A hoisted store makes it resolve locally whether or not the app declares it, so an undeclared
|
|
137
|
+
// one only shows up as a 500 from a deployed function, where the closure ships what the app asks
|
|
138
|
+
// for. Declare it here, where the migration already knows the app uses the loader.
|
|
139
|
+
const fontRange = pnextOptionalDependencyRange('next-font')
|
|
140
|
+
if (
|
|
141
|
+
result.googleFontImports.length > 0 &&
|
|
142
|
+
fontRange &&
|
|
143
|
+
!('next-font' in dependencies) &&
|
|
144
|
+
!('next-font' in devDependencies)
|
|
145
|
+
) {
|
|
146
|
+
dependencies['next-font'] = fontRange
|
|
147
|
+
result.edits.push({
|
|
148
|
+
file: 'package.json',
|
|
149
|
+
description: 'next-font added',
|
|
150
|
+
details: [
|
|
151
|
+
`dependencies: next-font ${fontRange} (next/font/google metadata, used by ${result.googleFontImports.length} file${result.googleFontImports.length === 1 ? '' : 's'})`,
|
|
152
|
+
],
|
|
153
|
+
})
|
|
154
|
+
}
|
|
135
155
|
if (!('preact' in dependencies) && !('preact' in devDependencies)) {
|
|
136
156
|
dependencies.preact = '^10'
|
|
137
157
|
result.edits.push({
|
|
@@ -27,10 +27,12 @@ export interface MigrationResult {
|
|
|
27
27
|
reports: ReportItem[]
|
|
28
28
|
/** Files reading cookies()/headers()/draftMode() synchronously (Next 14 style). */
|
|
29
29
|
syncRequestApis: string[]
|
|
30
|
+
/** Files importing next/font/google, which needs the `next-font` metadata package. */
|
|
31
|
+
googleFontImports: string[]
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function emptyResult(): MigrationResult {
|
|
33
|
-
return { edits: [], reports: [], syncRequestApis: [] }
|
|
35
|
+
return { edits: [], reports: [], syncRequestApis: [], googleFontImports: [] }
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export function printHeader(root: string, dryRun: boolean) {
|
package/src/cli/migrate/run.ts
CHANGED
|
@@ -36,9 +36,10 @@ export async function migrateApp(directory: string | undefined, options: { dryRu
|
|
|
36
36
|
|
|
37
37
|
printHeader(root, options.dryRun)
|
|
38
38
|
const result = emptyResult()
|
|
39
|
-
|
|
40
|
-
//
|
|
39
|
+
// The scan runs first: what it finds decides what the config enables (sync request APIs) and
|
|
40
|
+
// which dependencies the app needs (next/font/google → next-font).
|
|
41
41
|
await withSpinner('Scanning sources ...', () => scanSources(root, result))
|
|
42
|
+
if (pkg) await migratePackageJson(root, pkg, result, options.dryRun)
|
|
42
43
|
await createPnextConfig(root, result, options.dryRun)
|
|
43
44
|
await migrateTsconfig(root, result, options.dryRun)
|
|
44
45
|
await removeNextEnv(root, result, options.dryRun)
|
package/src/cli/migrate/scan.ts
CHANGED
|
@@ -53,6 +53,9 @@ export async function scanSources(root: string, result: MigrationResult) {
|
|
|
53
53
|
streaming.push(relative)
|
|
54
54
|
}
|
|
55
55
|
if (specifier === 'next/head') headImports.push(relative)
|
|
56
|
+
// next/font/local reads the app's own files; only the Google loader needs the metadata
|
|
57
|
+
// package, so only it earns the dependency.
|
|
58
|
+
if (specifier === 'next/font/google') result.googleFontImports.push(relative)
|
|
56
59
|
}
|
|
57
60
|
if (readsRequestApiSynchronously(text)) result.syncRequestApis.push(relative)
|
|
58
61
|
}
|
package/src/utils/fs.ts
CHANGED
|
@@ -137,3 +137,16 @@ export function pnextVersionRange() {
|
|
|
137
137
|
const { version } = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version: string }
|
|
138
138
|
return `^${version}`
|
|
139
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The range pnext declares for one of its optional dependencies. An app that uses the feature has to
|
|
143
|
+
* declare the package itself — the vercel adapter ships pnext's optional deps only where the app
|
|
144
|
+
* depends on them too — so the range has to come from here rather than being written down twice.
|
|
145
|
+
*/
|
|
146
|
+
export function pnextOptionalDependencyRange(name: string) {
|
|
147
|
+
const pkgPath = fileURLToPath(new URL('../../package.json', import.meta.url))
|
|
148
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as {
|
|
149
|
+
optionalDependencies?: Record<string, string>
|
|
150
|
+
}
|
|
151
|
+
return pkg.optionalDependencies?.[name]
|
|
152
|
+
}
|