@wular/pnext 0.0.15 → 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/bin/pnext +3 -1
- package/package.json +4 -1
- package/reference/getting-started.md +1 -1
- package/src/cli/adapters/vercel.ts +47 -15
- package/src/cli/index.ts +21 -1
- package/src/cli/migrate/package-json.ts +21 -1
- package/src/cli/migrate/report.ts +3 -1
- package/src/cli/migrate/run.ts +3 -2
- package/src/cli/migrate/scan.ts +3 -0
- package/src/cli/serve/pipeline.ts +5 -3
- package/src/render/renderer.ts +10 -5
- package/src/runtime/modules.ts +21 -8
- package/src/utils/fs.ts +13 -0
package/bin/pnext
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
|
|
3
|
+
# Presence only. The version floor is enforced in src/cli/index.ts, where
|
|
4
|
+
# Bun.version costs nothing — asking `bun --version` here would fork per command.
|
|
3
5
|
if ! command -v bun >/dev/null 2>&1; then
|
|
4
|
-
echo "pnext requires Bun. Install it from https://bun.sh/get" >&2
|
|
6
|
+
echo "pnext requires Bun >=1.3.6. Install it from https://bun.sh/get" >&2
|
|
5
7
|
exit 1
|
|
6
8
|
fi
|
|
7
9
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wular/pnext",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "A fast little framework for server-first React apps, fully compatible with Next.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"bun": ">=1.3.6"
|
|
38
|
+
},
|
|
36
39
|
"exports": {
|
|
37
40
|
".": "./src/index.ts",
|
|
38
41
|
"./internal": "./src/internal.ts",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
pnext is a Preact framework for file-routed apps. It renders on the server by default and ships client JavaScript only where a component opts in.
|
|
4
4
|
|
|
5
|
-
> pnext requires [Bun](https://bun.sh/get) - make sure it is installed first.
|
|
5
|
+
> pnext requires [Bun](https://bun.sh/get) 1.3.6 or newer - make sure it is installed first.
|
|
6
6
|
|
|
7
7
|
<!-- tabs:start -->
|
|
8
8
|
|
|
@@ -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) {
|
|
@@ -381,9 +402,12 @@ export default Object.assign(handler, { fetch: handler });
|
|
|
381
402
|
`${JSON.stringify(
|
|
382
403
|
{
|
|
383
404
|
// The Bun function runtime (project-level `bunVersion` does not apply
|
|
384
|
-
// to prebuilt Build Output API functions
|
|
385
|
-
//
|
|
386
|
-
|
|
405
|
+
// to prebuilt Build Output API functions, so the version is pinned
|
|
406
|
+
// here). Not `bun1.x`: that resolves to Bun 1.3.14, which segfaults a
|
|
407
|
+
// second into serving and takes the whole function down with SIGABRT.
|
|
408
|
+
// Note: bun functions have a 150 MiB uncompressed size limit vs 250
|
|
409
|
+
// MiB for nodejs.
|
|
410
|
+
runtime: 'bun1.4.x',
|
|
387
411
|
handler: 'index.mjs',
|
|
388
412
|
launcherType: 'Nodejs',
|
|
389
413
|
architecture: FUNCTION_PLATFORM.architecture,
|
|
@@ -463,7 +487,7 @@ async function copyTree(
|
|
|
463
487
|
// Nothing to skip at this level: the whole tree clones in one syscall.
|
|
464
488
|
if (exclude.size === 0 && !existsSync(to)) {
|
|
465
489
|
await mkdir(path.dirname(to), { recursive: true })
|
|
466
|
-
if (clone?.(from, to)) return normalizeClone(to, pack, visited)
|
|
490
|
+
if (clone?.(from, to)) return normalizeClone(to, from, pack, visited)
|
|
467
491
|
}
|
|
468
492
|
// The build output is still live while the walk runs — typegen rewrites
|
|
469
493
|
// `.pnext/types` under it — so a directory can vanish between the parent's
|
|
@@ -477,7 +501,8 @@ async function copyTree(
|
|
|
477
501
|
const target = path.join(to, entry.name)
|
|
478
502
|
// Matched by name before type: a symlinked `node_modules` must be pruned
|
|
479
503
|
// as the directory it points at, not followed.
|
|
480
|
-
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
|
|
481
506
|
if (entry.isSymbolicLink()) {
|
|
482
507
|
const link = resolveLinkTarget(source)
|
|
483
508
|
if (!link) return
|
|
@@ -491,7 +516,7 @@ async function copyTree(
|
|
|
491
516
|
// One syscall for the whole subtree beats one per file — the excluded
|
|
492
517
|
// names are stripped from the copy afterwards.
|
|
493
518
|
if (entry.isDirectory() && clone?.(source, target)) {
|
|
494
|
-
return normalizeClone(target, pack, visited)
|
|
519
|
+
return normalizeClone(target, source, pack, visited)
|
|
495
520
|
}
|
|
496
521
|
if (entry.isDirectory()) return copyTree(source, target, pack, [], visited)
|
|
497
522
|
if (entry.isFile() && shipsFile(pack, entry.name)) return linkOrCopyFile(source, target)
|
|
@@ -552,18 +577,25 @@ async function directoryCloner() {
|
|
|
552
577
|
* both paths emit the same tree. Reading directory entries costs nothing next
|
|
553
578
|
* to the per-file copies this replaces.
|
|
554
579
|
*/
|
|
555
|
-
async function normalizeClone(
|
|
556
|
-
|
|
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]]
|
|
557
589
|
const pending: Promise<unknown>[] = []
|
|
558
590
|
while (stack.length > 0) {
|
|
559
|
-
const current = stack.pop()!
|
|
591
|
+
const [current, currentFrom] = stack.pop()!
|
|
560
592
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
561
593
|
const full = path.join(current, entry.name)
|
|
594
|
+
const source = path.join(currentFrom, entry.name)
|
|
562
595
|
// Matched by name before type, as the walk does: the pages-compat shim
|
|
563
596
|
// links `node_modules` at the app root, and following it would pull the
|
|
564
|
-
// whole store into the function.
|
|
565
|
-
|
|
566
|
-
if (pack.prunedDirs.has(entry.name)) {
|
|
597
|
+
// whole store into the function.
|
|
598
|
+
if (!shipsDir(pack, entry.name, source, entry.isSymbolicLink())) {
|
|
567
599
|
rmSync(full, { recursive: true, force: true })
|
|
568
600
|
} else if (entry.isSymbolicLink()) {
|
|
569
601
|
const link = resolveLinkTarget(full)
|
|
@@ -577,7 +609,7 @@ async function normalizeClone(dir: string, pack: PackRules, visited: Set<string>
|
|
|
577
609
|
pending.push(linkOrCopyFile(link.path, full))
|
|
578
610
|
}
|
|
579
611
|
} else if (entry.isDirectory()) {
|
|
580
|
-
stack.push(full)
|
|
612
|
+
stack.push([full, source])
|
|
581
613
|
} else if (!shipsFile(pack, entry.name)) {
|
|
582
614
|
rmSync(full, { force: true })
|
|
583
615
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -4,9 +4,29 @@ import path from 'node:path'
|
|
|
4
4
|
import { markBoot } from './boot/trace'
|
|
5
5
|
import { commandBinaryName, nameEsbuildProcess, namedBunBinary } from './boot/named-bin'
|
|
6
6
|
|
|
7
|
+
const MIN_BUN = '1.3.6'
|
|
8
|
+
// Not a floor but a hole in one: 1.3.14 segfaults about a second into serving,
|
|
9
|
+
// so a pnext server dies before its first response with no error of its own to
|
|
10
|
+
// show. 1.3.13 and 1.4.x are both fine.
|
|
11
|
+
const BROKEN_BUN = new Set(['1.3.14'])
|
|
12
|
+
|
|
7
13
|
// Reached only if something bypassed bin/pnext (which guards the same way).
|
|
8
14
|
if (typeof Bun === 'undefined') {
|
|
9
|
-
console.error(
|
|
15
|
+
console.error(`pnext requires Bun >=${MIN_BUN}. Install it from https://bun.sh/get`)
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
// A prerelease sorts below its own release, so compare the release: a 1.4.0-canary is a 1.4.0 here.
|
|
19
|
+
const bunVersion = Bun.version.split('-')[0] ?? Bun.version
|
|
20
|
+
if (BROKEN_BUN.has(bunVersion)) {
|
|
21
|
+
console.error(
|
|
22
|
+
`pnext does not support Bun ${Bun.version}: it crashes the process at runtime. ` +
|
|
23
|
+
'Upgrade to latest Bun with `bun upgrade`.',
|
|
24
|
+
)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
} else if (!Bun.semver.satisfies(bunVersion, `>=${MIN_BUN}`)) {
|
|
27
|
+
console.error(
|
|
28
|
+
`pnext requires Bun >=${MIN_BUN}, found ${Bun.version}. Upgrade with \`bun upgrade\`.`,
|
|
29
|
+
)
|
|
10
30
|
process.exit(1)
|
|
11
31
|
}
|
|
12
32
|
|
|
@@ -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
|
}
|
|
@@ -14,6 +14,7 @@ import { loadConfig, pathToFileHref } from '../../config'
|
|
|
14
14
|
import type { ResolvedConfig } from '../../config'
|
|
15
15
|
import { bootstrapCompat } from '../../compat-bootstrap'
|
|
16
16
|
import { registerServerRuntime, serverBundleTargetForRuntime } from '../../runtime/loader'
|
|
17
|
+
import { importModuleOnce } from '../../runtime/modules'
|
|
17
18
|
import { applyProxyResponse, createProxyRunner } from '../../routing/proxy'
|
|
18
19
|
import {
|
|
19
20
|
isPprShellUpgradeEligible,
|
|
@@ -146,7 +147,7 @@ export async function createRequestHandler(
|
|
|
146
147
|
const href = compatModuleMode(config)
|
|
147
148
|
? await moduleHrefForRoute(config, route)
|
|
148
149
|
: pathToFileHref(route.file)
|
|
149
|
-
const module =
|
|
150
|
+
const module = await importModuleOnce<Parameters<typeof handleRouteModule>[0]>(href)
|
|
150
151
|
const rendered = await getRenderExtensions().collectRenderMeta(
|
|
151
152
|
() =>
|
|
152
153
|
withRouteRuntime(route.segmentConfig?.runtime, () =>
|
|
@@ -980,8 +981,9 @@ async function handleRoute(
|
|
|
980
981
|
const href = compatModuleMode(config)
|
|
981
982
|
? await moduleHrefForRoute(config, route)
|
|
982
983
|
: new URL(`file://${route.file}`).href
|
|
983
|
-
const imported =
|
|
984
|
-
Parameters<typeof metadataRouteHandlerModule>[0]
|
|
984
|
+
const imported = await importModuleOnce<
|
|
985
|
+
RouteHandlerModule & Parameters<typeof metadataRouteHandlerModule>[0]
|
|
986
|
+
>(href)
|
|
985
987
|
return (metadataRouteHandlerModule(imported, route) ?? imported) as RouteHandlerModule
|
|
986
988
|
})
|
|
987
989
|
return spans.withRouteHandlerSpan(route.route, () =>
|
package/src/render/renderer.ts
CHANGED
|
@@ -26,7 +26,12 @@ import {
|
|
|
26
26
|
import { clientEntryName } from '../client/chunk-name'
|
|
27
27
|
import { pathToFileHref, type ResolvedConfig } from '../config'
|
|
28
28
|
import { markErrorLogged } from '../utils/error-log'
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
devClientModuleHref,
|
|
31
|
+
devServerModuleHref,
|
|
32
|
+
importDevModule,
|
|
33
|
+
importModuleOnce,
|
|
34
|
+
} from '../runtime/modules'
|
|
30
35
|
import { serverBundleTargetForRuntime } from '../runtime/loader'
|
|
31
36
|
import {
|
|
32
37
|
getActionModuleExtensions,
|
|
@@ -663,8 +668,8 @@ function clientModuleHref(file: string, options: ModuleLoadOptions) {
|
|
|
663
668
|
}
|
|
664
669
|
|
|
665
670
|
async function importModule(file: string, options: ModuleLoadOptions) {
|
|
666
|
-
//
|
|
667
|
-
//
|
|
671
|
+
// Re-throw a module's FIRST evaluation error on every later request rather
|
|
672
|
+
// than the downstream symptom a half-evaluated module produces.
|
|
668
673
|
let module: Record<string, unknown>
|
|
669
674
|
if (options.moduleLoader) module = await options.moduleLoader(file)
|
|
670
675
|
else if (options.dev) {
|
|
@@ -672,7 +677,7 @@ async function importModule(file: string, options: ModuleLoadOptions) {
|
|
|
672
677
|
} else {
|
|
673
678
|
const href = await moduleHref(file, options)
|
|
674
679
|
await drainPreplanBuilds()
|
|
675
|
-
module =
|
|
680
|
+
module = await importModuleOnce<Record<string, unknown>>(href)
|
|
676
681
|
}
|
|
677
682
|
// Tag any 'use server' action module's exports with their wire id so
|
|
678
683
|
// <form action={fn}> can progressively enhance. Compat-owned; a no-op for a
|
|
@@ -687,7 +692,7 @@ async function importClientModule(file: string, options: ModuleLoadOptions) {
|
|
|
687
692
|
const href = await clientModuleHref(file, options)
|
|
688
693
|
if (options.dev) return importDevModule<Record<string, unknown>>(href)
|
|
689
694
|
await drainPreplanBuilds()
|
|
690
|
-
return
|
|
695
|
+
return importModuleOnce<Record<string, unknown>>(href)
|
|
691
696
|
}
|
|
692
697
|
|
|
693
698
|
function clientScriptPath(route: RouteManifestEntry, dev?: boolean) {
|
package/src/runtime/modules.ts
CHANGED
|
@@ -185,21 +185,34 @@ const routeModuleLoaders = new Map<string, Promise<DevRouteModuleLoaders>>()
|
|
|
185
185
|
// and the entry is never consulted again.
|
|
186
186
|
const moduleEvalErrors = new Map<string, unknown>()
|
|
187
187
|
|
|
188
|
-
/**
|
|
189
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Import a compiled module, re-throwing its first evaluation error on later imports.
|
|
190
|
+
*
|
|
191
|
+
* Production needs this as much as dev: a re-import resolves with a half-evaluated namespace whose
|
|
192
|
+
* `export const` bindings are still in TDZ, so the render's next read (`module.metadata`) throws
|
|
193
|
+
* `Cannot access 'metadata' before initialization` and that replaces the real error in the response.
|
|
194
|
+
* Prod hrefs are content-addressed too, so the entry keys the same way dev's does.
|
|
195
|
+
*/
|
|
196
|
+
export async function importModuleOnce<T>(href: string): Promise<T> {
|
|
190
197
|
if (moduleEvalErrors.has(href)) throw moduleEvalErrors.get(href)
|
|
191
|
-
// Pre-planned vendor builds are enqueued without an awaiter; nothing
|
|
192
|
-
// evaluates until they all settle (no-op when the pipeline is empty).
|
|
193
|
-
await drainPreplanBuilds()
|
|
194
198
|
try {
|
|
195
|
-
|
|
196
|
-
noteModuleImported(href)
|
|
197
|
-
return module
|
|
199
|
+
return (await import(href)) as T
|
|
198
200
|
} catch (error) {
|
|
199
201
|
moduleEvalErrors.set(href, error)
|
|
200
202
|
throw error
|
|
201
203
|
}
|
|
202
204
|
}
|
|
205
|
+
|
|
206
|
+
/** `importModuleOnce` plus dev's registry-generation accounting. */
|
|
207
|
+
export async function importDevModule<T>(href: string): Promise<T> {
|
|
208
|
+
if (moduleEvalErrors.has(href)) throw moduleEvalErrors.get(href)
|
|
209
|
+
// Pre-planned vendor builds are enqueued without an awaiter; nothing
|
|
210
|
+
// evaluates until they all settle (no-op when the pipeline is empty).
|
|
211
|
+
await drainPreplanBuilds()
|
|
212
|
+
const module = await importModuleOnce<T>(href)
|
|
213
|
+
noteModuleImported(href)
|
|
214
|
+
return module
|
|
215
|
+
}
|
|
203
216
|
const extensionlessDynamicImportExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'])
|
|
204
217
|
const externalLoadNamespaces = {
|
|
205
218
|
server: 'pnext-route-external-server',
|
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
|
+
}
|