@napi-rs/cli 3.7.4 → 3.8.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/README.md +33 -0
- package/cli.mjs +9 -12
- package/dist/cli.js +9249 -1312
- package/dist/index.cjs +9269 -1332
- package/dist/index.d.cts +175 -157
- package/dist/index.d.ts +175 -157
- package/dist/index.js +9249 -1312
- package/docs/artifacts.md +33 -0
- package/docs/build.md +58 -0
- package/docs/create-npm-dirs.md +32 -0
- package/docs/new.md +39 -0
- package/docs/pre-publish.md +37 -0
- package/docs/rename.md +37 -0
- package/docs/universalize.md +31 -0
- package/docs/version.md +31 -0
- package/docs/wasi.md +161 -0
- package/package.json +11 -10
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +1019 -114
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/artifacts.spec.ts +17 -0
- package/src/api/__tests__/create-npm-dirs.spec.ts +60 -15
- package/src/api/__tests__/new.spec.ts +41 -5
- package/src/api/artifacts.ts +934 -108
- package/src/api/build.ts +1317 -188
- package/src/api/create-npm-dirs.ts +475 -102
- package/src/api/new.ts +354 -35
- package/src/api/pre-publish.ts +2845 -99
- package/src/api/rename.ts +1638 -124
- package/src/api/templates/js-binding.ts +216 -38
- package/src/api/templates/load-wasi-template.ts +1641 -111
- package/src/api/templates/wasi-worker-template.ts +32 -3
- package/src/commands/rename.ts +8 -3
- package/src/def/artifacts.ts +2 -2
- package/src/utils/__tests__/__snapshots__/target.spec.ts.md +2 -2
- package/src/utils/__tests__/__snapshots__/target.spec.ts.snap +0 -0
- package/src/utils/__tests__/misc.spec.ts +124 -3
- package/src/utils/__tests__/version.spec.ts +47 -1
- package/src/utils/config.ts +15 -2
- package/src/utils/metadata.ts +155 -47
- package/src/utils/misc.ts +6079 -0
- package/src/utils/target.ts +66 -8
- package/src/utils/typegen.ts +1223 -63
- package/src/utils/version.ts +96 -0
package/src/api/pre-publish.ts
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import {
|
|
4
|
+
constants,
|
|
5
|
+
existsSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
statSync,
|
|
11
|
+
} from 'node:fs'
|
|
12
|
+
import { createRequire } from 'node:module'
|
|
13
|
+
import {
|
|
14
|
+
chmod,
|
|
15
|
+
copyFile,
|
|
16
|
+
cp,
|
|
17
|
+
lstat,
|
|
18
|
+
mkdtemp,
|
|
19
|
+
readdir,
|
|
20
|
+
rm,
|
|
21
|
+
symlink,
|
|
22
|
+
} from 'node:fs/promises'
|
|
23
|
+
import { tmpdir } from 'node:os'
|
|
24
|
+
import {
|
|
25
|
+
dirname,
|
|
26
|
+
extname,
|
|
27
|
+
isAbsolute,
|
|
28
|
+
join,
|
|
29
|
+
relative,
|
|
30
|
+
resolve,
|
|
31
|
+
sep,
|
|
32
|
+
} from 'node:path'
|
|
33
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
4
34
|
|
|
5
35
|
import { Octokit } from '@octokit/rest'
|
|
36
|
+
import { Range } from 'semver'
|
|
6
37
|
|
|
7
38
|
import {
|
|
8
39
|
applyDefaultPrePublishOptions,
|
|
@@ -11,13 +42,78 @@ import {
|
|
|
11
42
|
import {
|
|
12
43
|
readFileAsync,
|
|
13
44
|
readNapiConfig,
|
|
45
|
+
collectRelativeDeclarationSpecifiers,
|
|
46
|
+
commitFileSystemTransaction,
|
|
47
|
+
createWasmModuleTypeDef,
|
|
48
|
+
copyFileAtomic,
|
|
14
49
|
debugFactory,
|
|
15
|
-
|
|
50
|
+
MINIMUM_WASI_NODE_VERSION,
|
|
51
|
+
mkdirAsync,
|
|
52
|
+
restrictWasiNodeEngine,
|
|
53
|
+
resolvePackageReconciliationPaths,
|
|
54
|
+
wasiLoaderSuffix,
|
|
55
|
+
wasiTargetHasThreads,
|
|
56
|
+
writeFileAtomic,
|
|
57
|
+
withFileSystemReconciliation,
|
|
58
|
+
AVAILABLE_TARGETS,
|
|
59
|
+
parseTriple,
|
|
60
|
+
type CommonPackageJsonFields,
|
|
61
|
+
type FileSystemTransactionWrite,
|
|
62
|
+
type Target,
|
|
16
63
|
} from '../utils/index.js'
|
|
17
64
|
|
|
18
|
-
import { version } from './version.js'
|
|
19
|
-
|
|
20
65
|
const debug = debugFactory('pre-publish')
|
|
66
|
+
const THREADLESS_WASI_ROOT_SUBPATHS = new Set([
|
|
67
|
+
'./workerd',
|
|
68
|
+
'./wasm',
|
|
69
|
+
'./wasm.wasm',
|
|
70
|
+
])
|
|
71
|
+
const MANAGED_OPTIONAL_DEPENDENCY_SUFFIXES = new Set(
|
|
72
|
+
AVAILABLE_TARGETS.map((target) => parseTriple(target).platformArchABI),
|
|
73
|
+
)
|
|
74
|
+
const LEGACY_DEEP_IMPORT_EXTENSIONS = ['.js', '.json', '.node']
|
|
75
|
+
const DECLARATION_EXTENSIONS = ['.d.ts', '.d.cts', '.d.mts']
|
|
76
|
+
const RECONCILIATION_STATE_PREFIXES = [
|
|
77
|
+
'.napi-rs-filesystem-reconciliation',
|
|
78
|
+
'.napi-rs-filesystem-transaction',
|
|
79
|
+
]
|
|
80
|
+
const PUBLICATION_CONTEXT_EXCLUDED_NAMES = new Set(['.git'])
|
|
81
|
+
const PUBLICATION_CONTEXT_FILES = [
|
|
82
|
+
'.npmrc',
|
|
83
|
+
'.pnp.cjs',
|
|
84
|
+
'.pnp.loader.mjs',
|
|
85
|
+
'.pnpmfile.cjs',
|
|
86
|
+
'.yarnrc',
|
|
87
|
+
'.yarnrc.yml',
|
|
88
|
+
'bun.lock',
|
|
89
|
+
'bun.lockb',
|
|
90
|
+
'npm-shrinkwrap.json',
|
|
91
|
+
'package-lock.json',
|
|
92
|
+
'package.json',
|
|
93
|
+
'pnpm-lock.yaml',
|
|
94
|
+
'pnpm-workspace.yaml',
|
|
95
|
+
'yarn.lock',
|
|
96
|
+
]
|
|
97
|
+
const PUBLICATION_LIFECYCLE_SCRIPTS = [
|
|
98
|
+
'prepublish',
|
|
99
|
+
'prepare',
|
|
100
|
+
'prepublishOnly',
|
|
101
|
+
'prepack',
|
|
102
|
+
'postpack',
|
|
103
|
+
'publish',
|
|
104
|
+
'postpublish',
|
|
105
|
+
]
|
|
106
|
+
const WASI_ROOT_FACADE_MARKER_PREFIX = '// napi-rs-wasi-root-facade:'
|
|
107
|
+
const directBufferDependency = '^6.0.3'
|
|
108
|
+
const wasiRuntimeDependencies = [
|
|
109
|
+
'@napi-rs/wasm-runtime',
|
|
110
|
+
'@emnapi/core',
|
|
111
|
+
'@emnapi/runtime',
|
|
112
|
+
'buffer',
|
|
113
|
+
]
|
|
114
|
+
const require = createRequire(import.meta.url)
|
|
115
|
+
type TypeScriptModule = typeof import('typescript')
|
|
116
|
+
let loadedTypeScript: TypeScriptModule | undefined
|
|
21
117
|
|
|
22
118
|
interface PackageInfo {
|
|
23
119
|
name: string
|
|
@@ -25,19 +121,347 @@ interface PackageInfo {
|
|
|
25
121
|
tag: string
|
|
26
122
|
}
|
|
27
123
|
|
|
124
|
+
interface PreparedReleasePackage {
|
|
125
|
+
artifactPath: string
|
|
126
|
+
filename: string
|
|
127
|
+
packageDir: string
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface PreparedPrePublish {
|
|
131
|
+
npmClient: string
|
|
132
|
+
packageName: string
|
|
133
|
+
packageVersion: string
|
|
134
|
+
releasePackages: PreparedReleasePackage[]
|
|
135
|
+
workspaceRoot?: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function removePreparedSnapshot(
|
|
139
|
+
snapshotRoot: string,
|
|
140
|
+
phase: 'preparation' | 'publication',
|
|
141
|
+
primaryFailed: boolean,
|
|
142
|
+
primaryError?: unknown,
|
|
143
|
+
) {
|
|
144
|
+
try {
|
|
145
|
+
await removeOwnedTemporaryTree(snapshotRoot)
|
|
146
|
+
} catch (cleanupError) {
|
|
147
|
+
if (primaryFailed) {
|
|
148
|
+
throw new AggregateError(
|
|
149
|
+
[primaryError, cleanupError],
|
|
150
|
+
`Pre-publish ${phase} failed and its prepared snapshot could not be removed from ${snapshotRoot}`,
|
|
151
|
+
{ cause: primaryError },
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
debug.warn(
|
|
155
|
+
`Pre-publish ${phase} completed, but its prepared snapshot could not be removed from ${snapshotRoot}: ${String(cleanupError)}`,
|
|
156
|
+
)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
if (primaryFailed) {
|
|
160
|
+
throw primaryError
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function removeOwnedTemporaryTree(root: string) {
|
|
165
|
+
try {
|
|
166
|
+
await rm(root, { recursive: true, force: true })
|
|
167
|
+
return
|
|
168
|
+
} catch (initialError) {
|
|
169
|
+
if (
|
|
170
|
+
!['EACCES', 'EPERM'].includes(
|
|
171
|
+
(initialError as NodeJS.ErrnoException).code ?? '',
|
|
172
|
+
)
|
|
173
|
+
) {
|
|
174
|
+
throw initialError
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
await makeOwnedTemporaryDirectoriesWritable(root)
|
|
178
|
+
await rm(root, { recursive: true, force: true })
|
|
179
|
+
return
|
|
180
|
+
} catch (retryError) {
|
|
181
|
+
throw new AggregateError(
|
|
182
|
+
[initialError, retryError],
|
|
183
|
+
`Prepared snapshot could not be removed from ${root} after restoring directory permissions`,
|
|
184
|
+
{ cause: initialError },
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function makeOwnedTemporaryDirectoriesWritable(root: string) {
|
|
191
|
+
let stats
|
|
192
|
+
try {
|
|
193
|
+
stats = await lstat(root)
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
|
196
|
+
throw error
|
|
197
|
+
}
|
|
198
|
+
if (!stats.isDirectory()) {
|
|
199
|
+
throw new Error(`Prepared snapshot path is not a directory: ${root}`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await chmod(root, stats.mode | 0o700)
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
|
206
|
+
throw error
|
|
207
|
+
}
|
|
208
|
+
let entries
|
|
209
|
+
try {
|
|
210
|
+
entries = await readdir(root, { withFileTypes: true })
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
|
213
|
+
throw error
|
|
214
|
+
}
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
if (entry.isDirectory()) {
|
|
217
|
+
await makeOwnedTemporaryDirectoriesWritable(join(root, entry.name))
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function copyOwnedTemporaryPath(source: string, destination: string) {
|
|
223
|
+
await cp(source, destination, { recursive: true })
|
|
224
|
+
const stats = await lstat(destination)
|
|
225
|
+
if (stats.isDirectory()) {
|
|
226
|
+
await makeOwnedTemporaryDirectoriesWritable(destination)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
28
230
|
export async function prePublish(userOptions: PrePublishOptions) {
|
|
29
231
|
debug('Receive pre-publish options:')
|
|
30
232
|
debug(' %O', userOptions)
|
|
31
233
|
|
|
32
234
|
const options = applyDefaultPrePublishOptions(userOptions)
|
|
33
235
|
|
|
34
|
-
const
|
|
236
|
+
const resolvedPaths = resolvePackageReconciliationPaths(
|
|
237
|
+
options.cwd,
|
|
238
|
+
options.packageJsonPath,
|
|
239
|
+
[options.npmDir],
|
|
240
|
+
)
|
|
241
|
+
const {
|
|
242
|
+
boundary: reconciliationRoot,
|
|
243
|
+
packageJsonPath,
|
|
244
|
+
packageRoot: rootDir,
|
|
245
|
+
} = resolvedPaths
|
|
246
|
+
const npmDir = resolvedPaths.managedPaths[0]
|
|
247
|
+
let preparedSnapshotRoot: string | undefined
|
|
248
|
+
let prepared: PreparedPrePublish
|
|
249
|
+
try {
|
|
250
|
+
prepared = await withFileSystemReconciliation(
|
|
251
|
+
reconciliationRoot,
|
|
252
|
+
async () => {
|
|
253
|
+
const {
|
|
254
|
+
packageJson,
|
|
255
|
+
targets,
|
|
256
|
+
packageName,
|
|
257
|
+
binaryName,
|
|
258
|
+
npmClient,
|
|
259
|
+
wasm,
|
|
260
|
+
} = await readNapiConfig(
|
|
261
|
+
packageJsonPath,
|
|
262
|
+
options.configPath
|
|
263
|
+
? resolve(options.cwd, options.configPath)
|
|
264
|
+
: undefined,
|
|
265
|
+
)
|
|
266
|
+
const threadlessWasiTarget = targets.find(
|
|
267
|
+
(target) =>
|
|
268
|
+
target.platform === 'wasi' && !wasiTargetHasThreads(target),
|
|
269
|
+
)
|
|
35
270
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
271
|
+
let preparedWorkspaceRoot: string | undefined
|
|
272
|
+
if (!options.dryRun) {
|
|
273
|
+
preparedSnapshotRoot = await mkdtemp(
|
|
274
|
+
join(tmpdir(), 'napi-rs-pre-publish-stage-'),
|
|
275
|
+
)
|
|
276
|
+
preparedWorkspaceRoot = join(preparedSnapshotRoot, 'workspace')
|
|
277
|
+
}
|
|
278
|
+
const releasePackagePlans: ReleasePackageMaterializationPlan[] = []
|
|
279
|
+
for (const target of targets) {
|
|
280
|
+
const pkgDir = join(npmDir, target.platformArchABI)
|
|
281
|
+
const validationOptions: ReleasePackageValidationOptions = {
|
|
282
|
+
pkgDir,
|
|
283
|
+
rootDir,
|
|
284
|
+
packageName,
|
|
285
|
+
binaryName,
|
|
286
|
+
target,
|
|
287
|
+
requireDirectBufferDependency:
|
|
288
|
+
wasm?.browser?.buffer === true &&
|
|
289
|
+
(wasm.browser.fs !== true || !wasiTargetHasThreads(target)),
|
|
290
|
+
}
|
|
291
|
+
releasePackagePlans.push(
|
|
292
|
+
preparedWorkspaceRoot
|
|
293
|
+
? await stageReleasePackage(
|
|
294
|
+
preparedWorkspaceRoot,
|
|
295
|
+
reconciliationRoot,
|
|
296
|
+
packageJson.version,
|
|
297
|
+
validationOptions,
|
|
298
|
+
)
|
|
299
|
+
: await validateReleasePackage(validationOptions),
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
const rootFacadeReconciliation = reconcileThreadlessWasiRootFacade(
|
|
303
|
+
packageJson,
|
|
304
|
+
rootDir,
|
|
305
|
+
npmDir,
|
|
306
|
+
packageName,
|
|
307
|
+
)
|
|
308
|
+
let reconciledPackageJson = rootFacadeReconciliation.packageJson
|
|
309
|
+
let rootFacade: ThreadlessWasiRootFacade | undefined
|
|
310
|
+
if (threadlessWasiTarget) {
|
|
311
|
+
rootFacade = planThreadlessWasiRootFacade({
|
|
312
|
+
packageJsonPath,
|
|
313
|
+
packageJson: reconciledPackageJson,
|
|
314
|
+
packageName,
|
|
315
|
+
binaryName,
|
|
316
|
+
target: threadlessWasiTarget,
|
|
317
|
+
npmDir,
|
|
318
|
+
managedGeneratedFiles: rootFacadeReconciliation.staleGeneratedFiles,
|
|
319
|
+
})
|
|
320
|
+
const stagedThreadlessPackage = releasePackagePlans.find(
|
|
321
|
+
(plan) =>
|
|
322
|
+
plan.target.platformArchABI ===
|
|
323
|
+
threadlessWasiTarget.platformArchABI,
|
|
324
|
+
)?.stagedPkgDir
|
|
325
|
+
if (stagedThreadlessPackage) {
|
|
326
|
+
const stagedWasmSourcePath = join(
|
|
327
|
+
stagedThreadlessPackage,
|
|
328
|
+
`${binaryName}.${threadlessWasiTarget.platformArchABI}.wasm`,
|
|
329
|
+
)
|
|
330
|
+
rootFacade = {
|
|
331
|
+
...rootFacade,
|
|
332
|
+
marker: createThreadlessWasiRootFacadeMarker(
|
|
333
|
+
`${packageName}-${threadlessWasiTarget.platformArchABI}`,
|
|
334
|
+
readFileSync(stagedWasmSourcePath),
|
|
335
|
+
),
|
|
336
|
+
wasmSourcePath: stagedWasmSourcePath,
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
reconciledPackageJson = applyThreadlessWasiRootFacade(
|
|
340
|
+
reconciledPackageJson,
|
|
341
|
+
rootFacade,
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
const optionalDependencies = {
|
|
345
|
+
...asRecord(packageJson.optionalDependencies),
|
|
346
|
+
}
|
|
347
|
+
const managedPackageNames = new Set([packageName])
|
|
348
|
+
for (const flavorPackage of rootFacadeReconciliation.managedFlavorPackages) {
|
|
349
|
+
for (const suffix of MANAGED_OPTIONAL_DEPENDENCY_SUFFIXES) {
|
|
350
|
+
const ending = `-${suffix}`
|
|
351
|
+
if (flavorPackage.endsWith(ending)) {
|
|
352
|
+
managedPackageNames.add(flavorPackage.slice(0, -ending.length))
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const managedPackageName of managedPackageNames) {
|
|
357
|
+
for (const suffix of MANAGED_OPTIONAL_DEPENDENCY_SUFFIXES) {
|
|
358
|
+
delete optionalDependencies[`${managedPackageName}-${suffix}`]
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (const target of targets) {
|
|
362
|
+
optionalDependencies[`${packageName}-${target.platformArchABI}`] =
|
|
363
|
+
packageJson.version
|
|
364
|
+
}
|
|
365
|
+
const nodeEngine =
|
|
366
|
+
targets.length > 0 &&
|
|
367
|
+
targets.every((target) => target.platform === 'wasi')
|
|
368
|
+
? restrictWasiNodeEngine(
|
|
369
|
+
packageJson.engines?.node ?? MINIMUM_WASI_NODE_VERSION,
|
|
370
|
+
)
|
|
371
|
+
: undefined
|
|
372
|
+
const rootReleasePlan: RootReleaseMaterializationPlan = {
|
|
373
|
+
packageJson: reconciledPackageJson,
|
|
374
|
+
optionalDependencies,
|
|
375
|
+
nodeEngine,
|
|
376
|
+
facade: rootFacade,
|
|
377
|
+
staleGeneratedFiles: rootFacadeReconciliation.staleGeneratedFiles,
|
|
378
|
+
}
|
|
379
|
+
if (preparedWorkspaceRoot) {
|
|
380
|
+
const stagedTransactionRoot = join(
|
|
381
|
+
preparedSnapshotRoot!,
|
|
382
|
+
'transaction-root',
|
|
383
|
+
)
|
|
384
|
+
const stagedRootDir = publicationContextPath(
|
|
385
|
+
preparedWorkspaceRoot,
|
|
386
|
+
reconciliationRoot,
|
|
387
|
+
rootDir,
|
|
388
|
+
)
|
|
389
|
+
await stageRootReleasePlan(
|
|
390
|
+
packageJsonPath,
|
|
391
|
+
rootDir,
|
|
392
|
+
stagedRootDir,
|
|
393
|
+
rootReleasePlan,
|
|
394
|
+
)
|
|
395
|
+
await mkdirAsync(stagedTransactionRoot, { recursive: true })
|
|
396
|
+
const stagedRootManifestPath = join(
|
|
397
|
+
stagedTransactionRoot,
|
|
398
|
+
'package.json',
|
|
399
|
+
)
|
|
400
|
+
await copyFile(
|
|
401
|
+
join(stagedRootDir, 'package.json'),
|
|
402
|
+
stagedRootManifestPath,
|
|
403
|
+
constants.COPYFILE_FICLONE,
|
|
404
|
+
)
|
|
405
|
+
if (releasePackagePlans.length > 0) {
|
|
406
|
+
await stagePublicationContext(
|
|
407
|
+
reconciliationRoot,
|
|
408
|
+
preparedWorkspaceRoot,
|
|
409
|
+
rootDir,
|
|
410
|
+
releasePackagePlans,
|
|
411
|
+
)
|
|
412
|
+
await registerPublicationWorkspaces(
|
|
413
|
+
reconciliationRoot,
|
|
414
|
+
preparedWorkspaceRoot,
|
|
415
|
+
releasePackagePlans.map(({ pkgDir }) => pkgDir),
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
await commitPrePublishFileSystemTransaction({
|
|
419
|
+
packageJsonPath,
|
|
420
|
+
transactionRoot: reconciliationRoot,
|
|
421
|
+
releasePackagePlans,
|
|
422
|
+
rootDir,
|
|
423
|
+
rootReleasePlan,
|
|
424
|
+
stagedRootManifestPath,
|
|
425
|
+
stagedRootDir,
|
|
426
|
+
})
|
|
427
|
+
} else if (rootFacade) {
|
|
428
|
+
await validateRootReleasePlan(
|
|
429
|
+
packageJsonPath,
|
|
430
|
+
rootDir,
|
|
431
|
+
rootReleasePlan,
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
npmClient,
|
|
437
|
+
packageName,
|
|
438
|
+
packageVersion: packageJson.version,
|
|
439
|
+
releasePackages: releasePackagePlans.map((plan) =>
|
|
440
|
+
prepareReleasePackage(plan, binaryName),
|
|
441
|
+
),
|
|
442
|
+
workspaceRoot: preparedWorkspaceRoot,
|
|
443
|
+
}
|
|
444
|
+
},
|
|
40
445
|
)
|
|
446
|
+
} catch (error) {
|
|
447
|
+
if (preparedSnapshotRoot) {
|
|
448
|
+
await removePreparedSnapshot(
|
|
449
|
+
preparedSnapshotRoot,
|
|
450
|
+
'preparation',
|
|
451
|
+
true,
|
|
452
|
+
error,
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
throw error
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const {
|
|
459
|
+
npmClient,
|
|
460
|
+
packageName,
|
|
461
|
+
packageVersion,
|
|
462
|
+
releasePackages,
|
|
463
|
+
workspaceRoot,
|
|
464
|
+
} = prepared
|
|
41
465
|
|
|
42
466
|
async function createGhRelease(packageName: string, version: string) {
|
|
43
467
|
if (!options.ghRelease) {
|
|
@@ -130,105 +554,2427 @@ export async function prePublish(userOptions: PrePublishOptions) {
|
|
|
130
554
|
return { owner, repo, pkgInfo, octokit }
|
|
131
555
|
}
|
|
132
556
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
557
|
+
let publicationFailed = false
|
|
558
|
+
let publicationError: unknown
|
|
559
|
+
try {
|
|
560
|
+
const { owner, repo, pkgInfo, octokit } = options.ghReleaseId
|
|
561
|
+
? getRepoInfo(packageName, packageVersion)
|
|
562
|
+
: await createGhRelease(packageName, packageVersion)
|
|
139
563
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
564
|
+
for (const releasePackage of releasePackages) {
|
|
565
|
+
const { artifactPath, filename, packageDir } = releasePackage
|
|
566
|
+
let publicationPackageDir = packageDir
|
|
567
|
+
let publicationArtifactPath = artifactPath
|
|
568
|
+
let publicationExecutionRoot: string | undefined
|
|
569
|
+
let releaseFailed = false
|
|
570
|
+
let releaseError: unknown
|
|
146
571
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const pkgDir = resolve(
|
|
153
|
-
options.cwd,
|
|
154
|
-
options.npmDir,
|
|
155
|
-
`${target.platformArchABI}`,
|
|
156
|
-
)
|
|
157
|
-
const ext =
|
|
158
|
-
target.platform === 'wasi' || target.platform === 'wasm' ? 'wasm' : 'node'
|
|
159
|
-
const filename = `${binaryName}.${target.platformArchABI}.${ext}`
|
|
160
|
-
const dstPath = join(pkgDir, filename)
|
|
572
|
+
try {
|
|
573
|
+
if (!options.dryRun) {
|
|
574
|
+
if (!existsSync(artifactPath)) {
|
|
575
|
+
throw new Error(`Release artifact does not exist: ${artifactPath}`)
|
|
576
|
+
}
|
|
161
577
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
578
|
+
if (!options.skipOptionalPublish) {
|
|
579
|
+
if (!workspaceRoot) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
'Prepared publication workspace is missing for a real publish',
|
|
582
|
+
)
|
|
583
|
+
}
|
|
584
|
+
const publicationExecution =
|
|
585
|
+
await createPublicationExecutionPackage(workspaceRoot, packageDir)
|
|
586
|
+
publicationExecutionRoot = publicationExecution.root
|
|
587
|
+
publicationPackageDir = publicationExecution.packageDir
|
|
588
|
+
publicationArtifactPath = join(publicationPackageDir, filename)
|
|
589
|
+
try {
|
|
590
|
+
const output = execSync(`${npmClient} publish`, {
|
|
591
|
+
cwd: publicationPackageDir,
|
|
592
|
+
env: process.env,
|
|
593
|
+
stdio: 'pipe',
|
|
594
|
+
})
|
|
595
|
+
process.stdout.write(output)
|
|
596
|
+
} catch (e) {
|
|
597
|
+
if (
|
|
598
|
+
e instanceof Error &&
|
|
599
|
+
e.message.includes(
|
|
600
|
+
'You cannot publish over the previously published versions',
|
|
601
|
+
)
|
|
602
|
+
) {
|
|
603
|
+
console.info(e.message)
|
|
604
|
+
debug.warn(`${packageDir} has been published, skipping`)
|
|
605
|
+
} else {
|
|
606
|
+
throw e
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
167
610
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
611
|
+
if (options.ghRelease && repo && owner) {
|
|
612
|
+
debug.info(`Creating GitHub release ${pkgInfo.tag}`)
|
|
613
|
+
try {
|
|
614
|
+
const releaseId = options.ghReleaseId
|
|
615
|
+
? Number(options.ghReleaseId)
|
|
616
|
+
: (
|
|
617
|
+
await octokit!.repos.getReleaseByTag({
|
|
618
|
+
repo: repo,
|
|
619
|
+
owner: owner,
|
|
620
|
+
tag: pkgInfo.tag,
|
|
621
|
+
})
|
|
622
|
+
).data.id
|
|
623
|
+
const artifactStats = statSync(publicationArtifactPath)
|
|
624
|
+
const assetInfo = await octokit!.repos.uploadReleaseAsset({
|
|
625
|
+
owner: owner,
|
|
626
|
+
repo: repo,
|
|
627
|
+
name: filename,
|
|
628
|
+
release_id: releaseId,
|
|
629
|
+
mediaType: { format: 'raw' },
|
|
630
|
+
headers: {
|
|
631
|
+
'content-length': artifactStats.size,
|
|
632
|
+
'content-type': 'application/octet-stream',
|
|
633
|
+
},
|
|
634
|
+
// @ts-expect-error octokit types are wrong
|
|
635
|
+
data: await readFileAsync(publicationArtifactPath),
|
|
636
|
+
})
|
|
637
|
+
debug.info(`GitHub release created`)
|
|
638
|
+
debug.info(
|
|
639
|
+
`Download URL: %s`,
|
|
640
|
+
assetInfo.data.browser_download_url,
|
|
641
|
+
)
|
|
642
|
+
} catch (e) {
|
|
643
|
+
debug.error(
|
|
644
|
+
`Param: ${JSON.stringify(
|
|
645
|
+
{
|
|
646
|
+
owner,
|
|
647
|
+
repo,
|
|
648
|
+
tag: pkgInfo.tag,
|
|
649
|
+
filename: publicationArtifactPath,
|
|
650
|
+
},
|
|
651
|
+
null,
|
|
652
|
+
2,
|
|
653
|
+
)}`,
|
|
654
|
+
)
|
|
655
|
+
debug.error(e)
|
|
656
|
+
}
|
|
187
657
|
}
|
|
188
658
|
}
|
|
659
|
+
} catch (error) {
|
|
660
|
+
releaseFailed = true
|
|
661
|
+
releaseError = error
|
|
189
662
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
tag: pkgInfo.tag,
|
|
201
|
-
})
|
|
202
|
-
).data.id
|
|
203
|
-
const dstFileStats = statSync(dstPath)
|
|
204
|
-
const assetInfo = await octokit!.repos.uploadReleaseAsset({
|
|
205
|
-
owner: owner,
|
|
206
|
-
repo: repo,
|
|
207
|
-
name: filename,
|
|
208
|
-
release_id: releaseId,
|
|
209
|
-
mediaType: { format: 'raw' },
|
|
210
|
-
headers: {
|
|
211
|
-
'content-length': dstFileStats.size,
|
|
212
|
-
'content-type': 'application/octet-stream',
|
|
213
|
-
},
|
|
214
|
-
// @ts-expect-error octokit types are wrong
|
|
215
|
-
data: await readFileAsync(dstPath),
|
|
216
|
-
})
|
|
217
|
-
debug.info(`GitHub release created`)
|
|
218
|
-
debug.info(`Download URL: %s`, assetInfo.data.browser_download_url)
|
|
219
|
-
} catch (e) {
|
|
220
|
-
debug.error(
|
|
221
|
-
`Param: ${JSON.stringify(
|
|
222
|
-
{ owner, repo, tag: pkgInfo.tag, filename: dstPath },
|
|
223
|
-
null,
|
|
224
|
-
2,
|
|
225
|
-
)}`,
|
|
226
|
-
)
|
|
227
|
-
debug.error(e)
|
|
228
|
-
}
|
|
663
|
+
if (publicationExecutionRoot) {
|
|
664
|
+
await removePreparedSnapshot(
|
|
665
|
+
publicationExecutionRoot,
|
|
666
|
+
'publication',
|
|
667
|
+
releaseFailed,
|
|
668
|
+
releaseError,
|
|
669
|
+
)
|
|
670
|
+
}
|
|
671
|
+
if (releaseFailed) {
|
|
672
|
+
throw releaseError
|
|
229
673
|
}
|
|
230
674
|
}
|
|
675
|
+
} catch (error) {
|
|
676
|
+
publicationFailed = true
|
|
677
|
+
publicationError = error
|
|
678
|
+
}
|
|
679
|
+
if (preparedSnapshotRoot) {
|
|
680
|
+
await removePreparedSnapshot(
|
|
681
|
+
preparedSnapshotRoot,
|
|
682
|
+
'publication',
|
|
683
|
+
publicationFailed,
|
|
684
|
+
publicationError,
|
|
685
|
+
)
|
|
686
|
+
}
|
|
687
|
+
if (publicationFailed) {
|
|
688
|
+
throw publicationError
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
interface ThreadlessWasiRootFacadeOptions {
|
|
693
|
+
packageJsonPath: string
|
|
694
|
+
packageJson: CommonPackageJsonFields
|
|
695
|
+
packageName: string
|
|
696
|
+
binaryName: string
|
|
697
|
+
target: Target
|
|
698
|
+
npmDir: string
|
|
699
|
+
managedGeneratedFiles: string[]
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
interface ThreadlessWasiRootFacade {
|
|
703
|
+
exports: Record<string, unknown>
|
|
704
|
+
publishConfigExports?: Record<string, unknown>
|
|
705
|
+
generatedFiles: string[]
|
|
706
|
+
files: ThreadlessWasiRootFacadeFiles
|
|
707
|
+
wasmSourcePath: string
|
|
708
|
+
marker: string
|
|
709
|
+
forwardingModule: string
|
|
710
|
+
packageJsonUpdate: {
|
|
711
|
+
files?: string[]
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
interface ThreadlessWasiRootFacadeFiles {
|
|
716
|
+
workerdEntry: string
|
|
717
|
+
workerdTypeDef: string
|
|
718
|
+
wasmEntry: string
|
|
719
|
+
wasmTypeDef: string
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
interface ThreadlessWasiRootFacadeReconciliation {
|
|
723
|
+
managedFlavorPackages: string[]
|
|
724
|
+
packageJson: CommonPackageJsonFields
|
|
725
|
+
staleGeneratedFiles: string[]
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
interface ManagedThreadlessWasiRootFacade {
|
|
729
|
+
files: ThreadlessWasiRootFacadeFiles
|
|
730
|
+
flavorPackage: string
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
interface ThreadlessWasiRootFacadeMarker {
|
|
734
|
+
version: 1
|
|
735
|
+
flavorPackage: string
|
|
736
|
+
wasmSha256: string
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function reconcileThreadlessWasiRootFacade(
|
|
740
|
+
packageJson: CommonPackageJsonFields,
|
|
741
|
+
rootDir: string,
|
|
742
|
+
npmDir: string,
|
|
743
|
+
packageName: string,
|
|
744
|
+
): ThreadlessWasiRootFacadeReconciliation {
|
|
745
|
+
const reconciledPackageJson = { ...packageJson }
|
|
746
|
+
const staleGeneratedFiles = new Set<string>()
|
|
747
|
+
const managedFlavorPackages = new Set<string>()
|
|
748
|
+
const rootExports = removeThreadlessWasiRootExports(
|
|
749
|
+
packageJson.exports,
|
|
750
|
+
packageJson,
|
|
751
|
+
rootDir,
|
|
752
|
+
npmDir,
|
|
753
|
+
packageName,
|
|
754
|
+
)
|
|
755
|
+
for (const file of rootExports.generatedFiles) {
|
|
756
|
+
staleGeneratedFiles.add(file)
|
|
757
|
+
}
|
|
758
|
+
if (rootExports.managedFlavorPackage) {
|
|
759
|
+
managedFlavorPackages.add(rootExports.managedFlavorPackage)
|
|
760
|
+
}
|
|
761
|
+
setOptionalProperty(
|
|
762
|
+
reconciledPackageJson as Record<string, unknown>,
|
|
763
|
+
'exports',
|
|
764
|
+
rootExports.exports,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
const publishConfig = asRecord(packageJson.publishConfig)
|
|
768
|
+
if (
|
|
769
|
+
publishConfig &&
|
|
770
|
+
Object.prototype.hasOwnProperty.call(publishConfig, 'exports')
|
|
771
|
+
) {
|
|
772
|
+
const publishConfigExports = removeThreadlessWasiRootExports(
|
|
773
|
+
publishConfig.exports as CommonPackageJsonFields['exports'],
|
|
774
|
+
packageJson,
|
|
775
|
+
rootDir,
|
|
776
|
+
npmDir,
|
|
777
|
+
packageName,
|
|
778
|
+
)
|
|
779
|
+
for (const file of publishConfigExports.generatedFiles) {
|
|
780
|
+
staleGeneratedFiles.add(file)
|
|
781
|
+
}
|
|
782
|
+
if (publishConfigExports.managedFlavorPackage) {
|
|
783
|
+
managedFlavorPackages.add(publishConfigExports.managedFlavorPackage)
|
|
784
|
+
}
|
|
785
|
+
const reconciledPublishConfig = { ...publishConfig }
|
|
786
|
+
setOptionalProperty(
|
|
787
|
+
reconciledPublishConfig,
|
|
788
|
+
'exports',
|
|
789
|
+
publishConfigExports.exports,
|
|
790
|
+
)
|
|
791
|
+
reconciledPackageJson.publishConfig = reconciledPublishConfig
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (Array.isArray(packageJson.files)) {
|
|
795
|
+
reconciledPackageJson.files = packageJson.files.filter(
|
|
796
|
+
(file) => !staleGeneratedFiles.has(file),
|
|
797
|
+
)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return {
|
|
801
|
+
managedFlavorPackages: [...managedFlavorPackages],
|
|
802
|
+
packageJson: reconciledPackageJson,
|
|
803
|
+
staleGeneratedFiles: [...staleGeneratedFiles],
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function removeThreadlessWasiRootExports(
|
|
808
|
+
currentExports: CommonPackageJsonFields['exports'],
|
|
809
|
+
packageJson: CommonPackageJsonFields,
|
|
810
|
+
rootDir: string,
|
|
811
|
+
npmDir: string,
|
|
812
|
+
packageName: string,
|
|
813
|
+
) {
|
|
814
|
+
const exportsMap = asRecord(currentExports)
|
|
815
|
+
const managedFacade = getManagedThreadlessWasiRootFacadeFiles(
|
|
816
|
+
exportsMap,
|
|
817
|
+
rootDir,
|
|
818
|
+
npmDir,
|
|
819
|
+
packageName,
|
|
820
|
+
)
|
|
821
|
+
if (!exportsMap || !managedFacade) {
|
|
822
|
+
return {
|
|
823
|
+
exports: currentExports,
|
|
824
|
+
generatedFiles: [],
|
|
825
|
+
managedFlavorPackage: undefined,
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const { files: generatedFiles, flavorPackage: managedFlavorPackage } =
|
|
829
|
+
managedFacade
|
|
830
|
+
|
|
831
|
+
const nextExports = { ...exportsMap }
|
|
832
|
+
delete nextExports['./workerd']
|
|
833
|
+
delete nextExports['./wasm']
|
|
834
|
+
delete nextExports['./wasm.wasm']
|
|
835
|
+
const keys = Object.keys(nextExports)
|
|
836
|
+
const generatedLegacyExports = createLegacyDeepImportExports(rootDir)
|
|
837
|
+
if (
|
|
838
|
+
Object.prototype.hasOwnProperty.call(nextExports, '.') &&
|
|
839
|
+
nextExports['./*'] === './*' &&
|
|
840
|
+
isDeepStrictEqual(
|
|
841
|
+
nextExports['.'],
|
|
842
|
+
createLegacyRootExport(packageJson, rootDir),
|
|
843
|
+
) &&
|
|
844
|
+
keys
|
|
845
|
+
.filter((key) => key !== '.' && key !== './*')
|
|
846
|
+
.every(
|
|
847
|
+
(key) =>
|
|
848
|
+
generatedLegacyExports[key] !== undefined &&
|
|
849
|
+
isDeepStrictEqual(nextExports[key], generatedLegacyExports[key]),
|
|
850
|
+
)
|
|
851
|
+
) {
|
|
852
|
+
return {
|
|
853
|
+
exports: undefined,
|
|
854
|
+
generatedFiles: Object.values(generatedFiles),
|
|
855
|
+
managedFlavorPackage,
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (keys.length === 1 && keys[0] === '.') {
|
|
859
|
+
return {
|
|
860
|
+
exports: nextExports['.'],
|
|
861
|
+
generatedFiles: Object.values(generatedFiles),
|
|
862
|
+
managedFlavorPackage,
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
exports: keys.length > 0 ? nextExports : undefined,
|
|
867
|
+
generatedFiles: Object.values(generatedFiles),
|
|
868
|
+
managedFlavorPackage,
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function getManagedThreadlessWasiRootFacadeFiles(
|
|
873
|
+
exportsMap: Record<string, unknown> | undefined,
|
|
874
|
+
rootDir: string,
|
|
875
|
+
npmDir: string,
|
|
876
|
+
packageName: string,
|
|
877
|
+
): ManagedThreadlessWasiRootFacade | undefined {
|
|
878
|
+
if (!exportsMap) {
|
|
879
|
+
return undefined
|
|
880
|
+
}
|
|
881
|
+
const workerdExport = asRecord(exportsMap['./workerd'])
|
|
882
|
+
const workerdEntry =
|
|
883
|
+
typeof workerdExport?.default === 'string'
|
|
884
|
+
? workerdExport.default.match(
|
|
885
|
+
/^\.\/([^/\\]+\.wasm32-wasip1)\.workerd\.mjs$/,
|
|
886
|
+
)
|
|
887
|
+
: null
|
|
888
|
+
if (!workerdEntry) {
|
|
889
|
+
return undefined
|
|
890
|
+
}
|
|
891
|
+
const generatedPrefix = workerdEntry[1]
|
|
892
|
+
const files: ThreadlessWasiRootFacadeFiles = {
|
|
893
|
+
workerdEntry: `${generatedPrefix}.workerd.mjs`,
|
|
894
|
+
workerdTypeDef: `${generatedPrefix}.workerd.d.mts`,
|
|
895
|
+
wasmEntry: `${generatedPrefix}.wasm`,
|
|
896
|
+
wasmTypeDef: `${generatedPrefix}.wasm.d.mts`,
|
|
897
|
+
}
|
|
898
|
+
const expectedWorkerdExport = {
|
|
899
|
+
types: `./${files.workerdTypeDef}`,
|
|
900
|
+
default: `./${files.workerdEntry}`,
|
|
901
|
+
}
|
|
902
|
+
const expectedWasmExport = {
|
|
903
|
+
types: `./${files.wasmTypeDef}`,
|
|
904
|
+
default: `./${files.wasmEntry}`,
|
|
905
|
+
}
|
|
906
|
+
if (
|
|
907
|
+
!isDeepStrictEqual(workerdExport, expectedWorkerdExport) ||
|
|
908
|
+
!isDeepStrictEqual(exportsMap['./wasm'], expectedWasmExport) ||
|
|
909
|
+
!isDeepStrictEqual(exportsMap['./wasm.wasm'], expectedWasmExport)
|
|
910
|
+
) {
|
|
911
|
+
return undefined
|
|
912
|
+
}
|
|
913
|
+
const flavorPackage = `${packageName}-wasm32-wasip1`
|
|
914
|
+
const managedFlavorPackage =
|
|
915
|
+
getManagedThreadlessWasiRootFacadeMarker(rootDir, files) ??
|
|
916
|
+
getPartialManagedThreadlessWasiRootFacadeMarker(rootDir, npmDir, files) ??
|
|
917
|
+
getLegacyThreadlessWasiRootFacade(rootDir, npmDir, files)
|
|
918
|
+
if (managedFlavorPackage) {
|
|
919
|
+
return { files, flavorPackage: managedFlavorPackage }
|
|
920
|
+
}
|
|
921
|
+
if (
|
|
922
|
+
hasPartialOrCorruptThreadlessWasiRootFacade(rootDir, files, flavorPackage)
|
|
923
|
+
) {
|
|
924
|
+
throw new Error(
|
|
925
|
+
'The threadless WASI root facade is partial or corrupt and ownership cannot be verified. Restore the generated facade files or remove the generated-shaped exports and files before running pre-publish.',
|
|
926
|
+
)
|
|
927
|
+
}
|
|
928
|
+
return undefined
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function getManagedThreadlessWasiRootFacadeMarker(
|
|
932
|
+
rootDir: string,
|
|
933
|
+
files: ThreadlessWasiRootFacadeFiles,
|
|
934
|
+
) {
|
|
935
|
+
const workerdEntry = readRegularFile(join(rootDir, files.workerdEntry))
|
|
936
|
+
const workerdTypeDef = readRegularFile(join(rootDir, files.workerdTypeDef))
|
|
937
|
+
const wasmEntry = readRegularFile(join(rootDir, files.wasmEntry))
|
|
938
|
+
const wasmTypeDef = readRegularFile(join(rootDir, files.wasmTypeDef))
|
|
939
|
+
if (!workerdEntry || !workerdTypeDef || !wasmEntry || !wasmTypeDef) {
|
|
940
|
+
return undefined
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const markedFiles = [workerdEntry, workerdTypeDef, wasmTypeDef].map((file) =>
|
|
944
|
+
parseThreadlessWasiRootFacadeMarker(file.toString('utf8')),
|
|
945
|
+
)
|
|
946
|
+
if (markedFiles.some((file) => file === undefined)) {
|
|
947
|
+
return undefined
|
|
948
|
+
}
|
|
949
|
+
const [markedWorkerdEntry, markedWorkerdTypeDef, markedWasmTypeDef] =
|
|
950
|
+
markedFiles as Array<{
|
|
951
|
+
marker: ThreadlessWasiRootFacadeMarker
|
|
952
|
+
markerLine: string
|
|
953
|
+
body: string
|
|
954
|
+
}>
|
|
955
|
+
if (
|
|
956
|
+
markedWorkerdEntry.markerLine !== markedWorkerdTypeDef.markerLine ||
|
|
957
|
+
markedWorkerdEntry.markerLine !== markedWasmTypeDef.markerLine
|
|
958
|
+
) {
|
|
959
|
+
return undefined
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const expectedMarker: ThreadlessWasiRootFacadeMarker = {
|
|
963
|
+
version: 1,
|
|
964
|
+
flavorPackage: markedWorkerdEntry.marker.flavorPackage,
|
|
965
|
+
wasmSha256: createHash('sha256').update(wasmEntry).digest('hex'),
|
|
966
|
+
}
|
|
967
|
+
if (!isDeepStrictEqual(markedWorkerdEntry.marker, expectedMarker)) {
|
|
968
|
+
return undefined
|
|
969
|
+
}
|
|
970
|
+
const forwardingModule = createThreadlessWasiRootForwardingModule(
|
|
971
|
+
markedWorkerdEntry.marker.flavorPackage,
|
|
972
|
+
)
|
|
973
|
+
return markedWorkerdEntry.body === forwardingModule &&
|
|
974
|
+
markedWorkerdTypeDef.body === forwardingModule &&
|
|
975
|
+
markedWasmTypeDef.body === createWasmModuleTypeDef()
|
|
976
|
+
? markedWorkerdEntry.marker.flavorPackage
|
|
977
|
+
: undefined
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function getPartialManagedThreadlessWasiRootFacadeMarker(
|
|
981
|
+
rootDir: string,
|
|
982
|
+
npmDir: string,
|
|
983
|
+
files: ThreadlessWasiRootFacadeFiles,
|
|
984
|
+
) {
|
|
985
|
+
const availableWasmHashes = new Set<string>()
|
|
986
|
+
for (const wasm of [
|
|
987
|
+
readRegularFile(join(rootDir, files.wasmEntry)),
|
|
988
|
+
readRegularFile(join(npmDir, 'wasm32-wasip1', files.wasmEntry)),
|
|
989
|
+
]) {
|
|
990
|
+
if (wasm) {
|
|
991
|
+
availableWasmHashes.add(createHash('sha256').update(wasm).digest('hex'))
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (availableWasmHashes.size === 0) {
|
|
995
|
+
return undefined
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const flavorPackages = new Set<string>()
|
|
999
|
+
for (const file of [
|
|
1000
|
+
files.workerdEntry,
|
|
1001
|
+
files.workerdTypeDef,
|
|
1002
|
+
files.wasmTypeDef,
|
|
1003
|
+
]) {
|
|
1004
|
+
const contents = readRegularFile(join(rootDir, file))
|
|
1005
|
+
if (!contents) {
|
|
1006
|
+
continue
|
|
1007
|
+
}
|
|
1008
|
+
const markedFile = parseThreadlessWasiRootFacadeMarker(
|
|
1009
|
+
contents.toString('utf8'),
|
|
1010
|
+
)
|
|
1011
|
+
if (
|
|
1012
|
+
markedFile !== undefined &&
|
|
1013
|
+
availableWasmHashes.has(markedFile.marker.wasmSha256)
|
|
1014
|
+
) {
|
|
1015
|
+
flavorPackages.add(markedFile.marker.flavorPackage)
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return flavorPackages.size === 1 ? [...flavorPackages][0] : undefined
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function getLegacyThreadlessWasiRootFacade(
|
|
1022
|
+
rootDir: string,
|
|
1023
|
+
npmDir: string,
|
|
1024
|
+
files: ThreadlessWasiRootFacadeFiles,
|
|
1025
|
+
) {
|
|
1026
|
+
const workerdEntry = readRegularFile(join(rootDir, files.workerdEntry))
|
|
1027
|
+
const workerdTypeDef = readRegularFile(join(rootDir, files.workerdTypeDef))
|
|
1028
|
+
const wasmEntry = readRegularFile(join(rootDir, files.wasmEntry))
|
|
1029
|
+
const wasmTypeDef = readRegularFile(join(rootDir, files.wasmTypeDef))
|
|
1030
|
+
const flavorWasmEntry = readRegularFile(
|
|
1031
|
+
join(npmDir, 'wasm32-wasip1', files.wasmEntry),
|
|
1032
|
+
)
|
|
1033
|
+
if (!wasmEntry || !flavorWasmEntry || !wasmEntry.equals(flavorWasmEntry)) {
|
|
1034
|
+
return undefined
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const forwardingPackages = new Set<string>()
|
|
1038
|
+
for (const [file, contents] of [
|
|
1039
|
+
[files.workerdEntry, workerdEntry],
|
|
1040
|
+
[files.workerdTypeDef, workerdTypeDef],
|
|
1041
|
+
] as const) {
|
|
1042
|
+
if (!existsSync(join(rootDir, file)) && contents === undefined) {
|
|
1043
|
+
continue
|
|
1044
|
+
}
|
|
1045
|
+
const flavorPackage = contents
|
|
1046
|
+
? parseThreadlessWasiRootForwardingModule(contents.toString('utf8'))
|
|
1047
|
+
: undefined
|
|
1048
|
+
if (!flavorPackage) {
|
|
1049
|
+
return undefined
|
|
1050
|
+
}
|
|
1051
|
+
forwardingPackages.add(flavorPackage)
|
|
1052
|
+
}
|
|
1053
|
+
if (
|
|
1054
|
+
(existsSync(join(rootDir, files.wasmTypeDef)) ||
|
|
1055
|
+
wasmTypeDef !== undefined) &&
|
|
1056
|
+
wasmTypeDef?.toString('utf8') !== createWasmModuleTypeDef()
|
|
1057
|
+
) {
|
|
1058
|
+
return undefined
|
|
1059
|
+
}
|
|
1060
|
+
return forwardingPackages.size === 1 ? [...forwardingPackages][0] : undefined
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function hasPartialOrCorruptThreadlessWasiRootFacade(
|
|
1064
|
+
rootDir: string,
|
|
1065
|
+
files: ThreadlessWasiRootFacadeFiles,
|
|
1066
|
+
flavorPackage: string,
|
|
1067
|
+
) {
|
|
1068
|
+
const facadeFiles = Object.values(files).map(
|
|
1069
|
+
(file) => [file, readRegularFile(join(rootDir, file))] as const,
|
|
1070
|
+
)
|
|
1071
|
+
if (facadeFiles.some(([, contents]) => contents === undefined)) {
|
|
1072
|
+
return true
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const forwardingModule =
|
|
1076
|
+
createThreadlessWasiRootForwardingModule(flavorPackage)
|
|
1077
|
+
const expectedLegacyContents = new Map([
|
|
1078
|
+
[files.workerdEntry, forwardingModule],
|
|
1079
|
+
[files.workerdTypeDef, forwardingModule],
|
|
1080
|
+
[files.wasmTypeDef, createWasmModuleTypeDef()],
|
|
1081
|
+
])
|
|
1082
|
+
return facadeFiles.some(([file, contents]) => {
|
|
1083
|
+
const source = contents!.toString('utf8')
|
|
1084
|
+
return (
|
|
1085
|
+
source.startsWith(WASI_ROOT_FACADE_MARKER_PREFIX) ||
|
|
1086
|
+
source === expectedLegacyContents.get(file)
|
|
1087
|
+
)
|
|
1088
|
+
})
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function readRegularFile(path: string) {
|
|
1092
|
+
try {
|
|
1093
|
+
if (!lstatSync(path).isFile()) {
|
|
1094
|
+
return undefined
|
|
1095
|
+
}
|
|
1096
|
+
return readFileSync(path)
|
|
1097
|
+
} catch {
|
|
1098
|
+
return undefined
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function parseThreadlessWasiRootFacadeMarker(source: string) {
|
|
1103
|
+
const markerEnd = source.indexOf('\n')
|
|
1104
|
+
if (markerEnd === -1) {
|
|
1105
|
+
return undefined
|
|
1106
|
+
}
|
|
1107
|
+
const markerLine = source.slice(0, markerEnd)
|
|
1108
|
+
if (!markerLine.startsWith(WASI_ROOT_FACADE_MARKER_PREFIX)) {
|
|
1109
|
+
return undefined
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
let marker: unknown
|
|
1113
|
+
try {
|
|
1114
|
+
marker = JSON.parse(markerLine.slice(WASI_ROOT_FACADE_MARKER_PREFIX.length))
|
|
1115
|
+
} catch {
|
|
1116
|
+
return undefined
|
|
1117
|
+
}
|
|
1118
|
+
const markerRecord = asRecord(marker)
|
|
1119
|
+
if (
|
|
1120
|
+
!markerRecord ||
|
|
1121
|
+
!isDeepStrictEqual(Object.keys(markerRecord), [
|
|
1122
|
+
'version',
|
|
1123
|
+
'flavorPackage',
|
|
1124
|
+
'wasmSha256',
|
|
1125
|
+
]) ||
|
|
1126
|
+
markerRecord.version !== 1 ||
|
|
1127
|
+
typeof markerRecord.flavorPackage !== 'string' ||
|
|
1128
|
+
typeof markerRecord.wasmSha256 !== 'string' ||
|
|
1129
|
+
!/^[0-9a-f]{64}$/.test(markerRecord.wasmSha256) ||
|
|
1130
|
+
markerLine !==
|
|
1131
|
+
`${WASI_ROOT_FACADE_MARKER_PREFIX}${JSON.stringify(markerRecord)}`
|
|
1132
|
+
) {
|
|
1133
|
+
return undefined
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
marker: markerRecord as unknown as ThreadlessWasiRootFacadeMarker,
|
|
1137
|
+
markerLine,
|
|
1138
|
+
body: source.slice(markerEnd + 1),
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function createThreadlessWasiRootFacadeMarker(
|
|
1143
|
+
flavorPackage: string,
|
|
1144
|
+
wasm: Buffer,
|
|
1145
|
+
) {
|
|
1146
|
+
const marker: ThreadlessWasiRootFacadeMarker = {
|
|
1147
|
+
version: 1,
|
|
1148
|
+
flavorPackage,
|
|
1149
|
+
wasmSha256: createHash('sha256').update(wasm).digest('hex'),
|
|
1150
|
+
}
|
|
1151
|
+
return `${WASI_ROOT_FACADE_MARKER_PREFIX}${JSON.stringify(marker)}`
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function createThreadlessWasiRootForwardingModule(flavorPackage: string) {
|
|
1155
|
+
return `export * from ${JSON.stringify(`${flavorPackage}/workerd`)}\n`
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function parseThreadlessWasiRootForwardingModule(source: string) {
|
|
1159
|
+
const match = /^export \* from (".+")\n$/.exec(source)
|
|
1160
|
+
if (!match) {
|
|
1161
|
+
return undefined
|
|
1162
|
+
}
|
|
1163
|
+
let specifier: unknown
|
|
1164
|
+
try {
|
|
1165
|
+
specifier = JSON.parse(match[1])
|
|
1166
|
+
} catch {
|
|
1167
|
+
return undefined
|
|
1168
|
+
}
|
|
1169
|
+
if (
|
|
1170
|
+
typeof specifier !== 'string' ||
|
|
1171
|
+
!specifier.endsWith('-wasm32-wasip1/workerd')
|
|
1172
|
+
) {
|
|
1173
|
+
return undefined
|
|
1174
|
+
}
|
|
1175
|
+
return specifier.slice(0, -'/workerd'.length)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function applyThreadlessWasiRootFacade(
|
|
1179
|
+
packageJson: CommonPackageJsonFields,
|
|
1180
|
+
rootFacade: ThreadlessWasiRootFacade,
|
|
1181
|
+
) {
|
|
1182
|
+
const updatedPackageJson = { ...packageJson }
|
|
1183
|
+
if (rootFacade.publishConfigExports) {
|
|
1184
|
+
// The facade exports were built from publishConfig.exports, so write the
|
|
1185
|
+
// result back there and leave the package's local exports map untouched.
|
|
1186
|
+
updatedPackageJson.publishConfig = {
|
|
1187
|
+
...asRecord(packageJson.publishConfig),
|
|
1188
|
+
exports: rootFacade.publishConfigExports,
|
|
1189
|
+
}
|
|
1190
|
+
} else {
|
|
1191
|
+
updatedPackageJson.exports = rootFacade.exports
|
|
1192
|
+
}
|
|
1193
|
+
if (rootFacade.packageJsonUpdate.files) {
|
|
1194
|
+
updatedPackageJson.files = rootFacade.packageJsonUpdate.files
|
|
1195
|
+
}
|
|
1196
|
+
return updatedPackageJson
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function syncThreadlessWasiRootFacadeManifest(
|
|
1200
|
+
destination: Record<string, unknown>,
|
|
1201
|
+
source: CommonPackageJsonFields,
|
|
1202
|
+
) {
|
|
1203
|
+
setOptionalProperty(destination, 'exports', source.exports)
|
|
1204
|
+
setOptionalProperty(destination, 'files', source.files)
|
|
1205
|
+
|
|
1206
|
+
const destinationPublishConfig = asRecord(destination.publishConfig)
|
|
1207
|
+
const sourcePublishConfig = asRecord(source.publishConfig)
|
|
1208
|
+
if (destinationPublishConfig || sourcePublishConfig?.exports !== undefined) {
|
|
1209
|
+
const publishConfig = { ...destinationPublishConfig }
|
|
1210
|
+
setOptionalProperty(publishConfig, 'exports', sourcePublishConfig?.exports)
|
|
1211
|
+
destination.publishConfig = publishConfig
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function setOptionalProperty(
|
|
1216
|
+
object: Record<string, unknown>,
|
|
1217
|
+
key: string,
|
|
1218
|
+
value: unknown,
|
|
1219
|
+
) {
|
|
1220
|
+
if (value === undefined) {
|
|
1221
|
+
delete object[key]
|
|
1222
|
+
} else {
|
|
1223
|
+
object[key] = value
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
1228
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
1229
|
+
? (value as Record<string, unknown>)
|
|
1230
|
+
: undefined
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function planThreadlessWasiRootFacade({
|
|
1234
|
+
packageJsonPath,
|
|
1235
|
+
packageJson,
|
|
1236
|
+
packageName,
|
|
1237
|
+
binaryName,
|
|
1238
|
+
target,
|
|
1239
|
+
npmDir,
|
|
1240
|
+
managedGeneratedFiles,
|
|
1241
|
+
}: ThreadlessWasiRootFacadeOptions): ThreadlessWasiRootFacade {
|
|
1242
|
+
const rootDir = dirname(packageJsonPath)
|
|
1243
|
+
const flavorPackageName = `${packageName}-${target.platformArchABI}`
|
|
1244
|
+
const wasmFileName = `${binaryName}.${target.platformArchABI}.wasm`
|
|
1245
|
+
const generatedPrefix = `${binaryName}.${target.platformArchABI}`
|
|
1246
|
+
const files: ThreadlessWasiRootFacadeFiles = {
|
|
1247
|
+
workerdEntry: `${generatedPrefix}.workerd.mjs`,
|
|
1248
|
+
workerdTypeDef: `${generatedPrefix}.workerd.d.mts`,
|
|
1249
|
+
wasmEntry: wasmFileName,
|
|
1250
|
+
wasmTypeDef: `${wasmFileName}.d.mts`,
|
|
1251
|
+
}
|
|
1252
|
+
const publishConfig = asRecord(packageJson.publishConfig)
|
|
1253
|
+
const hasPublishConfigExports =
|
|
1254
|
+
publishConfig &&
|
|
1255
|
+
Object.prototype.hasOwnProperty.call(publishConfig, 'exports')
|
|
1256
|
+
const effectiveExports = hasPublishConfigExports
|
|
1257
|
+
? (publishConfig.exports as CommonPackageJsonFields['exports'])
|
|
1258
|
+
: packageJson.exports
|
|
1259
|
+
const exports = addThreadlessWasiRootExports(
|
|
1260
|
+
effectiveExports,
|
|
1261
|
+
packageJson,
|
|
1262
|
+
files,
|
|
1263
|
+
rootDir,
|
|
1264
|
+
)
|
|
1265
|
+
const publishConfigExports = hasPublishConfigExports ? exports : undefined
|
|
1266
|
+
const forwardingModule =
|
|
1267
|
+
createThreadlessWasiRootForwardingModule(flavorPackageName)
|
|
1268
|
+
const generatedFiles = Object.values(files)
|
|
1269
|
+
const managedFiles = new Set(managedGeneratedFiles)
|
|
1270
|
+
const wasmSourcePath = join(npmDir, target.platformArchABI, wasmFileName)
|
|
1271
|
+
const marker = createThreadlessWasiRootFacadeMarker(
|
|
1272
|
+
flavorPackageName,
|
|
1273
|
+
readFileSync(wasmSourcePath),
|
|
1274
|
+
)
|
|
1275
|
+
const conflictingFile = generatedFiles.find((file) => {
|
|
1276
|
+
const rootPath = join(rootDir, file)
|
|
1277
|
+
if (!existsSync(rootPath) || managedFiles.has(file)) {
|
|
1278
|
+
return false
|
|
1279
|
+
}
|
|
1280
|
+
// `napi artifacts` already copies the flavor artifact into the root
|
|
1281
|
+
// package. Treat that exact regular file as managed so the standard
|
|
1282
|
+
// artifacts -> pre-publish flow can materialize the root facade.
|
|
1283
|
+
return !(
|
|
1284
|
+
file === files.wasmEntry &&
|
|
1285
|
+
lstatSync(rootPath).isFile() &&
|
|
1286
|
+
readFileSync(rootPath).equals(readFileSync(wasmSourcePath))
|
|
1287
|
+
)
|
|
1288
|
+
})
|
|
1289
|
+
if (conflictingFile) {
|
|
1290
|
+
throw new Error(
|
|
1291
|
+
`Cannot generate the threadless WASI root facade file ${conflictingFile}: the path already exists and is not owned by a managed facade. Remove or rename the existing file before running pre-publish.`,
|
|
1292
|
+
)
|
|
1293
|
+
}
|
|
1294
|
+
const facade: ThreadlessWasiRootFacade = {
|
|
1295
|
+
exports,
|
|
1296
|
+
publishConfigExports,
|
|
1297
|
+
generatedFiles,
|
|
1298
|
+
files,
|
|
1299
|
+
wasmSourcePath,
|
|
1300
|
+
marker,
|
|
1301
|
+
forwardingModule,
|
|
1302
|
+
packageJsonUpdate: {},
|
|
1303
|
+
}
|
|
1304
|
+
if (Array.isArray(packageJson.files)) {
|
|
1305
|
+
facade.packageJsonUpdate = {
|
|
1306
|
+
files: [...new Set([...packageJson.files, ...generatedFiles])],
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return facade
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
async function materializeThreadlessWasiRootFacade(
|
|
1313
|
+
rootDir: string,
|
|
1314
|
+
facade: ThreadlessWasiRootFacade,
|
|
1315
|
+
) {
|
|
1316
|
+
await copyFileAtomic(
|
|
1317
|
+
facade.wasmSourcePath,
|
|
1318
|
+
join(rootDir, facade.files.wasmEntry),
|
|
1319
|
+
)
|
|
1320
|
+
await Promise.all([
|
|
1321
|
+
writeFileAtomic(
|
|
1322
|
+
join(rootDir, facade.files.workerdEntry),
|
|
1323
|
+
`${facade.marker}\n${facade.forwardingModule}`,
|
|
1324
|
+
'utf8',
|
|
1325
|
+
),
|
|
1326
|
+
writeFileAtomic(
|
|
1327
|
+
join(rootDir, facade.files.workerdTypeDef),
|
|
1328
|
+
`${facade.marker}\n${facade.forwardingModule}`,
|
|
1329
|
+
'utf8',
|
|
1330
|
+
),
|
|
1331
|
+
writeFileAtomic(
|
|
1332
|
+
join(rootDir, facade.files.wasmTypeDef),
|
|
1333
|
+
`${facade.marker}\n${createWasmModuleTypeDef()}`,
|
|
1334
|
+
'utf8',
|
|
1335
|
+
),
|
|
1336
|
+
])
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function addThreadlessWasiRootExports(
|
|
1340
|
+
currentExports: CommonPackageJsonFields['exports'],
|
|
1341
|
+
packageJson: CommonPackageJsonFields,
|
|
1342
|
+
files: ThreadlessWasiRootFacadeFiles,
|
|
1343
|
+
rootDir: string,
|
|
1344
|
+
) {
|
|
1345
|
+
let exportsMap: Record<string, unknown>
|
|
1346
|
+
if (currentExports === undefined) {
|
|
1347
|
+
exportsMap = {
|
|
1348
|
+
'.': createLegacyRootExport(packageJson, rootDir),
|
|
1349
|
+
// Adding an exports map would otherwise encapsulate every historical
|
|
1350
|
+
// deep import. The wildcard preserves explicit paths, while exact
|
|
1351
|
+
// aliases preserve CommonJS extension and directory-index resolution.
|
|
1352
|
+
'./*': './*',
|
|
1353
|
+
...createLegacyDeepImportExports(rootDir),
|
|
1354
|
+
}
|
|
1355
|
+
} else if (
|
|
1356
|
+
typeof currentExports === 'object' &&
|
|
1357
|
+
currentExports !== null &&
|
|
1358
|
+
!Array.isArray(currentExports) &&
|
|
1359
|
+
Object.keys(currentExports).some((key) => key.startsWith('.'))
|
|
1360
|
+
) {
|
|
1361
|
+
exportsMap = { ...currentExports }
|
|
1362
|
+
} else {
|
|
1363
|
+
// A string, array, or condition-only object describes the package root.
|
|
1364
|
+
exportsMap = { '.': currentExports }
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
setThreadlessWasiRootExport(exportsMap, './workerd', {
|
|
1368
|
+
types: `./${files.workerdTypeDef}`,
|
|
1369
|
+
default: `./${files.workerdEntry}`,
|
|
1370
|
+
})
|
|
1371
|
+
const wasmExport = {
|
|
1372
|
+
types: `./${files.wasmTypeDef}`,
|
|
1373
|
+
default: `./${files.wasmEntry}`,
|
|
1374
|
+
}
|
|
1375
|
+
setThreadlessWasiRootExport(exportsMap, './wasm', wasmExport)
|
|
1376
|
+
setThreadlessWasiRootExport(exportsMap, './wasm.wasm', wasmExport)
|
|
1377
|
+
return exportsMap
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function setThreadlessWasiRootExport(
|
|
1381
|
+
exportsMap: Record<string, unknown>,
|
|
1382
|
+
subpath: string,
|
|
1383
|
+
generatedExport: unknown,
|
|
1384
|
+
) {
|
|
1385
|
+
if (!Object.prototype.hasOwnProperty.call(exportsMap, subpath)) {
|
|
1386
|
+
exportsMap[subpath] = generatedExport
|
|
1387
|
+
return
|
|
1388
|
+
}
|
|
1389
|
+
if (isDeepStrictEqual(exportsMap[subpath], generatedExport)) {
|
|
1390
|
+
return
|
|
1391
|
+
}
|
|
1392
|
+
throw new Error(
|
|
1393
|
+
`Cannot generate the threadless WASI root export ${subpath}: package.json already defines that subpath. Remove or rename the existing export before running pre-publish.`,
|
|
1394
|
+
)
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function createLegacyRootExport(
|
|
1398
|
+
packageJson: CommonPackageJsonFields,
|
|
1399
|
+
rootDir: string,
|
|
1400
|
+
) {
|
|
1401
|
+
const main = resolveLegacyPackageTarget(
|
|
1402
|
+
rootDir,
|
|
1403
|
+
packageJson.main ?? 'index.js',
|
|
1404
|
+
)
|
|
1405
|
+
const rootExport: Record<string, string> = {}
|
|
1406
|
+
const legacyPackageJson = packageJson as CommonPackageJsonFields & {
|
|
1407
|
+
typings?: unknown
|
|
1408
|
+
}
|
|
1409
|
+
const typeDef =
|
|
1410
|
+
typeof packageJson.types === 'string'
|
|
1411
|
+
? packageJson.types
|
|
1412
|
+
: typeof legacyPackageJson.typings === 'string'
|
|
1413
|
+
? legacyPackageJson.typings
|
|
1414
|
+
: undefined
|
|
1415
|
+
if (typeDef) {
|
|
1416
|
+
rootExport.types = resolveLegacyPackageTarget(rootDir, typeDef)
|
|
1417
|
+
}
|
|
1418
|
+
if (typeof packageJson.browser === 'string') {
|
|
1419
|
+
rootExport.browser = resolveLegacyPackageTarget(
|
|
1420
|
+
rootDir,
|
|
1421
|
+
packageJson.browser,
|
|
1422
|
+
)
|
|
1423
|
+
}
|
|
1424
|
+
if (typeof packageJson.module === 'string') {
|
|
1425
|
+
const module = resolveLegacyPackageTarget(rootDir, packageJson.module)
|
|
1426
|
+
if (isNodeEsmTarget(module, packageJson.type)) {
|
|
1427
|
+
rootExport.import = module
|
|
1428
|
+
} else {
|
|
1429
|
+
rootExport.module = module
|
|
1430
|
+
rootExport.require = main
|
|
1431
|
+
}
|
|
1432
|
+
} else if (packageJson.type === 'module') {
|
|
1433
|
+
rootExport.import = main
|
|
1434
|
+
} else {
|
|
1435
|
+
rootExport.require = main
|
|
1436
|
+
}
|
|
1437
|
+
// Node ignores the legacy `module` field. CommonJS resolution reaches the
|
|
1438
|
+
// main entry through `node`, while ESM reaches `import` above when present.
|
|
1439
|
+
rootExport.node = main
|
|
1440
|
+
rootExport.default = main
|
|
1441
|
+
return rootExport
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function normalizePackageTarget(target: string) {
|
|
1445
|
+
return target.startsWith('./') ? target : `./${target}`
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function resolveLegacyPackageTarget(rootDir: string, target: string) {
|
|
1449
|
+
try {
|
|
1450
|
+
const resolvedTarget = createRequire(join(rootDir, 'package.json')).resolve(
|
|
1451
|
+
resolve(rootDir, target),
|
|
1452
|
+
)
|
|
1453
|
+
const relativeTarget = relative(rootDir, resolvedTarget)
|
|
1454
|
+
if (
|
|
1455
|
+
relativeTarget.length > 0 &&
|
|
1456
|
+
relativeTarget !== '..' &&
|
|
1457
|
+
!relativeTarget.startsWith(`..${sep}`) &&
|
|
1458
|
+
!resolve(rootDir, relativeTarget).startsWith(
|
|
1459
|
+
`${resolve(rootDir)}${sep}node_modules${sep}`,
|
|
1460
|
+
)
|
|
1461
|
+
) {
|
|
1462
|
+
return normalizePackageTarget(relativeTarget.split(sep).join('/'))
|
|
1463
|
+
}
|
|
1464
|
+
} catch {
|
|
1465
|
+
// Preserve the legacy value so root validation can report the missing path.
|
|
231
1466
|
}
|
|
1467
|
+
return normalizePackageTarget(target)
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function isNodeEsmTarget(
|
|
1471
|
+
target: string,
|
|
1472
|
+
packageType: CommonPackageJsonFields['type'],
|
|
1473
|
+
) {
|
|
1474
|
+
const extension = extname(target)
|
|
1475
|
+
return (
|
|
1476
|
+
extension === '.mjs' || (extension === '.js' && packageType === 'module')
|
|
1477
|
+
)
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function createLegacyDeepImportExports(rootDir: string) {
|
|
1481
|
+
const packedFiles = readNpmPackFiles(rootDir, 'root package')
|
|
1482
|
+
const exportsMap: Record<string, string> = {}
|
|
1483
|
+
const matchingFiles = [...packedFiles]
|
|
1484
|
+
.filter((file) =>
|
|
1485
|
+
LEGACY_DEEP_IMPORT_EXTENSIONS.some((extension) =>
|
|
1486
|
+
file.endsWith(extension),
|
|
1487
|
+
),
|
|
1488
|
+
)
|
|
1489
|
+
.sort()
|
|
1490
|
+
|
|
1491
|
+
for (const extension of LEGACY_DEEP_IMPORT_EXTENSIONS) {
|
|
1492
|
+
for (const file of matchingFiles.filter((file) =>
|
|
1493
|
+
file.endsWith(extension),
|
|
1494
|
+
)) {
|
|
1495
|
+
const target = `./${file}`
|
|
1496
|
+
const extensionless = `./${file.slice(0, -extension.length)}`
|
|
1497
|
+
exportsMap[extensionless] ??= target
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
for (const packageManifest of matchingFiles.filter((file) =>
|
|
1502
|
+
file.endsWith('/package.json'),
|
|
1503
|
+
)) {
|
|
1504
|
+
const directory = dirname(packageManifest).split(sep).join('/')
|
|
1505
|
+
const target = resolveLegacyPackageTarget(rootDir, directory)
|
|
1506
|
+
if (packedFiles.has(target.slice(2))) {
|
|
1507
|
+
exportsMap[`./${directory}`] ??= target
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
for (const extension of LEGACY_DEEP_IMPORT_EXTENSIONS) {
|
|
1512
|
+
for (const file of matchingFiles.filter((file) =>
|
|
1513
|
+
file.endsWith(extension),
|
|
1514
|
+
)) {
|
|
1515
|
+
const indexSuffix = `/index${extension}`
|
|
1516
|
+
if (file.endsWith(indexSuffix)) {
|
|
1517
|
+
const directory = `./${file.slice(0, -indexSuffix.length)}`
|
|
1518
|
+
exportsMap[directory] ??= `./${file}`
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
return exportsMap
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function collectRootPackagePathReferences(
|
|
1527
|
+
packageJson: CommonPackageJsonFields,
|
|
1528
|
+
) {
|
|
1529
|
+
const files = new Set<string>()
|
|
1530
|
+
addLegacyPackageFileReference(files, packageJson.main)
|
|
1531
|
+
if (packageJson.main === undefined && packageJson.exports === undefined) {
|
|
1532
|
+
addLegacyPackageFileReference(files, 'index.js')
|
|
1533
|
+
}
|
|
1534
|
+
addLegacyPackageFileReference(files, packageJson.module)
|
|
1535
|
+
addLegacyPackageFileReference(files, packageJson.types)
|
|
1536
|
+
addLegacyPackageFileReference(
|
|
1537
|
+
files,
|
|
1538
|
+
(packageJson as CommonPackageJsonFields & { typings?: unknown }).typings,
|
|
1539
|
+
)
|
|
1540
|
+
addLegacyPackageFileReference(files, packageJson.browser)
|
|
1541
|
+
collectRootExportFileReferences(files, packageJson.exports)
|
|
1542
|
+
|
|
1543
|
+
const publishConfig = asRecord(packageJson.publishConfig)
|
|
1544
|
+
if (
|
|
1545
|
+
publishConfig &&
|
|
1546
|
+
Object.prototype.hasOwnProperty.call(publishConfig, 'exports')
|
|
1547
|
+
) {
|
|
1548
|
+
collectRootExportFileReferences(files, publishConfig.exports)
|
|
1549
|
+
}
|
|
1550
|
+
return [...files]
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function addLegacyPackageFileReference(files: Set<string>, target: unknown) {
|
|
1554
|
+
if (typeof target === 'string') {
|
|
1555
|
+
addPackageFileReference(files, normalizePackageTarget(target))
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function collectRootExportFileReferences(
|
|
1560
|
+
files: Set<string>,
|
|
1561
|
+
exportsField: unknown,
|
|
1562
|
+
) {
|
|
1563
|
+
const exportsMap = asRecord(exportsField)
|
|
1564
|
+
if (
|
|
1565
|
+
exportsMap &&
|
|
1566
|
+
Object.keys(exportsMap).some((key) => key.startsWith('.'))
|
|
1567
|
+
) {
|
|
1568
|
+
for (const [subpath, target] of Object.entries(exportsMap)) {
|
|
1569
|
+
if (
|
|
1570
|
+
!subpath.includes('*') &&
|
|
1571
|
+
!THREADLESS_WASI_ROOT_SUBPATHS.has(subpath)
|
|
1572
|
+
) {
|
|
1573
|
+
collectPackageTargetFileReferences(files, target)
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return
|
|
1577
|
+
}
|
|
1578
|
+
collectPackageTargetFileReferences(files, exportsField)
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function collectPackageTargetFileReferences(
|
|
1582
|
+
files: Set<string>,
|
|
1583
|
+
target: unknown,
|
|
1584
|
+
) {
|
|
1585
|
+
if (typeof target === 'string') {
|
|
1586
|
+
addPackageFileReference(files, target)
|
|
1587
|
+
} else if (Array.isArray(target)) {
|
|
1588
|
+
for (const entry of target) {
|
|
1589
|
+
collectPackageTargetFileReferences(files, entry)
|
|
1590
|
+
}
|
|
1591
|
+
} else if (typeof target === 'object' && target !== null) {
|
|
1592
|
+
for (const entry of Object.values(target)) {
|
|
1593
|
+
collectPackageTargetFileReferences(files, entry)
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function addPackageFileReference(files: Set<string>, target: string) {
|
|
1599
|
+
if (
|
|
1600
|
+
!target.startsWith('./') ||
|
|
1601
|
+
target.includes('*') ||
|
|
1602
|
+
target.endsWith('/')
|
|
1603
|
+
) {
|
|
1604
|
+
return
|
|
1605
|
+
}
|
|
1606
|
+
const file = target.slice(2).replaceAll('\\', '/')
|
|
1607
|
+
if (
|
|
1608
|
+
file.length === 0 ||
|
|
1609
|
+
file.split('/').some((segment) => segment === '..')
|
|
1610
|
+
) {
|
|
1611
|
+
return
|
|
1612
|
+
}
|
|
1613
|
+
files.add(file)
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
interface PackagePath {
|
|
1617
|
+
path: string
|
|
1618
|
+
directory: boolean
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function validateRootPackagePaths(
|
|
1622
|
+
rootDir: string,
|
|
1623
|
+
files: string[],
|
|
1624
|
+
): PackagePath[] {
|
|
1625
|
+
const paths: PackagePath[] = []
|
|
1626
|
+
for (const file of new Set(files)) {
|
|
1627
|
+
const path = join(rootDir, file)
|
|
1628
|
+
if (!existsSync(path)) {
|
|
1629
|
+
throw new Error(`Root release package is incomplete: missing ${file}`)
|
|
1630
|
+
}
|
|
1631
|
+
paths.push({ path: file, directory: statSync(path).isDirectory() })
|
|
1632
|
+
}
|
|
1633
|
+
return paths
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function readNpmPackFiles(packageDir: string, packageDescription: string) {
|
|
1637
|
+
try {
|
|
1638
|
+
const output = execSync('npm pack --dry-run --json --ignore-scripts', {
|
|
1639
|
+
cwd: packageDir,
|
|
1640
|
+
encoding: 'utf8',
|
|
1641
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1642
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1643
|
+
})
|
|
1644
|
+
const packResult = JSON.parse(output) as {
|
|
1645
|
+
files?: { path?: unknown }[]
|
|
1646
|
+
}[]
|
|
1647
|
+
if (!Array.isArray(packResult) || !Array.isArray(packResult[0]?.files)) {
|
|
1648
|
+
throw new Error('npm pack returned an unexpected JSON result')
|
|
1649
|
+
}
|
|
1650
|
+
return new Set(
|
|
1651
|
+
packResult[0].files
|
|
1652
|
+
.map(({ path }) => path)
|
|
1653
|
+
.filter((path): path is string => typeof path === 'string')
|
|
1654
|
+
.map((path) => path.replaceAll('\\', '/').replace(/^\.\//, '')),
|
|
1655
|
+
)
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
throw new Error(
|
|
1658
|
+
`Failed to validate the ${packageDescription} with npm pack --dry-run. Ensure npm is available and the package can be packed.`,
|
|
1659
|
+
{ cause: error },
|
|
1660
|
+
)
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function validateRootFacadePacklist(
|
|
1665
|
+
rootDir: string,
|
|
1666
|
+
rootPackageFiles: string[],
|
|
1667
|
+
) {
|
|
1668
|
+
const paths = validateRootPackagePaths(rootDir, rootPackageFiles)
|
|
1669
|
+
const packedFiles = readNpmPackFiles(rootDir, 'threadless WASI root package')
|
|
1670
|
+
|
|
1671
|
+
const missingPaths = paths
|
|
1672
|
+
.filter(({ path, directory }) => {
|
|
1673
|
+
const normalizedPath = path.replaceAll('\\', '/')
|
|
1674
|
+
return directory
|
|
1675
|
+
? ![...packedFiles].some((file) =>
|
|
1676
|
+
file.startsWith(`${normalizedPath}/`),
|
|
1677
|
+
)
|
|
1678
|
+
: !packedFiles.has(normalizedPath)
|
|
1679
|
+
})
|
|
1680
|
+
.map(({ path }) => path)
|
|
1681
|
+
if (missingPaths.length > 0) {
|
|
1682
|
+
throw new Error(
|
|
1683
|
+
`The threadless WASI root package references paths omitted by npm pack: ${missingPaths.join(', ')}. Add them to package.json "files" or remove the matching .npmignore rules.`,
|
|
1684
|
+
)
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
interface RootReleaseMaterializationPlan {
|
|
1689
|
+
packageJson: CommonPackageJsonFields
|
|
1690
|
+
optionalDependencies: Record<string, unknown>
|
|
1691
|
+
nodeEngine?: string
|
|
1692
|
+
facade?: ThreadlessWasiRootFacade
|
|
1693
|
+
staleGeneratedFiles: string[]
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function pathIsWithin(root: string, path: string) {
|
|
1697
|
+
const relativePath = relative(resolve(root), resolve(path))
|
|
1698
|
+
return (
|
|
1699
|
+
relativePath === '' ||
|
|
1700
|
+
(!isAbsolute(relativePath) &&
|
|
1701
|
+
relativePath !== '..' &&
|
|
1702
|
+
!relativePath.startsWith(`..${sep}`))
|
|
1703
|
+
)
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function publicationContextPath(
|
|
1707
|
+
workspaceRoot: string,
|
|
1708
|
+
contextRoot: string,
|
|
1709
|
+
sourcePath: string,
|
|
1710
|
+
) {
|
|
1711
|
+
if (!pathIsWithin(contextRoot, sourcePath)) {
|
|
1712
|
+
throw new Error(
|
|
1713
|
+
`Publication context path is outside ${contextRoot}: ${sourcePath}`,
|
|
1714
|
+
)
|
|
1715
|
+
}
|
|
1716
|
+
return join(workspaceRoot, relative(contextRoot, sourcePath))
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
async function stagePublicationContext(
|
|
1720
|
+
contextRoot: string,
|
|
1721
|
+
workspaceRoot: string,
|
|
1722
|
+
rootDir: string,
|
|
1723
|
+
releasePackagePlans: ReleasePackageMaterializationPlan[],
|
|
1724
|
+
) {
|
|
1725
|
+
const canonicalContextRoot = realpathSync.native(contextRoot)
|
|
1726
|
+
const protectedDirectories = releasePackagePlans.map(({ pkgDir }) =>
|
|
1727
|
+
realpathSync.native(pkgDir),
|
|
1728
|
+
)
|
|
1729
|
+
const selection = await collectPublicationContextSelection(
|
|
1730
|
+
canonicalContextRoot,
|
|
1731
|
+
rootDir,
|
|
1732
|
+
releasePackagePlans,
|
|
1733
|
+
)
|
|
1734
|
+
await materializePublicationContextDirectory(
|
|
1735
|
+
canonicalContextRoot,
|
|
1736
|
+
workspaceRoot,
|
|
1737
|
+
protectedDirectories,
|
|
1738
|
+
[
|
|
1739
|
+
{
|
|
1740
|
+
sourceRoot: canonicalContextRoot,
|
|
1741
|
+
destinationRoot: workspaceRoot,
|
|
1742
|
+
},
|
|
1743
|
+
],
|
|
1744
|
+
selection,
|
|
1745
|
+
)
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
interface PublicationPathMapping {
|
|
1749
|
+
sourceRoot: string
|
|
1750
|
+
destinationRoot: string
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
interface PublicationContextSelection {
|
|
1754
|
+
files: Set<string>
|
|
1755
|
+
recursiveDirectories: Set<string>
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
async function collectPublicationContextSelection(
|
|
1759
|
+
contextRoot: string,
|
|
1760
|
+
rootDir: string,
|
|
1761
|
+
releasePackagePlans: ReleasePackageMaterializationPlan[],
|
|
1762
|
+
) {
|
|
1763
|
+
const files = new Set<string>()
|
|
1764
|
+
const recursiveDirectories = new Set<string>()
|
|
1765
|
+
const structuralDirectories = new Set<string>()
|
|
1766
|
+
for (const leaf of [
|
|
1767
|
+
rootDir,
|
|
1768
|
+
...releasePackagePlans.map(({ pkgDir }) => pkgDir),
|
|
1769
|
+
]) {
|
|
1770
|
+
let current = resolve(leaf)
|
|
1771
|
+
while (pathIsWithin(contextRoot, current)) {
|
|
1772
|
+
structuralDirectories.add(current)
|
|
1773
|
+
for (const name of PUBLICATION_CONTEXT_FILES) {
|
|
1774
|
+
const path = join(current, name)
|
|
1775
|
+
if (existsSync(path)) files.add(path)
|
|
1776
|
+
}
|
|
1777
|
+
if (current === contextRoot) break
|
|
1778
|
+
current = dirname(current)
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
const yarnContext = join(contextRoot, '.yarn')
|
|
1783
|
+
if (existsSync(yarnContext)) recursiveDirectories.add(yarnContext)
|
|
1784
|
+
|
|
1785
|
+
let hasLifecycleScripts = false
|
|
1786
|
+
for (const plan of releasePackagePlans) {
|
|
1787
|
+
if (!plan.stagedPkgDir) continue
|
|
1788
|
+
const manifest = JSON.parse(
|
|
1789
|
+
await readFileAsync(join(plan.stagedPkgDir, 'package.json'), 'utf8'),
|
|
1790
|
+
) as ReleasePackageManifest
|
|
1791
|
+
const scripts = asRecord(manifest.scripts)
|
|
1792
|
+
if (!scripts) continue
|
|
1793
|
+
for (const script of publicationLifecycleScriptCommands(scripts)) {
|
|
1794
|
+
hasLifecycleScripts = true
|
|
1795
|
+
for (const reference of relativeLifecycleScriptPaths(script)) {
|
|
1796
|
+
const path = resolve(plan.pkgDir, reference)
|
|
1797
|
+
if (!pathIsWithin(contextRoot, path)) {
|
|
1798
|
+
throw new Error(
|
|
1799
|
+
`Release package lifecycle script path escapes ${contextRoot}: ${reference}`,
|
|
1800
|
+
)
|
|
1801
|
+
}
|
|
1802
|
+
if (!existsSync(path) || pathIsWithin(plan.pkgDir, path)) continue
|
|
1803
|
+
const stats = statSync(path)
|
|
1804
|
+
if (stats.isDirectory()) {
|
|
1805
|
+
recursiveDirectories.add(path)
|
|
1806
|
+
} else if (stats.isFile()) {
|
|
1807
|
+
files.add(path)
|
|
1808
|
+
const parent = dirname(path)
|
|
1809
|
+
if (parent !== contextRoot) recursiveDirectories.add(parent)
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
if (hasLifecycleScripts) {
|
|
1815
|
+
for (const directory of structuralDirectories) {
|
|
1816
|
+
const nodeModules = join(directory, 'node_modules')
|
|
1817
|
+
if (existsSync(nodeModules)) recursiveDirectories.add(nodeModules)
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
for (const directory of structuralDirectories) {
|
|
1822
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
1823
|
+
if (!entry.isSymbolicLink()) continue
|
|
1824
|
+
const path = join(directory, entry.name)
|
|
1825
|
+
let target: string
|
|
1826
|
+
try {
|
|
1827
|
+
target = realpathSync.native(path)
|
|
1828
|
+
} catch {
|
|
1829
|
+
continue
|
|
1830
|
+
}
|
|
1831
|
+
if (
|
|
1832
|
+
[...recursiveDirectories].some((selectedDirectory) =>
|
|
1833
|
+
pathIsWithin(selectedDirectory, target),
|
|
1834
|
+
)
|
|
1835
|
+
) {
|
|
1836
|
+
files.add(path)
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
return { files, recursiveDirectories }
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function stagedPublicationWorkspacePatterns(
|
|
1845
|
+
workspaces: unknown[],
|
|
1846
|
+
releaseWorkspaces: string[],
|
|
1847
|
+
) {
|
|
1848
|
+
// Yarn applies negative workspace patterns after positive matches regardless
|
|
1849
|
+
// of order. The staged tree is already selectively materialized, so retain
|
|
1850
|
+
// positive source patterns and add exact release package paths.
|
|
1851
|
+
return [
|
|
1852
|
+
...new Set([
|
|
1853
|
+
...workspaces.filter(
|
|
1854
|
+
(workspace) =>
|
|
1855
|
+
typeof workspace !== 'string' || !workspace.startsWith('!'),
|
|
1856
|
+
),
|
|
1857
|
+
...releaseWorkspaces,
|
|
1858
|
+
]),
|
|
1859
|
+
]
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function publicationLifecycleScriptCommands(scripts: Record<string, unknown>) {
|
|
1863
|
+
const commands: string[] = []
|
|
1864
|
+
const pending = [...PUBLICATION_LIFECYCLE_SCRIPTS]
|
|
1865
|
+
const visited = new Set<string>()
|
|
1866
|
+
while (pending.length > 0) {
|
|
1867
|
+
const name = pending.shift()!
|
|
1868
|
+
if (visited.has(name)) continue
|
|
1869
|
+
visited.add(name)
|
|
1870
|
+
const command = scripts[name]
|
|
1871
|
+
if (typeof command !== 'string') continue
|
|
1872
|
+
commands.push(command)
|
|
1873
|
+
for (const referencedScript of lifecycleRunScriptReferences(command)) {
|
|
1874
|
+
if (!visited.has(referencedScript)) pending.push(referencedScript)
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return commands
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function lifecycleRunScriptReferences(script: string) {
|
|
1881
|
+
const references = new Set<string>()
|
|
1882
|
+
const pattern =
|
|
1883
|
+
/\b(?:npm|pnpm|bun)\s+run(?:-script)?\s+([^\s;&|]+)|\byarn\s+(?:run\s+)?([^\s;&|]+)/gu
|
|
1884
|
+
for (const match of script.matchAll(pattern)) {
|
|
1885
|
+
references.add(match[1] ?? match[2])
|
|
1886
|
+
}
|
|
1887
|
+
return references
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function relativeLifecycleScriptPaths(script: string) {
|
|
1891
|
+
const paths = new Set<string>()
|
|
1892
|
+
const pattern = /(?:^|[\s"'`=,(])((?:\.\.?[\\/])[^ \t\r\n"'`;&|<>),]+)/gu
|
|
1893
|
+
for (const match of script.matchAll(pattern)) {
|
|
1894
|
+
paths.add(match[1])
|
|
1895
|
+
}
|
|
1896
|
+
return paths
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function publicationContextEntryIsSelected(
|
|
1900
|
+
path: string,
|
|
1901
|
+
directory: boolean,
|
|
1902
|
+
selection?: PublicationContextSelection,
|
|
1903
|
+
) {
|
|
1904
|
+
if (!selection) return true
|
|
1905
|
+
if (selection.files.has(path)) return true
|
|
1906
|
+
if (
|
|
1907
|
+
[...selection.recursiveDirectories].some((root) => pathIsWithin(root, path))
|
|
1908
|
+
) {
|
|
1909
|
+
return true
|
|
1910
|
+
}
|
|
1911
|
+
if (!directory) return false
|
|
1912
|
+
return (
|
|
1913
|
+
[...selection.recursiveDirectories].some((root) =>
|
|
1914
|
+
pathIsWithin(path, root),
|
|
1915
|
+
) || [...selection.files].some((file) => pathIsWithin(path, file))
|
|
1916
|
+
)
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
async function materializePublicationContextDirectory(
|
|
1920
|
+
sourceDir: string,
|
|
1921
|
+
destinationDir: string,
|
|
1922
|
+
protectedDirectories: string[],
|
|
1923
|
+
mappings: PublicationPathMapping[],
|
|
1924
|
+
selection?: PublicationContextSelection,
|
|
1925
|
+
) {
|
|
1926
|
+
if (
|
|
1927
|
+
protectedDirectories.some(
|
|
1928
|
+
(protectedDirectory) => relative(protectedDirectory, sourceDir) === '',
|
|
1929
|
+
)
|
|
1930
|
+
) {
|
|
1931
|
+
return
|
|
1932
|
+
}
|
|
1933
|
+
await mkdirAsync(destinationDir, { recursive: true })
|
|
1934
|
+
for (const entry of await readdir(sourceDir, { withFileTypes: true })) {
|
|
1935
|
+
if (
|
|
1936
|
+
PUBLICATION_CONTEXT_EXCLUDED_NAMES.has(entry.name) ||
|
|
1937
|
+
RECONCILIATION_STATE_PREFIXES.some((prefix) =>
|
|
1938
|
+
entry.name.startsWith(prefix),
|
|
1939
|
+
)
|
|
1940
|
+
) {
|
|
1941
|
+
continue
|
|
1942
|
+
}
|
|
1943
|
+
const source = join(sourceDir, entry.name)
|
|
1944
|
+
const destination = join(destinationDir, entry.name)
|
|
1945
|
+
if (
|
|
1946
|
+
!publicationContextEntryIsSelected(source, entry.isDirectory(), selection)
|
|
1947
|
+
) {
|
|
1948
|
+
continue
|
|
1949
|
+
}
|
|
1950
|
+
const protectedDescendant = protectedDirectories.some(
|
|
1951
|
+
(protectedDirectory) =>
|
|
1952
|
+
pathIsWithin(source, protectedDirectory) &&
|
|
1953
|
+
relative(source, protectedDirectory) !== '',
|
|
1954
|
+
)
|
|
1955
|
+
if (existsSync(destination)) {
|
|
1956
|
+
if (entry.isDirectory()) {
|
|
1957
|
+
await materializePublicationContextDirectory(
|
|
1958
|
+
source,
|
|
1959
|
+
destination,
|
|
1960
|
+
protectedDirectories,
|
|
1961
|
+
mappings,
|
|
1962
|
+
selection,
|
|
1963
|
+
)
|
|
1964
|
+
}
|
|
1965
|
+
continue
|
|
1966
|
+
}
|
|
1967
|
+
if (protectedDescendant) {
|
|
1968
|
+
await materializePublicationContextDirectory(
|
|
1969
|
+
source,
|
|
1970
|
+
destination,
|
|
1971
|
+
protectedDirectories,
|
|
1972
|
+
mappings,
|
|
1973
|
+
selection,
|
|
1974
|
+
)
|
|
1975
|
+
} else if (entry.isDirectory()) {
|
|
1976
|
+
await materializePublicationContextDirectory(
|
|
1977
|
+
source,
|
|
1978
|
+
destination,
|
|
1979
|
+
protectedDirectories,
|
|
1980
|
+
mappings,
|
|
1981
|
+
selection,
|
|
1982
|
+
)
|
|
1983
|
+
} else if (entry.isSymbolicLink()) {
|
|
1984
|
+
await materializePublicationContextSymlink(
|
|
1985
|
+
source,
|
|
1986
|
+
destination,
|
|
1987
|
+
protectedDirectories,
|
|
1988
|
+
mappings,
|
|
1989
|
+
)
|
|
1990
|
+
} else if (entry.isFile()) {
|
|
1991
|
+
await copyFile(source, destination, constants.COPYFILE_FICLONE)
|
|
1992
|
+
} else {
|
|
1993
|
+
throw new Error(`Unsupported publication context entry: ${source}`)
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
async function materializePublicationContextSymlink(
|
|
1999
|
+
source: string,
|
|
2000
|
+
destination: string,
|
|
2001
|
+
protectedDirectories: string[],
|
|
2002
|
+
mappings: PublicationPathMapping[],
|
|
2003
|
+
) {
|
|
2004
|
+
const canonicalTarget = realpathSync.native(source)
|
|
2005
|
+
const targetStats = statSync(source)
|
|
2006
|
+
const mapping = mappings
|
|
2007
|
+
.filter(({ sourceRoot }) => pathIsWithin(sourceRoot, canonicalTarget))
|
|
2008
|
+
.sort((left, right) => right.sourceRoot.length - left.sourceRoot.length)[0]
|
|
2009
|
+
if (mapping) {
|
|
2010
|
+
const mappedTarget = join(
|
|
2011
|
+
mapping.destinationRoot,
|
|
2012
|
+
relative(mapping.sourceRoot, canonicalTarget),
|
|
2013
|
+
)
|
|
2014
|
+
if (resolve(mappedTarget) === resolve(destination)) {
|
|
2015
|
+
throw new Error(`Publication context contains a cyclic link: ${source}`)
|
|
2016
|
+
}
|
|
2017
|
+
await mkdirAsync(dirname(destination), { recursive: true })
|
|
2018
|
+
const linkTarget =
|
|
2019
|
+
process.platform === 'win32' && targetStats.isDirectory()
|
|
2020
|
+
? mappedTarget
|
|
2021
|
+
: relative(dirname(destination), mappedTarget) || '.'
|
|
2022
|
+
await symlink(
|
|
2023
|
+
linkTarget,
|
|
2024
|
+
destination,
|
|
2025
|
+
targetStats.isDirectory() ? 'junction' : 'file',
|
|
2026
|
+
)
|
|
2027
|
+
return
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
if (
|
|
2031
|
+
mappings.some(({ sourceRoot }) => pathIsWithin(canonicalTarget, sourceRoot))
|
|
2032
|
+
) {
|
|
2033
|
+
throw new Error(
|
|
2034
|
+
`Publication context link expands above its materialized root: ${source}`,
|
|
2035
|
+
)
|
|
2036
|
+
}
|
|
2037
|
+
if (targetStats.isDirectory()) {
|
|
2038
|
+
await materializePublicationContextDirectory(
|
|
2039
|
+
canonicalTarget,
|
|
2040
|
+
destination,
|
|
2041
|
+
protectedDirectories,
|
|
2042
|
+
[
|
|
2043
|
+
...mappings,
|
|
2044
|
+
{
|
|
2045
|
+
sourceRoot: canonicalTarget,
|
|
2046
|
+
destinationRoot: destination,
|
|
2047
|
+
},
|
|
2048
|
+
],
|
|
2049
|
+
undefined,
|
|
2050
|
+
)
|
|
2051
|
+
} else if (targetStats.isFile()) {
|
|
2052
|
+
await copyFile(canonicalTarget, destination, constants.COPYFILE_FICLONE)
|
|
2053
|
+
} else {
|
|
2054
|
+
throw new Error(`Unsupported publication context link target: ${source}`)
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
async function registerPublicationWorkspaces(
|
|
2059
|
+
contextRoot: string,
|
|
2060
|
+
workspaceRoot: string,
|
|
2061
|
+
packageDirs: string[],
|
|
2062
|
+
) {
|
|
2063
|
+
const manifestPath = join(workspaceRoot, 'package.json')
|
|
2064
|
+
let manifest: Record<string, unknown> = { private: true }
|
|
2065
|
+
if (existsSync(manifestPath)) {
|
|
2066
|
+
try {
|
|
2067
|
+
manifest = JSON.parse(await readFileAsync(manifestPath, 'utf8'))
|
|
2068
|
+
} catch (error) {
|
|
2069
|
+
throw new Error(
|
|
2070
|
+
`Failed to read staged workspace manifest ${manifestPath}`,
|
|
2071
|
+
{ cause: error },
|
|
2072
|
+
)
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const releaseWorkspaces = packageDirs.map((packageDir) =>
|
|
2077
|
+
relative(contextRoot, packageDir).split(sep).join('/'),
|
|
2078
|
+
)
|
|
2079
|
+
const workspaces = manifest.workspaces
|
|
2080
|
+
if (Array.isArray(workspaces)) {
|
|
2081
|
+
manifest.workspaces = stagedPublicationWorkspacePatterns(
|
|
2082
|
+
workspaces,
|
|
2083
|
+
releaseWorkspaces,
|
|
2084
|
+
)
|
|
2085
|
+
} else if (
|
|
2086
|
+
typeof workspaces === 'object' &&
|
|
2087
|
+
workspaces !== null &&
|
|
2088
|
+
Array.isArray((workspaces as { packages?: unknown }).packages)
|
|
2089
|
+
) {
|
|
2090
|
+
manifest.workspaces = {
|
|
2091
|
+
...workspaces,
|
|
2092
|
+
packages: stagedPublicationWorkspacePatterns(
|
|
2093
|
+
(workspaces as { packages: unknown[] }).packages,
|
|
2094
|
+
releaseWorkspaces,
|
|
2095
|
+
),
|
|
2096
|
+
}
|
|
2097
|
+
} else {
|
|
2098
|
+
manifest.workspaces = releaseWorkspaces
|
|
2099
|
+
}
|
|
2100
|
+
await writeFileAtomic(manifestPath, JSON.stringify(manifest, null, 2))
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
async function createPublicationExecutionPackage(
|
|
2104
|
+
workspaceRoot: string,
|
|
2105
|
+
packageDir: string,
|
|
2106
|
+
) {
|
|
2107
|
+
if (!pathIsWithin(workspaceRoot, packageDir)) {
|
|
2108
|
+
throw new Error(
|
|
2109
|
+
`Prepared release package is outside its publication workspace: ${packageDir}`,
|
|
2110
|
+
)
|
|
2111
|
+
}
|
|
2112
|
+
const root = await mkdtemp(join(tmpdir(), 'napi-rs-pre-publish-execution-'))
|
|
2113
|
+
const executionWorkspaceRoot = join(root, 'workspace')
|
|
2114
|
+
try {
|
|
2115
|
+
const canonicalWorkspaceRoot = realpathSync.native(workspaceRoot)
|
|
2116
|
+
await materializePublicationContextDirectory(
|
|
2117
|
+
canonicalWorkspaceRoot,
|
|
2118
|
+
executionWorkspaceRoot,
|
|
2119
|
+
[],
|
|
2120
|
+
[
|
|
2121
|
+
{
|
|
2122
|
+
sourceRoot: canonicalWorkspaceRoot,
|
|
2123
|
+
destinationRoot: executionWorkspaceRoot,
|
|
2124
|
+
},
|
|
2125
|
+
],
|
|
2126
|
+
)
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
await removePreparedSnapshot(root, 'preparation', true, error)
|
|
2129
|
+
}
|
|
2130
|
+
return {
|
|
2131
|
+
packageDir: join(
|
|
2132
|
+
executionWorkspaceRoot,
|
|
2133
|
+
relative(workspaceRoot, packageDir),
|
|
2134
|
+
),
|
|
2135
|
+
root,
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
async function validateRootReleasePlan(
|
|
2140
|
+
packageJsonPath: string,
|
|
2141
|
+
rootDir: string,
|
|
2142
|
+
plan: RootReleaseMaterializationPlan,
|
|
2143
|
+
) {
|
|
2144
|
+
const stagingRoot = await mkdtemp(
|
|
2145
|
+
join(tmpdir(), 'napi-rs-pre-publish-root-validation-'),
|
|
2146
|
+
)
|
|
2147
|
+
const stagedRootDir = join(stagingRoot, 'package')
|
|
2148
|
+
try {
|
|
2149
|
+
await stageRootReleasePlan(packageJsonPath, rootDir, stagedRootDir, plan)
|
|
2150
|
+
} finally {
|
|
2151
|
+
await removeOwnedTemporaryTree(stagingRoot)
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
async function stageRootReleasePlan(
|
|
2156
|
+
packageJsonPath: string,
|
|
2157
|
+
rootDir: string,
|
|
2158
|
+
stagedRootDir: string,
|
|
2159
|
+
plan: RootReleaseMaterializationPlan,
|
|
2160
|
+
) {
|
|
2161
|
+
await mkdirAsync(stagedRootDir, { recursive: true })
|
|
2162
|
+
await cp(packageJsonPath, join(stagedRootDir, 'package.json'))
|
|
2163
|
+
if (plan.facade) {
|
|
2164
|
+
const packedFiles = readNpmPackFiles(rootDir, 'root package')
|
|
2165
|
+
const referencedFiles = collectRootPackagePathReferences(plan.packageJson)
|
|
2166
|
+
for (const file of new Set([
|
|
2167
|
+
...packedFiles,
|
|
2168
|
+
...referencedFiles,
|
|
2169
|
+
'.npmignore',
|
|
2170
|
+
'.gitignore',
|
|
2171
|
+
'.npmrc',
|
|
2172
|
+
])) {
|
|
2173
|
+
if (file === 'package.json') {
|
|
2174
|
+
continue
|
|
2175
|
+
}
|
|
2176
|
+
const source = join(rootDir, file)
|
|
2177
|
+
if (!existsSync(source)) {
|
|
2178
|
+
continue
|
|
2179
|
+
}
|
|
2180
|
+
const destination = join(stagedRootDir, file)
|
|
2181
|
+
await mkdirAsync(dirname(destination), { recursive: true })
|
|
2182
|
+
await copyOwnedTemporaryPath(source, destination)
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
await materializeRootReleasePlan(stagedRootDir, plan)
|
|
2186
|
+
if (plan.facade) {
|
|
2187
|
+
validateRootFacadePacklist(stagedRootDir, [
|
|
2188
|
+
...plan.facade.generatedFiles,
|
|
2189
|
+
...collectRootPackagePathReferences(plan.packageJson),
|
|
2190
|
+
])
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
async function materializeRootReleasePlan(
|
|
2195
|
+
rootDir: string,
|
|
2196
|
+
plan: RootReleaseMaterializationPlan,
|
|
2197
|
+
) {
|
|
2198
|
+
if (plan.facade) {
|
|
2199
|
+
await materializeThreadlessWasiRootFacade(rootDir, plan.facade)
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
const packageJsonPath = join(rootDir, 'package.json')
|
|
2203
|
+
const updatedPackageJson = JSON.parse(
|
|
2204
|
+
await readFileAsync(packageJsonPath, 'utf8'),
|
|
2205
|
+
)
|
|
2206
|
+
updatedPackageJson.optionalDependencies = plan.optionalDependencies
|
|
2207
|
+
if (plan.nodeEngine !== undefined) {
|
|
2208
|
+
updatedPackageJson.engines = {
|
|
2209
|
+
...asRecord(updatedPackageJson.engines),
|
|
2210
|
+
node: plan.nodeEngine,
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
syncThreadlessWasiRootFacadeManifest(updatedPackageJson, plan.packageJson)
|
|
2214
|
+
await writeFileAtomic(
|
|
2215
|
+
packageJsonPath,
|
|
2216
|
+
JSON.stringify(updatedPackageJson, null, 2),
|
|
2217
|
+
)
|
|
2218
|
+
|
|
2219
|
+
const generatedFiles = new Set(plan.facade?.generatedFiles ?? [])
|
|
2220
|
+
await Promise.all(
|
|
2221
|
+
plan.staleGeneratedFiles
|
|
2222
|
+
.filter((file) => !generatedFiles.has(file))
|
|
2223
|
+
.map((file) => rm(join(rootDir, file), { force: true })),
|
|
2224
|
+
)
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
interface ReleasePackageValidationOptions {
|
|
2228
|
+
pkgDir: string
|
|
2229
|
+
rootDir: string
|
|
2230
|
+
packageName: string
|
|
2231
|
+
binaryName: string
|
|
2232
|
+
target: Target
|
|
2233
|
+
requireDirectBufferDependency: boolean
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
interface ReleasePackageMaterializationPlan {
|
|
2237
|
+
target: Target
|
|
2238
|
+
pkgDir: string
|
|
2239
|
+
rootDir: string
|
|
2240
|
+
stagedPkgDir?: string
|
|
2241
|
+
packageFiles: string[]
|
|
2242
|
+
declarationDependencies: string[]
|
|
2243
|
+
updateManifest: boolean
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
interface ReleasePackageManifest {
|
|
2247
|
+
name?: string
|
|
2248
|
+
type?: unknown
|
|
2249
|
+
cpu?: unknown
|
|
2250
|
+
os?: unknown
|
|
2251
|
+
main?: unknown
|
|
2252
|
+
types?: unknown
|
|
2253
|
+
browser?: unknown
|
|
2254
|
+
files?: unknown
|
|
2255
|
+
exports?: unknown
|
|
2256
|
+
dependencies?: unknown
|
|
2257
|
+
scripts?: unknown
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
async function validateReleasePackage({
|
|
2261
|
+
pkgDir,
|
|
2262
|
+
...options
|
|
2263
|
+
}: ReleasePackageValidationOptions): Promise<ReleasePackageMaterializationPlan> {
|
|
2264
|
+
assertReleasePackageTree(pkgDir)
|
|
2265
|
+
const packageJsonPath = join(pkgDir, 'package.json')
|
|
2266
|
+
if (!existsSync(packageJsonPath)) {
|
|
2267
|
+
throw new Error(
|
|
2268
|
+
`Release package manifest does not exist: ${packageJsonPath}`,
|
|
2269
|
+
)
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
const stagingRoot = await mkdtemp(
|
|
2273
|
+
join(tmpdir(), 'napi-rs-pre-publish-validation-'),
|
|
2274
|
+
)
|
|
2275
|
+
const stagedPkgDir = join(stagingRoot, 'package')
|
|
2276
|
+
try {
|
|
2277
|
+
await copyOwnedTemporaryPath(pkgDir, stagedPkgDir)
|
|
2278
|
+
assertReleasePackageTree(stagedPkgDir)
|
|
2279
|
+
const validation = await validateReleasePackageContents({
|
|
2280
|
+
...options,
|
|
2281
|
+
pkgDir: stagedPkgDir,
|
|
2282
|
+
})
|
|
2283
|
+
return {
|
|
2284
|
+
pkgDir,
|
|
2285
|
+
rootDir: options.rootDir,
|
|
2286
|
+
target: options.target,
|
|
2287
|
+
...validation,
|
|
2288
|
+
}
|
|
2289
|
+
} finally {
|
|
2290
|
+
await removeOwnedTemporaryTree(stagingRoot)
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
async function stageReleasePackage(
|
|
2295
|
+
workspaceRoot: string,
|
|
2296
|
+
contextRoot: string,
|
|
2297
|
+
packageVersion: string,
|
|
2298
|
+
options: ReleasePackageValidationOptions,
|
|
2299
|
+
) {
|
|
2300
|
+
assertReleasePackageTree(options.pkgDir)
|
|
2301
|
+
const stagedPkgDir = publicationContextPath(
|
|
2302
|
+
workspaceRoot,
|
|
2303
|
+
contextRoot,
|
|
2304
|
+
options.pkgDir,
|
|
2305
|
+
)
|
|
2306
|
+
await copyOwnedTemporaryPath(options.pkgDir, stagedPkgDir)
|
|
2307
|
+
assertReleasePackageTree(stagedPkgDir)
|
|
2308
|
+
const validation = await validateReleasePackageContents({
|
|
2309
|
+
...options,
|
|
2310
|
+
pkgDir: stagedPkgDir,
|
|
2311
|
+
})
|
|
2312
|
+
const packageJsonPath = join(stagedPkgDir, 'package.json')
|
|
2313
|
+
const packageJson = JSON.parse(await readFileAsync(packageJsonPath, 'utf8'))
|
|
2314
|
+
packageJson.version = packageVersion
|
|
2315
|
+
await writeFileAtomic(packageJsonPath, JSON.stringify(packageJson, null, 2))
|
|
2316
|
+
return {
|
|
2317
|
+
pkgDir: options.pkgDir,
|
|
2318
|
+
rootDir: options.rootDir,
|
|
2319
|
+
stagedPkgDir,
|
|
2320
|
+
target: options.target,
|
|
2321
|
+
...validation,
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
function prepareReleasePackage(
|
|
2326
|
+
plan: ReleasePackageMaterializationPlan,
|
|
2327
|
+
binaryName: string,
|
|
2328
|
+
): PreparedReleasePackage {
|
|
2329
|
+
const packageDir = plan.stagedPkgDir ?? plan.pkgDir
|
|
2330
|
+
const artifactExtension =
|
|
2331
|
+
plan.target.platform === 'wasi' || plan.target.platform === 'wasm'
|
|
2332
|
+
? 'wasm'
|
|
2333
|
+
: 'node'
|
|
2334
|
+
const filename = `${binaryName}.${plan.target.platformArchABI}.${artifactExtension}`
|
|
2335
|
+
return {
|
|
2336
|
+
artifactPath: join(packageDir, filename),
|
|
2337
|
+
filename,
|
|
2338
|
+
packageDir,
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
function assertReleasePackageTree(pkgDir: string) {
|
|
2343
|
+
let stats
|
|
2344
|
+
try {
|
|
2345
|
+
stats = lstatSync(pkgDir)
|
|
2346
|
+
} catch (error) {
|
|
2347
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
2348
|
+
throw new Error(`Release package directory does not exist: ${pkgDir}`, {
|
|
2349
|
+
cause: error,
|
|
2350
|
+
})
|
|
2351
|
+
}
|
|
2352
|
+
throw error
|
|
2353
|
+
}
|
|
2354
|
+
if (stats.isSymbolicLink()) {
|
|
2355
|
+
throw new Error(
|
|
2356
|
+
`Release package directory must not be a symbolic link: ${pkgDir}`,
|
|
2357
|
+
)
|
|
2358
|
+
}
|
|
2359
|
+
if (!stats.isDirectory()) {
|
|
2360
|
+
throw new Error(`Release package path is not a directory: ${pkgDir}`)
|
|
2361
|
+
}
|
|
2362
|
+
const canonicalRoot = realpathSync.native(pkgDir)
|
|
2363
|
+
const visit = (path: string) => {
|
|
2364
|
+
const pathStats = lstatSync(path)
|
|
2365
|
+
if (pathStats.isSymbolicLink()) {
|
|
2366
|
+
throw new Error(
|
|
2367
|
+
`Release package contents must not contain symbolic links: ${path}`,
|
|
2368
|
+
)
|
|
2369
|
+
}
|
|
2370
|
+
const canonicalPath = realpathSync.native(path)
|
|
2371
|
+
if (!pathIsWithin(canonicalRoot, canonicalPath)) {
|
|
2372
|
+
throw new Error(
|
|
2373
|
+
`Release package path escapes ${canonicalRoot}: ${canonicalPath}`,
|
|
2374
|
+
)
|
|
2375
|
+
}
|
|
2376
|
+
if (pathStats.isDirectory()) {
|
|
2377
|
+
for (const entry of readdirSync(path)) {
|
|
2378
|
+
visit(join(path, entry))
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
visit(pkgDir)
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
interface CommitPrePublishFileSystemTransactionOptions {
|
|
2386
|
+
packageJsonPath: string
|
|
2387
|
+
releasePackagePlans: ReleasePackageMaterializationPlan[]
|
|
2388
|
+
rootDir: string
|
|
2389
|
+
rootReleasePlan: RootReleaseMaterializationPlan
|
|
2390
|
+
stagedRootManifestPath?: string
|
|
2391
|
+
stagedRootDir: string
|
|
2392
|
+
transactionRoot?: string
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
export async function commitPrePublishFileSystemTransaction({
|
|
2396
|
+
packageJsonPath,
|
|
2397
|
+
releasePackagePlans,
|
|
2398
|
+
rootDir,
|
|
2399
|
+
rootReleasePlan,
|
|
2400
|
+
stagedRootDir,
|
|
2401
|
+
stagedRootManifestPath = join(stagedRootDir, 'package.json'),
|
|
2402
|
+
transactionRoot = rootDir,
|
|
2403
|
+
}: CommitPrePublishFileSystemTransactionOptions) {
|
|
2404
|
+
const writes: FileSystemTransactionWrite[] = []
|
|
2405
|
+
for (const plan of releasePackagePlans) {
|
|
2406
|
+
if (!plan.stagedPkgDir) {
|
|
2407
|
+
throw new Error(
|
|
2408
|
+
`Release package ${plan.target.platformArchABI} was not staged`,
|
|
2409
|
+
)
|
|
2410
|
+
}
|
|
2411
|
+
for (const declarationFile of plan.declarationDependencies) {
|
|
2412
|
+
writes.push({
|
|
2413
|
+
destination: join(plan.pkgDir, declarationFile),
|
|
2414
|
+
source: join(plan.stagedPkgDir, declarationFile),
|
|
2415
|
+
})
|
|
2416
|
+
}
|
|
2417
|
+
writes.push({
|
|
2418
|
+
destination: join(plan.pkgDir, 'package.json'),
|
|
2419
|
+
source: join(plan.stagedPkgDir, 'package.json'),
|
|
2420
|
+
})
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
for (const generatedFile of rootReleasePlan.facade?.generatedFiles ?? []) {
|
|
2424
|
+
writes.push({
|
|
2425
|
+
destination: join(rootDir, generatedFile),
|
|
2426
|
+
source: join(stagedRootDir, generatedFile),
|
|
2427
|
+
})
|
|
2428
|
+
}
|
|
2429
|
+
// Keep the root manifest last so every package and facade mutation is
|
|
2430
|
+
// rolled back if the final publication metadata cannot be committed.
|
|
2431
|
+
writes.push({
|
|
2432
|
+
destination: packageJsonPath,
|
|
2433
|
+
source: stagedRootManifestPath,
|
|
2434
|
+
})
|
|
2435
|
+
|
|
2436
|
+
const generatedFiles = new Set(rootReleasePlan.facade?.generatedFiles ?? [])
|
|
2437
|
+
const removals = rootReleasePlan.staleGeneratedFiles
|
|
2438
|
+
.filter((file) => !generatedFiles.has(file))
|
|
2439
|
+
.map((file) => join(rootDir, file))
|
|
2440
|
+
await commitFileSystemTransaction(transactionRoot, writes, removals)
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
async function validateReleasePackageContents({
|
|
2444
|
+
pkgDir,
|
|
2445
|
+
rootDir,
|
|
2446
|
+
packageName,
|
|
2447
|
+
binaryName,
|
|
2448
|
+
target,
|
|
2449
|
+
requireDirectBufferDependency,
|
|
2450
|
+
}: ReleasePackageValidationOptions) {
|
|
2451
|
+
const packageJsonPath = join(pkgDir, 'package.json')
|
|
2452
|
+
if (!existsSync(packageJsonPath)) {
|
|
2453
|
+
throw new Error(
|
|
2454
|
+
`Release package manifest does not exist: ${packageJsonPath}`,
|
|
2455
|
+
)
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
let packageJson: ReleasePackageManifest
|
|
2459
|
+
try {
|
|
2460
|
+
packageJson = JSON.parse(await readFileAsync(packageJsonPath, 'utf8'))
|
|
2461
|
+
} catch (error) {
|
|
2462
|
+
throw new Error(`Failed to read release package ${packageJsonPath}`, {
|
|
2463
|
+
cause: error,
|
|
2464
|
+
})
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
const expectedPackageName = `${packageName}-${target.platformArchABI}`
|
|
2468
|
+
if (packageJson.name !== expectedPackageName) {
|
|
2469
|
+
throw new Error(
|
|
2470
|
+
`Release package ${pkgDir} has stale package name ${String(packageJson.name)}; expected ${expectedPackageName}`,
|
|
2471
|
+
)
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
if (
|
|
2475
|
+
!Array.isArray(packageJson.files) ||
|
|
2476
|
+
!packageJson.files.every((file): file is string => typeof file === 'string')
|
|
2477
|
+
) {
|
|
2478
|
+
throw new Error(
|
|
2479
|
+
`Release package ${expectedPackageName} must declare a string files array`,
|
|
2480
|
+
)
|
|
2481
|
+
}
|
|
2482
|
+
const packageFiles = [...new Set(packageJson.files)]
|
|
2483
|
+
for (const file of packageFiles) {
|
|
2484
|
+
resolveReleasePackageContentPath(pkgDir, file, expectedPackageName)
|
|
2485
|
+
}
|
|
2486
|
+
const packagedRuntimeImports =
|
|
2487
|
+
target.arch === 'wasm32'
|
|
2488
|
+
? await releasePackageRuntimeImports(
|
|
2489
|
+
pkgDir,
|
|
2490
|
+
expectedPackageName,
|
|
2491
|
+
packageFiles,
|
|
2492
|
+
)
|
|
2493
|
+
: new Set<string>()
|
|
2494
|
+
const packagedLoaderImportsBuffer = packagedRuntimeImports.has('buffer')
|
|
2495
|
+
|
|
2496
|
+
validateExpectedReleasePackageManifest(
|
|
2497
|
+
packageJson,
|
|
2498
|
+
packageFiles,
|
|
2499
|
+
binaryName,
|
|
2500
|
+
target,
|
|
2501
|
+
requireDirectBufferDependency || packagedLoaderImportsBuffer,
|
|
2502
|
+
packagedRuntimeImports,
|
|
2503
|
+
)
|
|
2504
|
+
|
|
2505
|
+
for (const file of packageFiles) {
|
|
2506
|
+
const path = resolveReleasePackageContentPath(
|
|
2507
|
+
pkgDir,
|
|
2508
|
+
file,
|
|
2509
|
+
expectedPackageName,
|
|
2510
|
+
)
|
|
2511
|
+
if (!existsSync(path) || !lstatSync(path).isFile()) {
|
|
2512
|
+
throw new Error(
|
|
2513
|
+
`Release package ${expectedPackageName} is incomplete: missing ${file}`,
|
|
2514
|
+
)
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
const declarationClosure = await completeDeclarationDependencyClosure({
|
|
2519
|
+
pkgDir,
|
|
2520
|
+
rootDir,
|
|
2521
|
+
packageName: expectedPackageName,
|
|
2522
|
+
packageFiles,
|
|
2523
|
+
})
|
|
2524
|
+
const updateManifest = declarationClosure.files.length > packageFiles.length
|
|
2525
|
+
if (updateManifest) {
|
|
2526
|
+
packageFiles.splice(0, packageFiles.length, ...declarationClosure.files)
|
|
2527
|
+
packageJson.files = packageFiles
|
|
2528
|
+
await writeFileAtomic(
|
|
2529
|
+
packageJsonPath,
|
|
2530
|
+
`${JSON.stringify(packageJson, null, 2)}\n`,
|
|
2531
|
+
)
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
const publicFiles = new Set<string>(packageFiles)
|
|
2535
|
+
addLegacyPackageFileReference(publicFiles, packageJson.main)
|
|
2536
|
+
addLegacyPackageFileReference(publicFiles, packageJson.types)
|
|
2537
|
+
addLegacyPackageFileReference(publicFiles, packageJson.browser)
|
|
2538
|
+
collectPackageTargetFileReferences(publicFiles, packageJson.exports)
|
|
2539
|
+
publicFiles.add('package.json')
|
|
2540
|
+
|
|
2541
|
+
const packedFiles = readNpmPackFiles(
|
|
2542
|
+
pkgDir,
|
|
2543
|
+
`release package ${expectedPackageName}`,
|
|
2544
|
+
)
|
|
2545
|
+
const omittedFiles = [...publicFiles].filter(
|
|
2546
|
+
(file) => !packedFiles.has(file.replaceAll('\\', '/')),
|
|
2547
|
+
)
|
|
2548
|
+
if (omittedFiles.length > 0) {
|
|
2549
|
+
throw new Error(
|
|
2550
|
+
`Release package ${expectedPackageName} references files omitted by npm pack: ${omittedFiles.join(', ')}`,
|
|
2551
|
+
)
|
|
2552
|
+
}
|
|
2553
|
+
return {
|
|
2554
|
+
packageFiles,
|
|
2555
|
+
declarationDependencies: declarationClosure.materializedFiles,
|
|
2556
|
+
updateManifest,
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
async function releasePackageRuntimeImports(
|
|
2561
|
+
pkgDir: string,
|
|
2562
|
+
packageName: string,
|
|
2563
|
+
packageFiles: string[],
|
|
2564
|
+
) {
|
|
2565
|
+
const runtimeImports = new Set<string>()
|
|
2566
|
+
for (const file of packageFiles) {
|
|
2567
|
+
if (!/\.(?:cjs|mjs|js)$/.test(file)) {
|
|
2568
|
+
continue
|
|
2569
|
+
}
|
|
2570
|
+
const path = resolveReleasePackageContentPath(pkgDir, file, packageName)
|
|
2571
|
+
if (!existsSync(path) || !lstatSync(path).isFile()) {
|
|
2572
|
+
continue
|
|
2573
|
+
}
|
|
2574
|
+
const source = await readFileAsync(path, 'utf8')
|
|
2575
|
+
for (const specifier of staticModuleSpecifiers(source, path)) {
|
|
2576
|
+
const dependency = wasiRuntimeDependencies.find(
|
|
2577
|
+
(candidate) =>
|
|
2578
|
+
specifier === candidate || specifier.startsWith(`${candidate}/`),
|
|
2579
|
+
)
|
|
2580
|
+
if (dependency) runtimeImports.add(dependency)
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return runtimeImports
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
function resolveReleasePackageContentPath(
|
|
2587
|
+
pkgDir: string,
|
|
2588
|
+
file: string,
|
|
2589
|
+
packageName: string,
|
|
2590
|
+
) {
|
|
2591
|
+
const normalizedFile = file.replaceAll('\\', '/')
|
|
2592
|
+
if (
|
|
2593
|
+
normalizedFile.length === 0 ||
|
|
2594
|
+
isAbsolute(file) ||
|
|
2595
|
+
normalizedFile.split('/').some((segment) => segment === '..')
|
|
2596
|
+
) {
|
|
2597
|
+
throw new Error(
|
|
2598
|
+
`Release package ${packageName} path escapes its package directory: ${file}`,
|
|
2599
|
+
)
|
|
2600
|
+
}
|
|
2601
|
+
const path = resolve(pkgDir, file)
|
|
2602
|
+
if (!pathIsWithin(pkgDir, path)) {
|
|
2603
|
+
throw new Error(
|
|
2604
|
+
`Release package ${packageName} path escapes its package directory: ${file}`,
|
|
2605
|
+
)
|
|
2606
|
+
}
|
|
2607
|
+
return path
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
function staticModuleSpecifiers(source: string, path: string) {
|
|
2611
|
+
const typescript = loadTypeScript()
|
|
2612
|
+
const sourceFile = typescript.createSourceFile(
|
|
2613
|
+
path,
|
|
2614
|
+
source,
|
|
2615
|
+
typescript.ScriptTarget.Latest,
|
|
2616
|
+
true,
|
|
2617
|
+
typescript.ScriptKind.JS,
|
|
2618
|
+
)
|
|
2619
|
+
const specifiers = new Set<string>()
|
|
2620
|
+
const addSpecifier = (value: unknown) => {
|
|
2621
|
+
if (typeof value === 'string') specifiers.add(value)
|
|
2622
|
+
}
|
|
2623
|
+
const visit = (node: import('typescript').Node) => {
|
|
2624
|
+
if (
|
|
2625
|
+
(typescript.isImportDeclaration(node) ||
|
|
2626
|
+
typescript.isExportDeclaration(node)) &&
|
|
2627
|
+
node.moduleSpecifier &&
|
|
2628
|
+
typescript.isStringLiteralLike(node.moduleSpecifier)
|
|
2629
|
+
) {
|
|
2630
|
+
addSpecifier(node.moduleSpecifier.text)
|
|
2631
|
+
} else if (
|
|
2632
|
+
typescript.isImportEqualsDeclaration(node) &&
|
|
2633
|
+
typescript.isExternalModuleReference(node.moduleReference) &&
|
|
2634
|
+
node.moduleReference.expression &&
|
|
2635
|
+
typescript.isStringLiteralLike(node.moduleReference.expression)
|
|
2636
|
+
) {
|
|
2637
|
+
addSpecifier(node.moduleReference.expression.text)
|
|
2638
|
+
} else if (
|
|
2639
|
+
typescript.isCallExpression(node) &&
|
|
2640
|
+
node.arguments.length === 1 &&
|
|
2641
|
+
typescript.isStringLiteralLike(node.arguments[0]) &&
|
|
2642
|
+
(node.expression.kind === typescript.SyntaxKind.ImportKeyword ||
|
|
2643
|
+
(typescript.isIdentifier(node.expression) &&
|
|
2644
|
+
node.expression.text === 'require') ||
|
|
2645
|
+
(typescript.isPropertyAccessExpression(node.expression) &&
|
|
2646
|
+
typescript.isIdentifier(node.expression.expression) &&
|
|
2647
|
+
node.expression.expression.text === 'require' &&
|
|
2648
|
+
node.expression.name.text === 'resolve'))
|
|
2649
|
+
) {
|
|
2650
|
+
addSpecifier(node.arguments[0].text)
|
|
2651
|
+
}
|
|
2652
|
+
typescript.forEachChild(node, visit)
|
|
2653
|
+
}
|
|
2654
|
+
visit(sourceFile)
|
|
2655
|
+
return specifiers
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
function loadTypeScript(): TypeScriptModule {
|
|
2659
|
+
loadedTypeScript ??= require('typescript') as TypeScriptModule
|
|
2660
|
+
return loadedTypeScript
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
function validateExpectedReleasePackageManifest(
|
|
2664
|
+
packageJson: ReleasePackageManifest,
|
|
2665
|
+
packageFiles: string[],
|
|
2666
|
+
binaryName: string,
|
|
2667
|
+
target: Target,
|
|
2668
|
+
requireDirectBufferDependency: boolean,
|
|
2669
|
+
packagedRuntimeImports: Set<string>,
|
|
2670
|
+
) {
|
|
2671
|
+
const isWasm = target.arch === 'wasm32'
|
|
2672
|
+
const artifactExtension = isWasm ? 'wasm' : 'node'
|
|
2673
|
+
const artifact = `${binaryName}.${target.platformArchABI}.${artifactExtension}`
|
|
2674
|
+
const expectedFiles = [artifact]
|
|
2675
|
+
const expectedMain = isWasm
|
|
2676
|
+
? `${binaryName}.${wasiLoaderSuffix(target.platformArchABI)}.cjs`
|
|
2677
|
+
: artifact
|
|
2678
|
+
|
|
2679
|
+
if (packageJson.main !== expectedMain) {
|
|
2680
|
+
throw new Error(
|
|
2681
|
+
`Release package ${packageJson.name} has stale main entry ${String(packageJson.main)}; expected ${expectedMain}`,
|
|
2682
|
+
)
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
if (isWasm) {
|
|
2686
|
+
validateWasiReleasePackageManifest(
|
|
2687
|
+
packageJson,
|
|
2688
|
+
requireDirectBufferDependency,
|
|
2689
|
+
packagedRuntimeImports,
|
|
2690
|
+
)
|
|
2691
|
+
const loaderSuffix = wasiLoaderSuffix(target.platformArchABI)
|
|
2692
|
+
const expectedTypes = `${binaryName}.${loaderSuffix}.d.cts`
|
|
2693
|
+
const expectedBrowser = `${binaryName}.${loaderSuffix}-browser.js`
|
|
2694
|
+
expectedFiles.push(expectedMain, expectedTypes, expectedBrowser)
|
|
2695
|
+
if (packageJson.types !== expectedTypes) {
|
|
2696
|
+
throw new Error(
|
|
2697
|
+
`Release package ${packageJson.name} has stale types entry ${String(packageJson.types)}; expected ${expectedTypes}`,
|
|
2698
|
+
)
|
|
2699
|
+
}
|
|
2700
|
+
if (packageJson.browser !== expectedBrowser) {
|
|
2701
|
+
throw new Error(
|
|
2702
|
+
`Release package ${packageJson.name} has stale browser entry ${String(packageJson.browser)}; expected ${expectedBrowser}`,
|
|
2703
|
+
)
|
|
2704
|
+
}
|
|
2705
|
+
if (wasiTargetHasThreads(target)) {
|
|
2706
|
+
expectedFiles.push('wasi-worker.mjs', 'wasi-worker-browser.mjs')
|
|
2707
|
+
if (packageJson.exports !== undefined) {
|
|
2708
|
+
throw new Error(
|
|
2709
|
+
`Release package ${packageJson.name} must omit exports for its threaded WASI legacy entries`,
|
|
2710
|
+
)
|
|
2711
|
+
}
|
|
2712
|
+
} else {
|
|
2713
|
+
const deferredEntry = `${binaryName}.${loaderSuffix}-deferred.js`
|
|
2714
|
+
const deferredTypeDef = `${binaryName}.${loaderSuffix}-deferred.d.ts`
|
|
2715
|
+
const wasmTypeDef = `${artifact}.d.ts`
|
|
2716
|
+
expectedFiles.push(deferredEntry, deferredTypeDef, wasmTypeDef)
|
|
2717
|
+
const exportsMap = asRecord(packageJson.exports)
|
|
2718
|
+
const expectedExports = {
|
|
2719
|
+
'.': {
|
|
2720
|
+
types: `./${expectedTypes}`,
|
|
2721
|
+
browser: `./${expectedBrowser}`,
|
|
2722
|
+
require: `./${expectedMain}`,
|
|
2723
|
+
default: `./${expectedMain}`,
|
|
2724
|
+
},
|
|
2725
|
+
'./workerd': {
|
|
2726
|
+
types: `./${deferredTypeDef}`,
|
|
2727
|
+
default: `./${deferredEntry}`,
|
|
2728
|
+
},
|
|
2729
|
+
'./wasm': {
|
|
2730
|
+
types: `./${wasmTypeDef}`,
|
|
2731
|
+
default: `./${artifact}`,
|
|
2732
|
+
},
|
|
2733
|
+
'./wasm.wasm': {
|
|
2734
|
+
types: `./${wasmTypeDef}`,
|
|
2735
|
+
default: `./${artifact}`,
|
|
2736
|
+
},
|
|
2737
|
+
'./package.json': './package.json',
|
|
2738
|
+
}
|
|
2739
|
+
for (const [subpath, expectedExport] of Object.entries(expectedExports)) {
|
|
2740
|
+
if (!isDeepStrictEqual(exportsMap?.[subpath], expectedExport)) {
|
|
2741
|
+
throw new Error(
|
|
2742
|
+
`Release package ${packageJson.name} has a stale or invalid ${subpath} export`,
|
|
2743
|
+
)
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
const missingExpectedFiles = expectedFiles.filter(
|
|
2750
|
+
(file) => !packageFiles.includes(file),
|
|
2751
|
+
)
|
|
2752
|
+
if (missingExpectedFiles.length > 0) {
|
|
2753
|
+
throw new Error(
|
|
2754
|
+
`Release package ${packageJson.name} does not publish required files: ${missingExpectedFiles.join(', ')}`,
|
|
2755
|
+
)
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
function validateWasiReleasePackageManifest(
|
|
2760
|
+
packageJson: ReleasePackageManifest,
|
|
2761
|
+
requireDirectBufferDependency: boolean,
|
|
2762
|
+
packagedRuntimeImports: Set<string>,
|
|
2763
|
+
) {
|
|
2764
|
+
if (packageJson.type !== 'module') {
|
|
2765
|
+
throw new Error(
|
|
2766
|
+
`Release package ${packageJson.name} must declare type module for its WASI JavaScript loaders`,
|
|
2767
|
+
)
|
|
2768
|
+
}
|
|
2769
|
+
if (packageJson.cpu !== undefined) {
|
|
2770
|
+
throw new Error(
|
|
2771
|
+
`Release package ${packageJson.name} must omit cpu so WASI can run on any host architecture`,
|
|
2772
|
+
)
|
|
2773
|
+
}
|
|
2774
|
+
if (packageJson.os !== undefined) {
|
|
2775
|
+
throw new Error(
|
|
2776
|
+
`Release package ${packageJson.name} must omit os so WASI can run on any host operating system`,
|
|
2777
|
+
)
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
const dependencies = asRecord(packageJson.dependencies)
|
|
2781
|
+
const declaredRuntimeDependencies = wasiRuntimeDependencies.filter(
|
|
2782
|
+
(dependency) => dependencies?.[dependency] !== undefined,
|
|
2783
|
+
)
|
|
2784
|
+
if (
|
|
2785
|
+
packagedRuntimeImports.size === 0 &&
|
|
2786
|
+
declaredRuntimeDependencies.length === 0
|
|
2787
|
+
) {
|
|
2788
|
+
return
|
|
2789
|
+
}
|
|
2790
|
+
if (!dependencies) {
|
|
2791
|
+
throw new Error(
|
|
2792
|
+
`Release package ${packageJson.name} must declare WASI runtime dependencies`,
|
|
2793
|
+
)
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
const wasmRuntimeVersion = requireStringDependency(
|
|
2797
|
+
packageJson.name,
|
|
2798
|
+
dependencies,
|
|
2799
|
+
'@napi-rs/wasm-runtime',
|
|
2800
|
+
)
|
|
2801
|
+
try {
|
|
2802
|
+
new Range(wasmRuntimeVersion)
|
|
2803
|
+
} catch {
|
|
2804
|
+
throw new Error(
|
|
2805
|
+
`Release package ${packageJson.name} has invalid @napi-rs/wasm-runtime dependency ${wasmRuntimeVersion}`,
|
|
2806
|
+
)
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
const emnapiVersion = require('emnapi/package.json').version
|
|
2810
|
+
for (const dependency of ['@emnapi/core', '@emnapi/runtime']) {
|
|
2811
|
+
const dependencyVersion = requireStringDependency(
|
|
2812
|
+
packageJson.name,
|
|
2813
|
+
dependencies,
|
|
2814
|
+
dependency,
|
|
2815
|
+
)
|
|
2816
|
+
if (dependencyVersion !== emnapiVersion) {
|
|
2817
|
+
throw new Error(
|
|
2818
|
+
`Release package ${packageJson.name} must declare ${dependency} ${emnapiVersion}; found ${dependencyVersion}`,
|
|
2819
|
+
)
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
if (requireDirectBufferDependency) {
|
|
2824
|
+
const bufferVersion = requireStringDependency(
|
|
2825
|
+
packageJson.name,
|
|
2826
|
+
dependencies,
|
|
2827
|
+
'buffer',
|
|
2828
|
+
)
|
|
2829
|
+
if (bufferVersion !== directBufferDependency) {
|
|
2830
|
+
throw new Error(
|
|
2831
|
+
`Release package ${packageJson.name} must declare buffer ${directBufferDependency}; found ${bufferVersion}`,
|
|
2832
|
+
)
|
|
2833
|
+
}
|
|
2834
|
+
} else if (dependencies.buffer !== undefined) {
|
|
2835
|
+
throw new Error(
|
|
2836
|
+
`Release package ${packageJson.name} must omit buffer when its loaders do not import it`,
|
|
2837
|
+
)
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
function requireStringDependency(
|
|
2842
|
+
packageName: string | undefined,
|
|
2843
|
+
dependencies: Record<string, unknown>,
|
|
2844
|
+
dependency: string,
|
|
2845
|
+
) {
|
|
2846
|
+
const version = dependencies[dependency]
|
|
2847
|
+
if (typeof version !== 'string' || version.trim().length === 0) {
|
|
2848
|
+
throw new Error(
|
|
2849
|
+
`Release package ${packageName} must declare dependency ${dependency}`,
|
|
2850
|
+
)
|
|
2851
|
+
}
|
|
2852
|
+
return version.trim()
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
interface DeclarationDependencyClosureOptions {
|
|
2856
|
+
pkgDir: string
|
|
2857
|
+
rootDir: string
|
|
2858
|
+
packageName: string
|
|
2859
|
+
packageFiles: string[]
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
async function completeDeclarationDependencyClosure({
|
|
2863
|
+
pkgDir,
|
|
2864
|
+
rootDir,
|
|
2865
|
+
packageName,
|
|
2866
|
+
packageFiles,
|
|
2867
|
+
}: DeclarationDependencyClosureOptions) {
|
|
2868
|
+
const includedFiles = new Set(packageFiles)
|
|
2869
|
+
const materializedFiles = new Set<string>()
|
|
2870
|
+
const queue = packageFiles.filter(isDeclarationFile)
|
|
2871
|
+
const visited = new Set<string>()
|
|
2872
|
+
|
|
2873
|
+
while (queue.length > 0) {
|
|
2874
|
+
const declarationFile = queue.shift()!
|
|
2875
|
+
if (visited.has(declarationFile)) {
|
|
2876
|
+
continue
|
|
2877
|
+
}
|
|
2878
|
+
visited.add(declarationFile)
|
|
2879
|
+
const declaration = await readFileAsync(
|
|
2880
|
+
join(pkgDir, declarationFile),
|
|
2881
|
+
'utf8',
|
|
2882
|
+
)
|
|
2883
|
+
for (const specifier of extractRelativeDeclarationSpecifiers(declaration)) {
|
|
2884
|
+
const dependency = resolveDeclarationDependency(
|
|
2885
|
+
pkgDir,
|
|
2886
|
+
declarationFile,
|
|
2887
|
+
specifier,
|
|
2888
|
+
)
|
|
2889
|
+
const sourceDependency =
|
|
2890
|
+
dependency ??
|
|
2891
|
+
resolveDeclarationDependency(rootDir, declarationFile, specifier)
|
|
2892
|
+
if (!sourceDependency) {
|
|
2893
|
+
throw new Error(
|
|
2894
|
+
`Release package ${packageName} declaration ${declarationFile} references missing ${specifier}`,
|
|
2895
|
+
)
|
|
2896
|
+
}
|
|
2897
|
+
if (!dependency) {
|
|
2898
|
+
const destination = join(pkgDir, sourceDependency)
|
|
2899
|
+
await mkdirAsync(dirname(destination), { recursive: true })
|
|
2900
|
+
await copyFileAtomic(join(rootDir, sourceDependency), destination)
|
|
2901
|
+
materializedFiles.add(sourceDependency)
|
|
2902
|
+
}
|
|
2903
|
+
if (!includedFiles.has(sourceDependency)) {
|
|
2904
|
+
includedFiles.add(sourceDependency)
|
|
2905
|
+
if (isDeclarationFile(sourceDependency)) {
|
|
2906
|
+
queue.push(sourceDependency)
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
return {
|
|
2912
|
+
files: [...includedFiles],
|
|
2913
|
+
materializedFiles: [...materializedFiles],
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
function extractRelativeDeclarationSpecifiers(source: string) {
|
|
2918
|
+
return new Set(collectRelativeDeclarationSpecifiers(source))
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
function resolveDeclarationDependency(
|
|
2922
|
+
baseDir: string,
|
|
2923
|
+
declarationFile: string,
|
|
2924
|
+
specifier: string,
|
|
2925
|
+
) {
|
|
2926
|
+
const canonicalBaseDir = realpathSync.native(baseDir)
|
|
2927
|
+
const unresolved = resolve(dirname(join(baseDir, declarationFile)), specifier)
|
|
2928
|
+
const unresolvedRelative = relative(baseDir, unresolved)
|
|
2929
|
+
const nodeModulesDir = join(resolve(baseDir), 'node_modules')
|
|
2930
|
+
if (
|
|
2931
|
+
unresolvedRelative === '..' ||
|
|
2932
|
+
unresolvedRelative.startsWith(`..${sep}`) ||
|
|
2933
|
+
unresolved === nodeModulesDir ||
|
|
2934
|
+
unresolved.startsWith(`${nodeModulesDir}${sep}`)
|
|
2935
|
+
) {
|
|
2936
|
+
return undefined
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
for (const candidate of declarationDependencyCandidates(unresolved)) {
|
|
2940
|
+
if (
|
|
2941
|
+
existsSync(candidate) &&
|
|
2942
|
+
lstatSync(candidate).isFile() &&
|
|
2943
|
+
pathIsWithin(canonicalBaseDir, realpathSync.native(candidate))
|
|
2944
|
+
) {
|
|
2945
|
+
return relative(baseDir, candidate).split(sep).join('/')
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
return undefined
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
function declarationDependencyCandidates(unresolved: string) {
|
|
2952
|
+
if (isDeclarationFile(unresolved)) {
|
|
2953
|
+
return [unresolved]
|
|
2954
|
+
}
|
|
2955
|
+
const extension = extname(unresolved)
|
|
2956
|
+
if (extension === '.cjs') {
|
|
2957
|
+
return [`${unresolved.slice(0, -extension.length)}.d.cts`]
|
|
2958
|
+
}
|
|
2959
|
+
if (extension === '.mjs') {
|
|
2960
|
+
return [`${unresolved.slice(0, -extension.length)}.d.mts`]
|
|
2961
|
+
}
|
|
2962
|
+
if (extension === '.js' || extension === '.jsx') {
|
|
2963
|
+
return [`${unresolved.slice(0, -extension.length)}.d.ts`]
|
|
2964
|
+
}
|
|
2965
|
+
if (extension.length > 0) {
|
|
2966
|
+
return [unresolved]
|
|
2967
|
+
}
|
|
2968
|
+
return [
|
|
2969
|
+
...DECLARATION_EXTENSIONS.map((candidate) => `${unresolved}${candidate}`),
|
|
2970
|
+
...DECLARATION_EXTENSIONS.map((candidate) =>
|
|
2971
|
+
join(unresolved, `index${candidate}`),
|
|
2972
|
+
),
|
|
2973
|
+
]
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
function isDeclarationFile(file: string) {
|
|
2977
|
+
return DECLARATION_EXTENSIONS.some((extension) => file.endsWith(extension))
|
|
232
2978
|
}
|
|
233
2979
|
|
|
234
2980
|
function parseTag(tag: string) {
|