@napi-rs/cli 3.7.3 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +33 -0
  2. package/cli.mjs +9 -12
  3. package/dist/cli.js +9277 -1313
  4. package/dist/index.cjs +9266 -1302
  5. package/dist/index.d.cts +176 -158
  6. package/dist/index.d.ts +176 -158
  7. package/dist/index.js +9277 -1313
  8. package/docs/artifacts.md +33 -0
  9. package/docs/build.md +58 -0
  10. package/docs/create-npm-dirs.md +32 -0
  11. package/docs/new.md +39 -0
  12. package/docs/pre-publish.md +37 -0
  13. package/docs/rename.md +37 -0
  14. package/docs/universalize.md +31 -0
  15. package/docs/version.md +31 -0
  16. package/docs/wasi.md +161 -0
  17. package/package.json +11 -10
  18. package/src/api/__tests__/__snapshots__/templates.spec.ts.md +1019 -114
  19. package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
  20. package/src/api/__tests__/artifacts.spec.ts +17 -0
  21. package/src/api/__tests__/create-npm-dirs.spec.ts +14 -15
  22. package/src/api/__tests__/new.spec.ts +41 -5
  23. package/src/api/artifacts.ts +934 -108
  24. package/src/api/build.ts +1325 -194
  25. package/src/api/create-npm-dirs.ts +472 -101
  26. package/src/api/new.ts +354 -35
  27. package/src/api/pre-publish.ts +2845 -99
  28. package/src/api/rename.ts +1638 -124
  29. package/src/api/templates/js-binding.ts +216 -38
  30. package/src/api/templates/load-wasi-template.ts +1641 -111
  31. package/src/api/templates/wasi-worker-template.ts +32 -3
  32. package/src/commands/rename.ts +8 -3
  33. package/src/def/artifacts.ts +2 -2
  34. package/src/utils/__tests__/__fixtures__/optional-napi-derive/Cargo.toml +3 -0
  35. package/src/utils/__tests__/__fixtures__/optional-napi-derive/main-crate/Cargo.toml +8 -0
  36. package/src/utils/__tests__/__fixtures__/optional-napi-derive/main-crate/src/lib.rs +1 -0
  37. package/src/utils/__tests__/__fixtures__/optional-napi-derive/napi-derive/Cargo.toml +5 -0
  38. package/src/utils/__tests__/__fixtures__/optional-napi-derive/napi-derive/src/lib.rs +1 -0
  39. package/src/utils/__tests__/__fixtures__/optional-napi-derive/with-optional-derive/Cargo.toml +11 -0
  40. package/src/utils/__tests__/__fixtures__/optional-napi-derive/with-optional-derive/src/lib.rs +1 -0
  41. package/src/utils/__tests__/__snapshots__/target.spec.ts.md +2 -2
  42. package/src/utils/__tests__/__snapshots__/target.spec.ts.snap +0 -0
  43. package/src/utils/__tests__/metadata.spec.ts +43 -2
  44. package/src/utils/__tests__/misc.spec.ts +124 -3
  45. package/src/utils/__tests__/version.spec.ts +47 -1
  46. package/src/utils/config.ts +15 -2
  47. package/src/utils/metadata.ts +206 -7
  48. package/src/utils/misc.ts +6079 -0
  49. package/src/utils/target.ts +66 -8
  50. package/src/utils/typegen.ts +1223 -63
  51. package/src/utils/version.ts +96 -0
package/src/api/rename.ts CHANGED
@@ -1,162 +1,1676 @@
1
- import { existsSync } from 'node:fs'
2
- import { rename } from 'node:fs/promises'
3
- import { resolve, join } from 'node:path'
1
+ import { lstatSync, realpathSync } from 'node:fs'
2
+ import { builtinModules } from 'node:module'
3
+ import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import {
6
+ basename,
7
+ dirname,
8
+ isAbsolute,
9
+ join,
10
+ relative,
11
+ resolve,
12
+ sep,
13
+ } from 'node:path'
4
14
 
5
15
  import { parse as parseToml, stringify as stringifyToml } from '@std/toml'
6
16
  import { load as yamlParse, dump as yamlStringify } from 'js-yaml'
7
- import { isNil, merge, omitBy, pick } from 'es-toolkit'
8
- import * as find from 'empathic/find'
17
+ import { isNil, omitBy, pick } from 'es-toolkit'
9
18
 
10
19
  import { applyDefaultRenameOptions, type RenameOptions } from '../def/rename.js'
11
- import { readConfig, readFileAsync, writeFileAsync } from '../utils/index.js'
20
+ import {
21
+ commitFileSystemTransaction,
22
+ readConfig,
23
+ readFileAsync,
24
+ resolvePackageReconciliationPaths,
25
+ type Target,
26
+ wasiLoaderSuffix,
27
+ wasiTargetHasThreads,
28
+ withPackageFileSystemReconciliation,
29
+ } from '../utils/index.js'
12
30
 
13
- export async function renameProject(userOptions: RenameOptions) {
14
- const options = applyDefaultRenameOptions(userOptions)
15
- const napiConfig = await readConfig(options)
16
- const oldName = napiConfig.binaryName
31
+ const WASI_ARTIFACT_METADATA_PREFIX = '// napi-rs-artifact-metadata:'
32
+ const SCOPED_PACKAGE_PATTERN = /^(?:@([^/]+?)\/)?([^/]+?)$/
33
+ const EXCLUDED_PACKAGE_NAMES = new Set(['node_modules', 'favicon.ico'])
34
+ const WINDOWS_RESERVED_FILENAME =
35
+ /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i
36
+ const PACKAGE_REFERENCE_FIELDS = new Set([
37
+ 'browser',
38
+ 'exports',
39
+ 'files',
40
+ 'imports',
41
+ 'main',
42
+ 'man',
43
+ 'module',
44
+ 'types',
45
+ 'typesVersions',
46
+ 'typings',
47
+ ])
48
+ const DEPENDENCY_FIELDS = new Set([
49
+ 'dependencies',
50
+ 'devDependencies',
51
+ 'optionalDependencies',
52
+ 'peerDependencies',
53
+ 'peerDependenciesMeta',
54
+ ])
55
+ const RECURSIVE_PACKAGE_KEY_FIELDS = new Set(['overrides', 'resolutions'])
56
+ const PNPM_PACKAGE_ARRAY_FIELDS = new Set([
57
+ 'ignoredBuiltDependencies',
58
+ 'ignoredOptionalDependencies',
59
+ 'minimumReleaseAgeExclude',
60
+ 'neverBuiltDependencies',
61
+ 'onlyBuiltDependencies',
62
+ 'trustPolicyExclude',
63
+ ])
17
64
 
18
- const packageJsonPath = resolve(options.cwd, options.packageJsonPath)
19
- const cargoTomlPath = resolve(options.cwd, options.manifestPath)
65
+ type JsonRecord = Record<string, unknown>
20
66
 
21
- const packageJsonContent = await readFileAsync(packageJsonPath, 'utf8')
22
- const packageJsonData = JSON.parse(packageJsonContent)
23
-
24
- merge(
25
- merge(
26
- packageJsonData,
27
- omitBy(
28
- // @ts-expect-error missing fields: author and license
29
- pick(options, ['name', 'description', 'author', 'license']),
30
- isNil,
31
- ),
32
- ),
33
- {
34
- napi: omitBy(
35
- {
36
- binaryName: options.binaryName,
37
- packageName: options.packageName,
38
- },
39
- isNil,
40
- ),
41
- },
67
+ interface ManagedFileRename {
68
+ source: string
69
+ destination: string
70
+ }
71
+
72
+ interface PlannedManagedFileRename extends ManagedFileRename {
73
+ replacesSameFile: boolean
74
+ }
75
+
76
+ interface PreparedWrite {
77
+ content: Buffer
78
+ destination: string
79
+ mode: number
80
+ removeBeforeWrite?: string
81
+ }
82
+
83
+ class RenameTransactionPlan {
84
+ readonly removals = new Set<string>()
85
+ readonly writes = new Map<string, PreparedWrite>()
86
+
87
+ addWrite(
88
+ destination: string,
89
+ content: Buffer | string,
90
+ mode: number,
91
+ removeBeforeWrite?: string,
92
+ ) {
93
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content)
94
+ const existing = this.writes.get(destination)
95
+ if (existing) {
96
+ if (
97
+ !existing.content.equals(buffer) ||
98
+ existing.mode !== mode ||
99
+ existing.removeBeforeWrite !== removeBeforeWrite
100
+ ) {
101
+ throw new Error(
102
+ `Rename prepared conflicting updates for ${destination}`,
103
+ )
104
+ }
105
+ return
106
+ }
107
+ this.writes.set(destination, {
108
+ content: buffer,
109
+ destination,
110
+ mode,
111
+ removeBeforeWrite,
112
+ })
113
+ }
114
+
115
+ hasWrite(destination: string) {
116
+ return this.writes.has(destination)
117
+ }
118
+ }
119
+
120
+ function pathExists(path: string) {
121
+ try {
122
+ lstatSync(path)
123
+ return true
124
+ } catch (error) {
125
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
126
+ return false
127
+ }
128
+ throw error
129
+ }
130
+ }
131
+
132
+ function assertRegularFile(path: string, description: string) {
133
+ const stats = lstatSync(path)
134
+ if (!stats.isFile()) {
135
+ throw new Error(`${description} must be a regular file: ${path}`)
136
+ }
137
+ return stats
138
+ }
139
+
140
+ function fileMode(path: string, description: string) {
141
+ return assertRegularFile(path, description).mode & 0o7777
142
+ }
143
+
144
+ function assertSafeBinaryName(name: unknown, description: string) {
145
+ const hasControlCharacter =
146
+ typeof name === 'string' &&
147
+ [...name].some((character) => character.charCodeAt(0) <= 0x1f)
148
+ if (
149
+ typeof name !== 'string' ||
150
+ name.length === 0 ||
151
+ name === '.' ||
152
+ name === '..' ||
153
+ name.trim() !== name ||
154
+ isAbsolute(name) ||
155
+ /[<>:"/\\|?*]/.test(name) ||
156
+ hasControlCharacter ||
157
+ name.endsWith('.') ||
158
+ WINDOWS_RESERVED_FILENAME.test(name)
159
+ ) {
160
+ throw new Error(
161
+ `${description} must be a safe filename stem: ${JSON.stringify(name)}`,
162
+ )
163
+ }
164
+ }
165
+
166
+ function npmPackageNameIssues(name: unknown) {
167
+ if (typeof name !== 'string') {
168
+ return { errors: ['name must be a string'], warnings: [] }
169
+ }
170
+
171
+ const errors: string[] = []
172
+ const warnings: string[] = []
173
+ if (name.length === 0) {
174
+ errors.push('name length must be greater than zero')
175
+ }
176
+ if (name.length > 214) {
177
+ warnings.push('name can no longer contain more than 214 characters')
178
+ }
179
+ if (name.startsWith('.')) {
180
+ errors.push('name cannot start with a period')
181
+ }
182
+ if (name.startsWith('_')) {
183
+ errors.push('name cannot start with an underscore')
184
+ }
185
+ if (name.trim() !== name) {
186
+ errors.push('name cannot contain leading or trailing spaces')
187
+ }
188
+ if (name.toLowerCase() !== name) {
189
+ warnings.push('name can no longer contain capital letters')
190
+ }
191
+ if (EXCLUDED_PACKAGE_NAMES.has(name.toLowerCase())) {
192
+ errors.push(`${name} is not a valid package name`)
193
+ }
194
+ if (builtinModules.includes(name.toLowerCase())) {
195
+ warnings.push(`${name} is a core module name`)
196
+ }
197
+
198
+ const match = name.match(SCOPED_PACKAGE_PATTERN)
199
+ const scope = match?.[1]
200
+ const packageName = match?.[2]
201
+ if (packageName && /[~'!()*]/.test(packageName)) {
202
+ warnings.push('name can no longer contain special characters ("~\'!()*")')
203
+ }
204
+ if (
205
+ !packageName ||
206
+ packageName.startsWith('.') ||
207
+ (scope
208
+ ? encodeURIComponent(scope) !== scope ||
209
+ encodeURIComponent(packageName) !== packageName
210
+ : encodeURIComponent(name) !== name)
211
+ ) {
212
+ errors.push('name can only contain URL-friendly characters')
213
+ }
214
+ return {
215
+ errors: [...new Set(errors)],
216
+ warnings: [...new Set(warnings)],
217
+ }
218
+ }
219
+
220
+ function assertValidNpmPackageName(
221
+ name: unknown,
222
+ description: string,
223
+ strict: boolean,
224
+ ): asserts name is string {
225
+ const { errors, warnings } = npmPackageNameIssues(name)
226
+ const issues = strict ? [...errors, ...warnings] : errors
227
+ if (issues.length > 0) {
228
+ throw new Error(
229
+ `${description} is not a valid npm package name: ${issues.join('; ')}`,
230
+ )
231
+ }
232
+ }
233
+
234
+ function validatePackageIdentity(
235
+ packageName: unknown,
236
+ targets: Target[],
237
+ description: string,
238
+ strict: boolean,
239
+ ) {
240
+ assertValidNpmPackageName(packageName, description, strict)
241
+ for (const target of targets) {
242
+ assertValidNpmPackageName(
243
+ `${packageName}-${target.platformArchABI}`,
244
+ `${description} flavor for ${target.triple}`,
245
+ strict,
246
+ )
247
+ }
248
+ }
249
+
250
+ function createManagedWasiFiles(binaryName: string, targets: Target[]) {
251
+ const files = new Set<string>()
252
+ const add = (suffix: string) => files.add(`${binaryName}.${suffix}`)
253
+ const wasiTargets = targets.filter((target) => target.platform === 'wasi')
254
+ const flavors =
255
+ wasiTargets.length > 0
256
+ ? wasiTargets.map((target) => ({
257
+ hasThreads: wasiTargetHasThreads(target),
258
+ platformArchABI: target.platformArchABI,
259
+ }))
260
+ : [{ hasThreads: true, platformArchABI: 'wasm32-wasi' }]
261
+
262
+ for (const flavor of flavors) {
263
+ const loaderSuffix = wasiLoaderSuffix(flavor.platformArchABI)
264
+ for (const suffix of [
265
+ `${flavor.platformArchABI}.wasm`,
266
+ `${flavor.platformArchABI}.debug.wasm`,
267
+ `${loaderSuffix}.cjs`,
268
+ `${loaderSuffix}.d.cts`,
269
+ `${loaderSuffix}-browser.js`,
270
+ ]) {
271
+ add(suffix)
272
+ }
273
+ if (!flavor.hasThreads) {
274
+ for (const suffix of [
275
+ `${flavor.platformArchABI}.wasm.d.ts`,
276
+ `${flavor.platformArchABI}.wasm.d.mts`,
277
+ `${flavor.platformArchABI}.workerd.mjs`,
278
+ `${flavor.platformArchABI}.workerd.d.mts`,
279
+ `${loaderSuffix}-deferred.js`,
280
+ `${loaderSuffix}-deferred.d.ts`,
281
+ ]) {
282
+ add(suffix)
283
+ }
284
+ }
285
+ }
286
+
287
+ add('wasm')
288
+ add('debug.wasm')
289
+ return files
290
+ }
291
+
292
+ function createManagedWasiRenames(
293
+ oldName: string,
294
+ newName: string,
295
+ targets: Target[],
296
+ ) {
297
+ const renames = new Map<string, string>()
298
+ for (const oldFile of createManagedWasiFiles(oldName, targets)) {
299
+ renames.set(oldFile, `${newName}${oldFile.slice(oldName.length)}`)
300
+ }
301
+ return renames
302
+ }
303
+
304
+ function createManagedPackageRenames(
305
+ oldPackageName: string,
306
+ newPackageName: string,
307
+ targets: Target[],
308
+ ) {
309
+ return new Map(
310
+ targets.map((target) => [
311
+ `${oldPackageName}-${target.platformArchABI}`,
312
+ `${newPackageName}-${target.platformArchABI}`,
313
+ ]),
42
314
  )
315
+ }
43
316
 
44
- if (options.repository) {
317
+ function replaceManifestReference(
318
+ value: string,
319
+ fileRenames: Map<string, string>,
320
+ packageRenames: Map<string, string>,
321
+ ) {
322
+ const directFile = fileRenames.get(value)
323
+ if (directFile) {
324
+ return directFile
325
+ }
326
+ if (value.startsWith('./')) {
327
+ const relativeFile = fileRenames.get(value.slice(2))
328
+ if (relativeFile) {
329
+ return `./${relativeFile}`
330
+ }
331
+ }
332
+ return replacePackageSelectors(value, packageRenames, false)
333
+ }
334
+
335
+ function isPackageSelectorPrefix(
336
+ value: string,
337
+ index: number,
338
+ packageName: string,
339
+ allowSelectorPrefix: boolean,
340
+ ) {
341
+ if (index === 0 || (index === 4 && value.startsWith('npm:'))) {
342
+ return true
343
+ }
344
+ if (!allowSelectorPrefix) {
345
+ return false
346
+ }
347
+
348
+ const previous = value[index - 1]
349
+ if (previous !== '/' && previous !== '>') {
350
+ return false
351
+ }
352
+ if (previous === '/' && !packageName.startsWith('@')) {
353
+ const previousSlash = value.lastIndexOf('/', index - 2)
354
+ const previousSegment = value
355
+ .slice(previousSlash + 1, index - 1)
356
+ .replace(/^npm:/, '')
357
+ if (previousSegment.startsWith('@')) {
358
+ return false
359
+ }
360
+ }
361
+ return true
362
+ }
363
+
364
+ function replacePackageSelectors(
365
+ value: string,
366
+ packageRenames: Map<string, string>,
367
+ allowSelectorPrefix: boolean,
368
+ ) {
369
+ let cursor = 0
370
+ let updated = ''
371
+ const renames = [...packageRenames].sort(
372
+ ([left], [right]) => right.length - left.length,
373
+ )
374
+
375
+ while (cursor < value.length) {
376
+ let match: { index: number; name: string; replacement: string } | undefined
377
+ for (const [name, replacement] of renames) {
378
+ let index = value.indexOf(name, cursor)
379
+ while (index !== -1) {
380
+ const next = value[index + name.length]
381
+ if (
382
+ isPackageSelectorPrefix(value, index, name, allowSelectorPrefix) &&
383
+ (next === undefined || next === '/' || next === '@')
384
+ ) {
385
+ break
386
+ }
387
+ index = value.indexOf(name, index + name.length)
388
+ }
389
+ if (
390
+ index !== -1 &&
391
+ (!match ||
392
+ index < match.index ||
393
+ (index === match.index && name.length > match.name.length))
394
+ ) {
395
+ match = { index, name, replacement }
396
+ }
397
+ }
398
+
399
+ if (!match) {
400
+ updated += value.slice(cursor)
401
+ break
402
+ }
403
+ updated += value.slice(cursor, match.index)
404
+ updated += match.replacement
405
+ cursor = match.index + match.name.length
406
+ }
407
+ return updated || value
408
+ }
409
+
410
+ function defineJsonProperty(target: JsonRecord, key: string, value: unknown) {
411
+ Object.defineProperty(target, key, {
412
+ configurable: true,
413
+ enumerable: true,
414
+ value,
415
+ writable: true,
416
+ })
417
+ }
418
+
419
+ function createJsonRecord() {
420
+ return Object.create(null) as JsonRecord
421
+ }
422
+
423
+ function rewriteReferenceTree(
424
+ value: unknown,
425
+ fileRenames: Map<string, string>,
426
+ packageRenames: Map<string, string>,
427
+ rewriteKeys = true,
428
+ ): unknown {
429
+ if (typeof value === 'string') {
430
+ return replaceManifestReference(value, fileRenames, packageRenames)
431
+ }
432
+ if (Array.isArray(value)) {
433
+ return value.map((entry) =>
434
+ rewriteReferenceTree(entry, fileRenames, packageRenames, rewriteKeys),
435
+ )
436
+ }
437
+ if (typeof value !== 'object' || value === null) {
438
+ return value
439
+ }
440
+
441
+ const updated = createJsonRecord()
442
+ for (const [key, entry] of Object.entries(value)) {
443
+ const updatedKey = rewriteKeys
444
+ ? replaceManifestReference(key, fileRenames, packageRenames)
445
+ : key
446
+ if (Object.prototype.hasOwnProperty.call(updated, updatedKey)) {
447
+ throw new Error(
448
+ `Renaming managed manifest reference ${key} conflicts with existing key ${updatedKey}`,
449
+ )
450
+ }
451
+ defineJsonProperty(
452
+ updated,
453
+ updatedKey,
454
+ rewriteReferenceTree(entry, fileRenames, packageRenames, rewriteKeys),
455
+ )
456
+ }
457
+ return updated
458
+ }
459
+
460
+ function rewritePackageMap(
461
+ value: unknown,
462
+ packageRenames: Map<string, string>,
463
+ recursive: boolean,
464
+ ): unknown {
465
+ if (Array.isArray(value)) {
466
+ return rewritePackageSelectorArray(value, packageRenames)
467
+ }
468
+ if (typeof value !== 'object' || value === null) {
469
+ return value
470
+ }
471
+
472
+ const updated = createJsonRecord()
473
+ for (const [key, entry] of Object.entries(value)) {
474
+ const updatedKey = replacePackageSelectors(key, packageRenames, true)
475
+ if (Object.prototype.hasOwnProperty.call(updated, updatedKey)) {
476
+ throw new Error(
477
+ `Renaming package reference ${key} conflicts with existing key ${updatedKey}`,
478
+ )
479
+ }
480
+ const updatedEntry =
481
+ typeof entry === 'string'
482
+ ? replacePackageDescriptor(entry, packageRenames)
483
+ : recursive
484
+ ? rewritePackageMap(entry, packageRenames, true)
485
+ : entry
486
+ defineJsonProperty(updated, updatedKey, updatedEntry)
487
+ }
488
+ return updated
489
+ }
490
+
491
+ function replacePackageDescriptor(
492
+ value: string,
493
+ packageRenames: Map<string, string>,
494
+ ) {
495
+ if (value.startsWith('npm:')) {
496
+ return replacePackageSelectors(value, packageRenames, false)
497
+ }
498
+ if (!value.startsWith('patch:')) {
499
+ return value
500
+ }
501
+
502
+ const source = value.slice('patch:'.length)
503
+ const packageSeparator = source.startsWith('@')
504
+ ? source.indexOf('@', source.indexOf('/') + 1)
505
+ : source.indexOf('@')
506
+ if (packageSeparator <= 0) {
507
+ return value
508
+ }
509
+ const packageName = source.slice(0, packageSeparator)
510
+ const replacement = packageRenames.get(packageName)
511
+ return replacement
512
+ ? `patch:${replacement}${source.slice(packageSeparator)}`
513
+ : value
514
+ }
515
+
516
+ function rewritePackageSelectorArray(
517
+ value: unknown,
518
+ packageRenames: Map<string, string>,
519
+ ) {
520
+ if (!Array.isArray(value)) {
521
+ return value
522
+ }
523
+ return value.map((entry) =>
524
+ typeof entry === 'string'
525
+ ? replacePackageSelectors(entry, packageRenames, true)
526
+ : entry,
527
+ )
528
+ }
529
+
530
+ function rewritePackageSelectorKeys(
531
+ value: unknown,
532
+ packageRenames: Map<string, string>,
533
+ ) {
534
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
535
+ return value
536
+ }
537
+ const updated = createJsonRecord()
538
+ for (const [key, entry] of Object.entries(value)) {
539
+ const updatedKey = replacePackageSelectors(key, packageRenames, true)
540
+ if (Object.prototype.hasOwnProperty.call(updated, updatedKey)) {
541
+ throw new Error(
542
+ `Renaming package reference ${key} conflicts with existing key ${updatedKey}`,
543
+ )
544
+ }
545
+ defineJsonProperty(updated, updatedKey, entry)
546
+ }
547
+ return updated
548
+ }
549
+
550
+ function rewritePackageExtensions(
551
+ value: unknown,
552
+ packageRenames: Map<string, string>,
553
+ ) {
554
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
555
+ return value
556
+ }
557
+ const updated = createJsonRecord()
558
+ for (const [key, entry] of Object.entries(value)) {
559
+ const updatedKey = replacePackageSelectors(key, packageRenames, true)
560
+ if (Object.prototype.hasOwnProperty.call(updated, updatedKey)) {
561
+ throw new Error(
562
+ `Renaming package reference ${key} conflicts with existing key ${updatedKey}`,
563
+ )
564
+ }
565
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
566
+ defineJsonProperty(updated, updatedKey, entry)
567
+ continue
568
+ }
569
+
570
+ const updatedExtension = createJsonRecord()
571
+ for (const [field, fieldValue] of Object.entries(entry)) {
572
+ defineJsonProperty(
573
+ updatedExtension,
574
+ field,
575
+ DEPENDENCY_FIELDS.has(field)
576
+ ? rewritePackageMap(fieldValue, packageRenames, false)
577
+ : fieldValue,
578
+ )
579
+ }
580
+ defineJsonProperty(updated, updatedKey, updatedExtension)
581
+ }
582
+ return updated
583
+ }
584
+
585
+ function rewritePnpmPeerDependencyRules(
586
+ value: unknown,
587
+ packageRenames: Map<string, string>,
588
+ ) {
589
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
590
+ return value
591
+ }
592
+ const updated = createJsonRecord()
593
+ for (const [key, entry] of Object.entries(value)) {
594
+ defineJsonProperty(
595
+ updated,
596
+ key,
597
+ key === 'ignoreMissing' || key === 'allowAny'
598
+ ? rewritePackageSelectorArray(entry, packageRenames)
599
+ : key === 'allowedVersions'
600
+ ? rewritePackageSelectorKeys(entry, packageRenames)
601
+ : entry,
602
+ )
603
+ }
604
+ return updated
605
+ }
606
+
607
+ function rewritePnpmUpdateConfig(
608
+ value: unknown,
609
+ packageRenames: Map<string, string>,
610
+ ) {
611
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
612
+ return value
613
+ }
614
+ const updated = createJsonRecord()
615
+ for (const [key, entry] of Object.entries(value)) {
616
+ defineJsonProperty(
617
+ updated,
618
+ key,
619
+ key === 'ignoreDependencies'
620
+ ? rewritePackageSelectorArray(entry, packageRenames)
621
+ : entry,
622
+ )
623
+ }
624
+ return updated
625
+ }
626
+
627
+ function rewritePnpmConfig(
628
+ value: unknown,
629
+ packageRenames: Map<string, string>,
630
+ ) {
631
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
632
+ return value
633
+ }
634
+ const updated = createJsonRecord()
635
+ for (const [key, entry] of Object.entries(value)) {
636
+ let rewritten = entry
637
+ if (key === 'overrides') {
638
+ rewritten = rewritePackageMap(entry, packageRenames, true)
639
+ } else if (key === 'patchedDependencies') {
640
+ rewritten = rewritePackageSelectorKeys(entry, packageRenames)
641
+ } else if (key === 'packageExtensions') {
642
+ rewritten = rewritePackageExtensions(entry, packageRenames)
643
+ } else if (key === 'allowedDeprecatedVersions') {
644
+ rewritten = rewritePackageSelectorKeys(entry, packageRenames)
645
+ } else if (key === 'allowBuilds') {
646
+ rewritten = rewritePackageSelectorKeys(entry, packageRenames)
647
+ } else if (key === 'peerDependencyRules') {
648
+ rewritten = rewritePnpmPeerDependencyRules(entry, packageRenames)
649
+ } else if (key === 'updateConfig') {
650
+ rewritten = rewritePnpmUpdateConfig(entry, packageRenames)
651
+ } else if (PNPM_PACKAGE_ARRAY_FIELDS.has(key)) {
652
+ rewritten = rewritePackageSelectorArray(entry, packageRenames)
653
+ }
654
+ defineJsonProperty(updated, key, rewritten)
655
+ }
656
+ return updated
657
+ }
658
+
659
+ function replaceScriptArgument(
660
+ argument: string,
661
+ fileRenames: Map<string, string>,
662
+ packageRenames: Map<string, string>,
663
+ ) {
664
+ const direct = replaceManifestReference(argument, fileRenames, packageRenames)
665
+ if (direct !== argument) {
666
+ return direct
667
+ }
668
+
669
+ const assignment = argument.match(
670
+ /^([A-Za-z_][A-Za-z0-9_]*|--?[A-Za-z0-9][A-Za-z0-9-]*)=(.+)$/,
671
+ )
672
+ if (!assignment) {
673
+ return argument
674
+ }
675
+ const replacement = replaceManifestReference(
676
+ assignment[2],
677
+ fileRenames,
678
+ packageRenames,
679
+ )
680
+ return replacement === assignment[2]
681
+ ? argument
682
+ : `${assignment[1]}=${replacement}`
683
+ }
684
+
685
+ function rewriteScriptCommand(
686
+ command: string,
687
+ fileRenames: Map<string, string>,
688
+ packageRenames: Map<string, string>,
689
+ ) {
690
+ let index = 0
691
+ let updated = ''
692
+ while (index < command.length) {
693
+ const character = command[index]
694
+ if (/\s|[|&;()<>]/.test(character)) {
695
+ updated += character
696
+ index += 1
697
+ continue
698
+ }
699
+
700
+ if (character === '"' || character === "'") {
701
+ const quote = character
702
+ let end = index + 1
703
+ while (end < command.length) {
704
+ if (command[end] === quote) {
705
+ break
706
+ }
707
+ if (
708
+ quote === '"' &&
709
+ command[end] === '\\' &&
710
+ end + 1 < command.length
711
+ ) {
712
+ end += 2
713
+ } else {
714
+ end += 1
715
+ }
716
+ }
717
+ if (end >= command.length) {
718
+ updated += command.slice(index)
719
+ break
720
+ }
721
+ const argument = command.slice(index + 1, end)
722
+ updated += `${quote}${replaceScriptArgument(
723
+ argument,
724
+ fileRenames,
725
+ packageRenames,
726
+ )}${quote}`
727
+ index = end + 1
728
+ continue
729
+ }
730
+
731
+ let end = index + 1
732
+ while (end < command.length && !/\s|[|&;()<>"']/.test(command[end])) {
733
+ end += 1
734
+ }
735
+ updated += replaceScriptArgument(
736
+ command.slice(index, end),
737
+ fileRenames,
738
+ packageRenames,
739
+ )
740
+ index = end
741
+ }
742
+ return updated
743
+ }
744
+
745
+ function rewriteScripts(
746
+ value: unknown,
747
+ fileRenames: Map<string, string>,
748
+ packageRenames: Map<string, string>,
749
+ ) {
750
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
751
+ return value
752
+ }
753
+ const updated = createJsonRecord()
754
+ for (const [key, entry] of Object.entries(value)) {
755
+ defineJsonProperty(
756
+ updated,
757
+ key,
758
+ typeof entry === 'string'
759
+ ? rewriteScriptCommand(entry, fileRenames, packageRenames)
760
+ : entry,
761
+ )
762
+ }
763
+ return updated
764
+ }
765
+
766
+ function rewritePublishConfig(
767
+ value: unknown,
768
+ fileRenames: Map<string, string>,
769
+ packageRenames: Map<string, string>,
770
+ ) {
771
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
772
+ return value
773
+ }
774
+ const updated = createJsonRecord()
775
+ for (const [key, entry] of Object.entries(value)) {
776
+ defineJsonProperty(
777
+ updated,
778
+ key,
779
+ PACKAGE_REFERENCE_FIELDS.has(key)
780
+ ? rewriteReferenceTree(entry, fileRenames, packageRenames)
781
+ : entry,
782
+ )
783
+ }
784
+ return updated
785
+ }
786
+
787
+ function rewritePackageManifest(
788
+ manifest: JsonRecord,
789
+ fileRenames: Map<string, string>,
790
+ packageRenames: Map<string, string>,
791
+ ) {
792
+ const updated = createJsonRecord()
793
+ for (const [key, entry] of Object.entries(manifest)) {
794
+ let rewritten = entry
795
+ if (key === 'name' && typeof entry === 'string') {
796
+ rewritten = replaceManifestReference(entry, new Map(), packageRenames)
797
+ } else if (PACKAGE_REFERENCE_FIELDS.has(key)) {
798
+ rewritten = rewriteReferenceTree(entry, fileRenames, packageRenames)
799
+ } else if (key === 'bin') {
800
+ rewritten = rewriteReferenceTree(
801
+ entry,
802
+ fileRenames,
803
+ packageRenames,
804
+ false,
805
+ )
806
+ } else if (DEPENDENCY_FIELDS.has(key)) {
807
+ rewritten = rewritePackageMap(entry, packageRenames, false)
808
+ } else if (key === 'dependenciesMeta') {
809
+ rewritten = rewritePackageSelectorKeys(entry, packageRenames)
810
+ } else if (key === 'trustedDependencies') {
811
+ rewritten = rewritePackageSelectorArray(entry, packageRenames)
812
+ } else if (key === 'bundledDependencies' || key === 'bundleDependencies') {
813
+ rewritten = rewritePackageMap(entry, packageRenames, false)
814
+ } else if (RECURSIVE_PACKAGE_KEY_FIELDS.has(key)) {
815
+ rewritten = rewritePackageMap(entry, packageRenames, true)
816
+ } else if (key === 'pnpm') {
817
+ rewritten = rewritePnpmConfig(entry, packageRenames)
818
+ } else if (key === 'scripts') {
819
+ rewritten = rewriteScripts(entry, fileRenames, packageRenames)
820
+ } else if (key === 'publishConfig') {
821
+ rewritten = rewritePublishConfig(entry, fileRenames, packageRenames)
822
+ }
823
+ defineJsonProperty(updated, key, rewritten)
824
+ }
825
+ return updated
826
+ }
827
+
828
+ function replaceManagedTextReferences(
829
+ content: string,
830
+ fileRenames: Map<string, string>,
831
+ packageRenames: Map<string, string>,
832
+ ) {
833
+ const references = new Map<
834
+ string,
835
+ { replacement: string; packageName: boolean }
836
+ >()
837
+ for (const [name, replacement] of fileRenames) {
838
+ references.set(name, { replacement, packageName: false })
839
+ }
840
+ for (const [name, replacement] of packageRenames) {
841
+ references.set(name, { replacement, packageName: true })
842
+ }
843
+ if (references.size === 0) {
844
+ return content
845
+ }
846
+
847
+ const pattern = new RegExp(
848
+ [...references.keys()]
849
+ .sort((left, right) => right.length - left.length)
850
+ .map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
851
+ .join('|'),
852
+ 'g',
853
+ )
854
+ return content.replace(pattern, (name: string, offset: number) => {
855
+ const reference = references.get(name)!
856
+ const previous = content[offset - 1]
857
+ const next = content[offset + name.length]
858
+ if (reference.packageName) {
859
+ if (previous && /[A-Za-z0-9._~@/-]/.test(previous)) {
860
+ return name
861
+ }
862
+ if (next && next !== '/' && /[A-Za-z0-9._~-]/.test(next)) {
863
+ return name
864
+ }
865
+ } else {
866
+ if (previous && /[A-Za-z0-9._-]/.test(previous)) {
867
+ return name
868
+ }
869
+ if (next && /[A-Za-z0-9._-]/.test(next)) {
870
+ return name
871
+ }
872
+ }
873
+ return reference.replacement
874
+ })
875
+ }
876
+
877
+ function isPathWithin(root: string, path: string, allowRoot: boolean) {
878
+ const relativePath = relative(root, path)
879
+ return (
880
+ (allowRoot && relativePath === '') ||
881
+ (relativePath !== '' &&
882
+ relativePath !== '..' &&
883
+ !relativePath.startsWith(`..${sep}`) &&
884
+ !isAbsolute(relativePath))
885
+ )
886
+ }
887
+
888
+ function assertPathWithin(
889
+ root: string,
890
+ path: string,
891
+ description: string,
892
+ allowRoot = false,
893
+ ) {
894
+ if (!isPathWithin(root, path, allowRoot)) {
895
+ throw new Error(`${description} escapes project root ${root}: ${path}`)
896
+ }
897
+ }
898
+
899
+ function assertRenamePathIdentity(
900
+ expected: string,
901
+ actual: string,
902
+ description: string,
903
+ ) {
904
+ if (relative(expected, actual) === '' && relative(actual, expected) === '') {
905
+ return
906
+ }
907
+ const error = new Error(
908
+ `${description} changed after package reconciliation paths were resolved: ${expected} -> ${actual}`,
909
+ ) as NodeJS.ErrnoException
910
+ error.code = 'ESTALE'
911
+ error.path = actual
912
+ throw error
913
+ }
914
+
915
+ function revalidateRenameReconciliationPaths(
916
+ options: ReturnType<typeof applyDefaultRenameOptions>,
917
+ expected: ReturnType<typeof resolvePackageReconciliationPaths>,
918
+ ) {
919
+ let actual: ReturnType<typeof resolvePackageReconciliationPaths>
920
+ try {
921
+ actual = resolvePackageReconciliationPaths(
922
+ options.cwd,
923
+ options.packageJsonPath,
924
+ [options.npmDir],
925
+ )
926
+ } catch (cause) {
927
+ const error = new Error(
928
+ 'Package reconciliation paths changed after their locks were acquired',
929
+ { cause },
930
+ ) as NodeJS.ErrnoException
931
+ error.code = 'ESTALE'
932
+ error.path = expected.boundary
933
+ throw error
934
+ }
935
+
936
+ assertRenamePathIdentity(
937
+ expected.boundary,
938
+ actual.boundary,
939
+ 'Package reconciliation boundary',
940
+ )
941
+ assertRenamePathIdentity(expected.cwd, actual.cwd, 'Project root')
942
+ assertRenamePathIdentity(
943
+ expected.packageRoot,
944
+ actual.packageRoot,
945
+ 'Package root',
946
+ )
947
+ assertRenamePathIdentity(
948
+ expected.packageJsonPath,
949
+ actual.packageJsonPath,
950
+ 'Package manifest',
951
+ )
952
+ if (expected.managedPaths.length !== actual.managedPaths.length) {
953
+ const error = new Error(
954
+ 'Managed package paths changed after their locks were acquired',
955
+ ) as NodeJS.ErrnoException
956
+ error.code = 'ESTALE'
957
+ error.path = actual.cwd
958
+ throw error
959
+ }
960
+ for (let index = 0; index < expected.managedPaths.length; index++) {
961
+ assertRenamePathIdentity(
962
+ expected.managedPaths[index],
963
+ actual.managedPaths[index],
964
+ `Managed package path ${index + 1}`,
965
+ )
966
+ }
967
+ return actual
968
+ }
969
+
970
+ async function canonicalizePath(path: string) {
971
+ let current = resolve(path)
972
+ const missingSegments: string[] = []
973
+ while (true) {
974
+ try {
975
+ return join(await realpath(current), ...missingSegments)
976
+ } catch (error) {
977
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
978
+ throw error
979
+ }
980
+ const parent = dirname(current)
981
+ if (parent === current) {
982
+ return join(current, ...missingSegments)
983
+ }
984
+ missingSegments.unshift(basename(current))
985
+ current = parent
986
+ }
987
+ }
988
+ }
989
+
990
+ async function resolveCanonicalRoot(
991
+ projectRoot: string,
992
+ path: string,
993
+ description: string,
994
+ ) {
995
+ const lexicalPath = resolve(path)
996
+ assertPathWithin(projectRoot, lexicalPath, description, true)
997
+ const canonicalPath = await canonicalizePath(lexicalPath)
998
+ assertPathWithin(projectRoot, canonicalPath, description, true)
999
+ return canonicalPath
1000
+ }
1001
+
1002
+ async function resolveCanonicalFile(
1003
+ projectRoot: string,
1004
+ path: string,
1005
+ description: string,
1006
+ ) {
1007
+ const lexicalPath = resolve(path)
1008
+ assertPathWithin(projectRoot, lexicalPath, description)
1009
+ const canonicalParent = await canonicalizePath(dirname(lexicalPath))
1010
+ assertPathWithin(projectRoot, canonicalParent, `${description} parent`, true)
1011
+ const canonicalPath = join(canonicalParent, basename(lexicalPath))
1012
+ assertPathWithin(projectRoot, canonicalPath, description)
1013
+ if (pathExists(canonicalPath)) {
1014
+ assertRegularFile(canonicalPath, description)
1015
+ }
1016
+ return canonicalPath
1017
+ }
1018
+
1019
+ function resolveManagedPath(
1020
+ directory: string,
1021
+ entry: string,
1022
+ description: string,
1023
+ ) {
1024
+ if (!entry || isAbsolute(entry)) {
1025
+ throw new Error(
1026
+ `${description} must be a non-empty relative path: ${entry}`,
1027
+ )
1028
+ }
1029
+ const root = resolve(directory)
1030
+ const path = resolve(root, entry)
1031
+ assertPathWithin(root, path, description)
1032
+ return path
1033
+ }
1034
+
1035
+ async function resolveProjectEntry(
1036
+ projectRoot: string,
1037
+ entry: string,
1038
+ description: string,
1039
+ ) {
1040
+ const path = resolve(projectRoot, entry)
1041
+ if (!isPathWithin(projectRoot, path, false)) {
1042
+ return
1043
+ }
1044
+ return resolveCanonicalFile(projectRoot, path, description)
1045
+ }
1046
+
1047
+ function collectManagedFileRenames(
1048
+ directory: string,
1049
+ renames: Map<string, string>,
1050
+ ) {
1051
+ const operations: ManagedFileRename[] = []
1052
+ for (const [oldFile, newFile] of renames) {
1053
+ const source = resolveManagedPath(
1054
+ directory,
1055
+ oldFile,
1056
+ 'Managed rename source',
1057
+ )
1058
+ const destination = resolveManagedPath(
1059
+ directory,
1060
+ newFile,
1061
+ 'Managed rename destination',
1062
+ )
1063
+ if (source !== destination && pathExists(source)) {
1064
+ assertRegularFile(source, 'Managed rename source')
1065
+ operations.push({ source, destination })
1066
+ }
1067
+ }
1068
+ return operations
1069
+ }
1070
+
1071
+ function pathsReferToSameFile(left: string, right: string) {
1072
+ try {
1073
+ const leftStats = lstatSync(left)
1074
+ const rightStats = lstatSync(right)
1075
+ if (!leftStats.isFile() || !rightStats.isFile()) {
1076
+ return false
1077
+ }
1078
+ const leftPath = realpathSync.native(left)
1079
+ const rightPath = realpathSync.native(right)
45
1080
  if (
46
- packageJsonData.repository &&
47
- typeof packageJsonData.repository === 'object' &&
48
- !Array.isArray(packageJsonData.repository)
1081
+ leftPath === rightPath ||
1082
+ (process.platform === 'win32' &&
1083
+ leftPath.toLowerCase() === rightPath.toLowerCase())
49
1084
  ) {
50
- packageJsonData.repository.url = options.repository
51
- } else {
52
- packageJsonData.repository = options.repository
1085
+ return true
1086
+ }
1087
+ return (
1088
+ (process.platform === 'darwin' || process.platform === 'win32') &&
1089
+ resolve(left).toLowerCase() === resolve(right).toLowerCase() &&
1090
+ leftStats.dev === rightStats.dev &&
1091
+ leftStats.ino === rightStats.ino
1092
+ )
1093
+ } catch (error) {
1094
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
1095
+ return false
53
1096
  }
1097
+ throw error
54
1098
  }
1099
+ }
55
1100
 
56
- if (options.configPath) {
57
- const configPath = resolve(options.cwd, options.configPath)
58
- const configContent = await readFileAsync(configPath, 'utf8')
59
- const configData = JSON.parse(configContent)
60
- merge(
61
- configData,
62
- omitBy(
63
- {
64
- binaryName: options.binaryName,
65
- packageName: options.packageName,
66
- },
67
- isNil,
68
- ),
1101
+ function preflightManagedFileRenames(operations: ManagedFileRename[]) {
1102
+ const operationsBySource = new Map<string, ManagedFileRename>()
1103
+ const sources = new Set<string>()
1104
+ const destinations = new Map<string, string>()
1105
+
1106
+ for (const operation of operations) {
1107
+ const existingSource = operationsBySource.get(operation.source)
1108
+ if (existingSource) {
1109
+ if (existingSource.destination !== operation.destination) {
1110
+ throw new Error(
1111
+ `Managed rename source ${operation.source} has conflicting destinations ${existingSource.destination} and ${operation.destination}`,
1112
+ )
1113
+ }
1114
+ continue
1115
+ }
1116
+ operationsBySource.set(operation.source, operation)
1117
+ sources.add(operation.source)
1118
+
1119
+ const existingDestination = destinations.get(operation.destination)
1120
+ if (existingDestination) {
1121
+ throw new Error(
1122
+ `Managed rename destination ${operation.destination} is targeted by both ${existingDestination} and ${operation.source}`,
1123
+ )
1124
+ }
1125
+ destinations.set(operation.destination, operation.source)
1126
+ }
1127
+
1128
+ const planned: PlannedManagedFileRename[] = []
1129
+ for (const operation of operationsBySource.values()) {
1130
+ const replacesSameFile =
1131
+ pathExists(operation.destination) &&
1132
+ pathsReferToSameFile(operation.source, operation.destination)
1133
+ if (
1134
+ pathExists(operation.destination) &&
1135
+ !sources.has(operation.destination) &&
1136
+ !replacesSameFile
1137
+ ) {
1138
+ throw new Error(
1139
+ `Cannot rename managed file ${operation.source}: destination already exists: ${operation.destination}`,
1140
+ )
1141
+ }
1142
+ planned.push({ ...operation, replacesSameFile })
1143
+ }
1144
+ return planned
1145
+ }
1146
+
1147
+ async function prepareManagedFileRenames(
1148
+ plan: RenameTransactionPlan,
1149
+ operations: PlannedManagedFileRename[],
1150
+ fileRenames: Map<string, string>,
1151
+ packageRenames: Map<string, string>,
1152
+ ) {
1153
+ const destinations = new Set(
1154
+ operations.map((operation) => operation.destination),
1155
+ )
1156
+ for (const operation of operations) {
1157
+ const mode = fileMode(operation.source, 'Managed rename source')
1158
+ const content = await readFileAsync(operation.source)
1159
+ plan.addWrite(
1160
+ operation.destination,
1161
+ operation.source.endsWith('.wasm')
1162
+ ? content
1163
+ : replaceManagedTextReferences(
1164
+ content.toString('utf8'),
1165
+ fileRenames,
1166
+ packageRenames,
1167
+ ),
1168
+ mode,
1169
+ operation.replacesSameFile ? operation.source : undefined,
1170
+ )
1171
+ if (!operation.replacesSameFile && !destinations.has(operation.source)) {
1172
+ plan.removals.add(operation.source)
1173
+ }
1174
+ }
1175
+ }
1176
+
1177
+ async function prepareManagedTextWrite(
1178
+ plan: RenameTransactionPlan,
1179
+ path: string,
1180
+ fileRenames: Map<string, string>,
1181
+ packageRenames: Map<string, string>,
1182
+ ) {
1183
+ if (plan.hasWrite(path) || !pathExists(path) || path.endsWith('.wasm')) {
1184
+ return
1185
+ }
1186
+ const mode = fileMode(path, 'Managed text file')
1187
+ const content = await readFileAsync(path, 'utf8')
1188
+ const updated = replaceManagedTextReferences(
1189
+ content,
1190
+ fileRenames,
1191
+ packageRenames,
1192
+ )
1193
+ if (updated !== content) {
1194
+ plan.addWrite(path, updated, mode)
1195
+ }
1196
+ }
1197
+
1198
+ function managedPackageReadme(packageName: string, target: Target) {
1199
+ return `# \`${packageName}-${target.platformArchABI}\`
1200
+
1201
+ This is the **${target.triple}** binary for \`${packageName}\`
1202
+ `
1203
+ }
1204
+
1205
+ async function prepareManagedPackageReadme(
1206
+ plan: RenameTransactionPlan,
1207
+ directory: string,
1208
+ oldPackageName: string,
1209
+ newPackageName: string,
1210
+ target: Target,
1211
+ ) {
1212
+ const path = resolveManagedPath(
1213
+ directory,
1214
+ 'README.md',
1215
+ 'Managed package README',
1216
+ )
1217
+ if (!pathExists(path)) {
1218
+ return
1219
+ }
1220
+ const mode = fileMode(path, 'Managed package README')
1221
+ const content = await readFileAsync(path, 'utf8')
1222
+ if (content === managedPackageReadme(oldPackageName, target)) {
1223
+ plan.addWrite(path, managedPackageReadme(newPackageName, target), mode)
1224
+ }
1225
+ }
1226
+
1227
+ function managedRootEntries(content: string) {
1228
+ const firstLine = content.split(/\r?\n/, 1)[0]
1229
+ if (!firstLine.startsWith(WASI_ARTIFACT_METADATA_PREFIX)) {
1230
+ return []
1231
+ }
1232
+ try {
1233
+ const metadata = JSON.parse(
1234
+ firstLine.slice(WASI_ARTIFACT_METADATA_PREFIX.length),
1235
+ ) as {
1236
+ rootEntry?: unknown
1237
+ managedRootEntries?: unknown
1238
+ }
1239
+ return [
1240
+ ...(typeof metadata.rootEntry === 'string' ? [metadata.rootEntry] : []),
1241
+ ...(Array.isArray(metadata.managedRootEntries)
1242
+ ? metadata.managedRootEntries.filter(
1243
+ (entry): entry is string => typeof entry === 'string',
1244
+ )
1245
+ : []),
1246
+ ]
1247
+ } catch {
1248
+ return []
1249
+ }
1250
+ }
1251
+
1252
+ function asJsonRecord(value: unknown): JsonRecord | undefined {
1253
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
1254
+ ? (value as JsonRecord)
1255
+ : undefined
1256
+ }
1257
+
1258
+ function updateNapiConfigRecord(record: JsonRecord, options: RenameOptions) {
1259
+ if (options.binaryName !== undefined) {
1260
+ defineJsonProperty(record, 'binaryName', options.binaryName)
1261
+ delete record.name
1262
+ }
1263
+ if (options.packageName !== undefined) {
1264
+ defineJsonProperty(record, 'packageName', options.packageName)
1265
+ }
1266
+ }
1267
+
1268
+ function serializeJsonLike(content: string, value: unknown) {
1269
+ return `${JSON.stringify(value, null, 2)}${content.endsWith('\n') ? '\n' : ''}`
1270
+ }
1271
+
1272
+ function sanitizeCargoPackageName(binaryName: string) {
1273
+ return binaryName.replace(/[^A-Za-z0-9_]/g, '_').toLowerCase()
1274
+ }
1275
+
1276
+ async function executeRenameTransaction(
1277
+ projectRoot: string,
1278
+ plan: RenameTransactionPlan,
1279
+ ) {
1280
+ if (plan.writes.size === 0 && plan.removals.size === 0) {
1281
+ return
1282
+ }
1283
+
1284
+ const stagingRoot = await mkdtemp(join(tmpdir(), 'napi-rename-stage-'))
1285
+ try {
1286
+ const writes = []
1287
+ let sequence = 0
1288
+ for (const prepared of plan.writes.values()) {
1289
+ const source = join(stagingRoot, String(sequence++))
1290
+ await writeFile(source, prepared.content, { mode: prepared.mode })
1291
+ writes.push({
1292
+ destination: prepared.destination,
1293
+ mode: prepared.mode,
1294
+ removeBeforeWrite: prepared.removeBeforeWrite,
1295
+ source,
1296
+ })
1297
+ }
1298
+
1299
+ await commitFileSystemTransaction(projectRoot, writes, [...plan.removals])
1300
+ } finally {
1301
+ await rm(stagingRoot, { force: true, recursive: true })
1302
+ }
1303
+ }
1304
+
1305
+ async function renameProjectUnlocked(
1306
+ userOptions: RenameOptions,
1307
+ resolvedPaths: ReturnType<typeof resolvePackageReconciliationPaths>,
1308
+ ) {
1309
+ const options = applyDefaultRenameOptions(userOptions)
1310
+ const lockedPaths = revalidateRenameReconciliationPaths(
1311
+ options,
1312
+ resolvedPaths,
1313
+ )
1314
+ const projectRoot = await realpath(resolve(options.cwd))
1315
+ assertRenamePathIdentity(lockedPaths.cwd, projectRoot, 'Project root')
1316
+ const requestedPackageJsonPath = await resolveCanonicalFile(
1317
+ projectRoot,
1318
+ resolve(projectRoot, options.packageJsonPath),
1319
+ 'package.json',
1320
+ )
1321
+ const packageJsonPath = await realpath(requestedPackageJsonPath)
1322
+ assertRenamePathIdentity(
1323
+ lockedPaths.packageJsonPath,
1324
+ packageJsonPath,
1325
+ 'Package manifest',
1326
+ )
1327
+ const configPath = options.configPath
1328
+ ? await resolveCanonicalFile(
1329
+ projectRoot,
1330
+ resolve(projectRoot, options.configPath),
1331
+ 'NAPI config',
1332
+ )
1333
+ : undefined
1334
+ const npmRoot = await resolveCanonicalRoot(
1335
+ projectRoot,
1336
+ resolve(projectRoot, options.npmDir),
1337
+ 'npm package root',
1338
+ )
1339
+ assertRenamePathIdentity(
1340
+ lockedPaths.managedPaths[0],
1341
+ npmRoot,
1342
+ 'npm package root',
1343
+ )
1344
+ const normalizedOptions = {
1345
+ ...options,
1346
+ cwd: projectRoot,
1347
+ packageJsonPath,
1348
+ configPath,
1349
+ npmDir: npmRoot,
1350
+ }
1351
+
1352
+ const napiConfig = await readConfig(normalizedOptions)
1353
+ const oldName = napiConfig.binaryName
1354
+ const oldPackageName = napiConfig.packageName
1355
+
1356
+ assertSafeBinaryName(oldName, 'Configured binary name')
1357
+ validatePackageIdentity(
1358
+ oldPackageName,
1359
+ napiConfig.targets,
1360
+ 'Configured package name',
1361
+ false,
1362
+ )
1363
+ if (options.binaryName !== undefined) {
1364
+ assertSafeBinaryName(options.binaryName, 'Requested binary name')
1365
+ }
1366
+ if (options.name !== undefined) {
1367
+ assertValidNpmPackageName(options.name, 'Requested root package name', true)
1368
+ }
1369
+ if (options.packageName !== undefined) {
1370
+ validatePackageIdentity(
1371
+ options.packageName,
1372
+ napiConfig.targets,
1373
+ 'Requested package name',
1374
+ true,
69
1375
  )
70
- await writeFileAsync(configPath, JSON.stringify(configData, null, 2))
71
1376
  }
72
1377
 
73
- await writeFileAsync(
1378
+ const newName = options.binaryName ?? oldName
1379
+ const newPackageName = options.packageName ?? oldPackageName
1380
+ const binaryNameChanged =
1381
+ options.binaryName !== undefined && oldName !== options.binaryName
1382
+ const packageNameChanged =
1383
+ options.packageName !== undefined && oldPackageName !== options.packageName
1384
+ const managedWasiRenames = binaryNameChanged
1385
+ ? createManagedWasiRenames(oldName, newName, napiConfig.targets)
1386
+ : new Map<string, string>()
1387
+ const managedPackageRenames = packageNameChanged
1388
+ ? createManagedPackageRenames(
1389
+ oldPackageName,
1390
+ newPackageName,
1391
+ napiConfig.targets,
1392
+ )
1393
+ : new Map<string, string>()
1394
+ const hasManagedReferenceRenames =
1395
+ managedWasiRenames.size > 0 || managedPackageRenames.size > 0
1396
+
1397
+ const packageJsonMode = fileMode(packageJsonPath, 'package.json')
1398
+ const packageJsonContent = await readFileAsync(packageJsonPath, 'utf8')
1399
+ const parsedPackageJson = JSON.parse(packageJsonContent)
1400
+ const packageJsonData = asJsonRecord(parsedPackageJson)
1401
+ if (!packageJsonData) {
1402
+ throw new Error(
1403
+ `package.json must contain a JSON object: ${packageJsonPath}`,
1404
+ )
1405
+ }
1406
+
1407
+ const managedRootEntryNames = new Set<string>()
1408
+ if (hasManagedReferenceRenames) {
1409
+ for (const field of ['main', 'module', 'browser', 'types'] as const) {
1410
+ const entry = packageJsonData[field]
1411
+ if (typeof entry === 'string') {
1412
+ managedRootEntryNames.add(entry)
1413
+ }
1414
+ }
1415
+ for (const oldFile of createManagedWasiFiles(oldName, napiConfig.targets)) {
1416
+ if (!oldFile.endsWith('.cjs')) {
1417
+ continue
1418
+ }
1419
+ const loaderPath = resolveManagedPath(
1420
+ projectRoot,
1421
+ oldFile,
1422
+ 'Managed WASI loader',
1423
+ )
1424
+ if (pathExists(loaderPath)) {
1425
+ assertRegularFile(loaderPath, 'Managed WASI loader')
1426
+ for (const entry of managedRootEntries(
1427
+ await readFileAsync(loaderPath, 'utf8'),
1428
+ )) {
1429
+ managedRootEntryNames.add(entry)
1430
+ }
1431
+ }
1432
+ }
1433
+ }
1434
+
1435
+ for (const [key, value] of Object.entries(
1436
+ omitBy(
1437
+ // @ts-expect-error missing fields: author and license
1438
+ pick(options, ['name', 'description', 'author', 'license']),
1439
+ isNil,
1440
+ ),
1441
+ )) {
1442
+ defineJsonProperty(packageJsonData, key, value)
1443
+ }
1444
+ if (options.binaryName !== undefined || options.packageName !== undefined) {
1445
+ const napi = asJsonRecord(packageJsonData.napi) ?? createJsonRecord()
1446
+ updateNapiConfigRecord(napi, options)
1447
+ defineJsonProperty(packageJsonData, 'napi', napi)
1448
+ }
1449
+ if (options.repository) {
1450
+ const repository = asJsonRecord(packageJsonData.repository)
1451
+ if (repository) {
1452
+ defineJsonProperty(repository, 'url', options.repository)
1453
+ } else {
1454
+ defineJsonProperty(packageJsonData, 'repository', options.repository)
1455
+ }
1456
+ }
1457
+
1458
+ const plan = new RenameTransactionPlan()
1459
+ plan.addWrite(
74
1460
  packageJsonPath,
75
- JSON.stringify(packageJsonData, null, 2),
1461
+ serializeJsonLike(
1462
+ packageJsonContent,
1463
+ rewritePackageManifest(
1464
+ packageJsonData,
1465
+ managedWasiRenames,
1466
+ managedPackageRenames,
1467
+ ),
1468
+ ),
1469
+ packageJsonMode,
76
1470
  )
77
1471
 
78
- const tomlContent = await readFileAsync(cargoTomlPath, 'utf8')
79
- const cargoToml = parseToml(tomlContent) as any
1472
+ if (configPath) {
1473
+ const configMode = fileMode(configPath, 'NAPI config')
1474
+ const configContent = await readFileAsync(configPath, 'utf8')
1475
+ const configData = asJsonRecord(JSON.parse(configContent))
1476
+ if (!configData) {
1477
+ throw new Error(`NAPI config must contain a JSON object: ${configPath}`)
1478
+ }
1479
+ updateNapiConfigRecord(configData, options)
1480
+ plan.addWrite(
1481
+ configPath,
1482
+ serializeJsonLike(configContent, configData),
1483
+ configMode,
1484
+ )
1485
+ }
80
1486
 
81
- // Update the package name
82
- if (cargoToml.package && options.binaryName) {
83
- // Sanitize the binary name for Rust package naming conventions
84
- const sanitizedName = options.binaryName
85
- .replace('@', '')
86
- .replace('/', '_')
87
- .replace(/-/g, '_')
88
- .toLowerCase()
89
- cargoToml.package.name = sanitizedName
1487
+ if (binaryNameChanged) {
1488
+ const cargoTomlPath = await resolveCanonicalFile(
1489
+ projectRoot,
1490
+ resolve(projectRoot, options.manifestPath),
1491
+ 'Cargo manifest',
1492
+ )
1493
+ const cargoTomlMode = fileMode(cargoTomlPath, 'Cargo manifest')
1494
+ const tomlContent = await readFileAsync(cargoTomlPath, 'utf8')
1495
+ const cargoToml = parseToml(tomlContent) as any
1496
+ if (cargoToml.package) {
1497
+ cargoToml.package.name = sanitizeCargoPackageName(newName)
1498
+ plan.addWrite(cargoTomlPath, stringifyToml(cargoToml), cargoTomlMode)
1499
+ }
1500
+
1501
+ const workflowPath = join(projectRoot, '.github', 'workflows', 'CI.yml')
1502
+ if (pathExists(workflowPath)) {
1503
+ const canonicalWorkflowPath = await resolveCanonicalFile(
1504
+ projectRoot,
1505
+ workflowPath,
1506
+ 'GitHub Actions workflow',
1507
+ )
1508
+ const workflowMode = fileMode(
1509
+ canonicalWorkflowPath,
1510
+ 'GitHub Actions workflow',
1511
+ )
1512
+ const workflowContent = await readFileAsync(canonicalWorkflowPath, 'utf8')
1513
+ const workflowData = yamlParse(workflowContent) as any
1514
+ if (workflowData.env?.APP_NAME) {
1515
+ workflowData.env.APP_NAME = newName
1516
+ plan.addWrite(
1517
+ canonicalWorkflowPath,
1518
+ yamlStringify(workflowData, {
1519
+ lineWidth: -1,
1520
+ noRefs: true,
1521
+ sortKeys: false,
1522
+ }),
1523
+ workflowMode,
1524
+ )
1525
+ }
1526
+ }
90
1527
  }
91
1528
 
92
- // Stringify the updated TOML
93
- const updatedTomlContent = stringifyToml(cargoToml)
1529
+ const targetDirectories: Array<{ directory: string; target: Target }> = []
1530
+ for (const target of napiConfig.targets) {
1531
+ const directory = await resolveCanonicalRoot(
1532
+ projectRoot,
1533
+ resolve(npmRoot, target.platformArchABI),
1534
+ `npm package directory for ${target.triple}`,
1535
+ )
1536
+ targetDirectories.push({ directory, target })
1537
+ }
94
1538
 
95
- await writeFileAsync(cargoTomlPath, updatedTomlContent)
96
- if (options.binaryName && oldName !== options.binaryName) {
97
- const githubActionsPath = find.dir('.github', {
98
- cwd: options.cwd,
99
- })
100
- if (githubActionsPath) {
101
- const githubActionsCIYmlPath = join(
102
- githubActionsPath,
103
- 'workflows',
104
- 'CI.yml',
105
- )
106
- if (existsSync(githubActionsCIYmlPath)) {
107
- const githubActionsContent = await readFileAsync(
108
- githubActionsCIYmlPath,
109
- 'utf8',
1539
+ const managedFileRenames = collectManagedFileRenames(
1540
+ projectRoot,
1541
+ managedWasiRenames,
1542
+ )
1543
+ for (const { target, directory } of targetDirectories) {
1544
+ if (!pathExists(directory)) {
1545
+ continue
1546
+ }
1547
+ if (target.platform === 'wasi') {
1548
+ managedFileRenames.push(
1549
+ ...collectManagedFileRenames(directory, managedWasiRenames),
1550
+ )
1551
+ }
1552
+ if (hasManagedReferenceRenames) {
1553
+ const targetPackageJsonPath = resolveManagedPath(
1554
+ directory,
1555
+ 'package.json',
1556
+ 'Managed package manifest',
1557
+ )
1558
+ if (pathExists(targetPackageJsonPath)) {
1559
+ const manifestMode = fileMode(
1560
+ targetPackageJsonPath,
1561
+ 'Managed package manifest',
110
1562
  )
111
- const githubActionsData = yamlParse(githubActionsContent) as any
112
- if (githubActionsData.env?.APP_NAME) {
113
- githubActionsData.env.APP_NAME = options.binaryName
114
- await writeFileAsync(
115
- githubActionsCIYmlPath,
116
- yamlStringify(githubActionsData, {
117
- lineWidth: -1,
118
- noRefs: true,
119
- sortKeys: false,
120
- }),
1563
+ const content = await readFileAsync(targetPackageJsonPath, 'utf8')
1564
+ const manifest = asJsonRecord(JSON.parse(content))
1565
+ if (!manifest) {
1566
+ throw new Error(
1567
+ `Managed package manifest must contain a JSON object: ${targetPackageJsonPath}`,
121
1568
  )
122
1569
  }
1570
+ const updatedContent = serializeJsonLike(
1571
+ content,
1572
+ rewritePackageManifest(
1573
+ manifest,
1574
+ managedWasiRenames,
1575
+ managedPackageRenames,
1576
+ ),
1577
+ )
1578
+ if (updatedContent !== content) {
1579
+ plan.addWrite(targetPackageJsonPath, updatedContent, manifestMode)
1580
+ }
123
1581
  }
124
1582
  }
125
- const oldWasiBrowserBindingPath = join(
126
- options.cwd,
127
- `${oldName}.wasi-browser.js`,
128
- )
129
- if (existsSync(oldWasiBrowserBindingPath)) {
130
- await rename(
131
- oldWasiBrowserBindingPath,
132
- join(options.cwd, `${options.binaryName}.wasi-browser.js`),
1583
+ }
1584
+
1585
+ const plannedFileRenames = preflightManagedFileRenames(managedFileRenames)
1586
+ await prepareManagedFileRenames(
1587
+ plan,
1588
+ plannedFileRenames,
1589
+ managedWasiRenames,
1590
+ managedPackageRenames,
1591
+ )
1592
+
1593
+ if (hasManagedReferenceRenames) {
1594
+ for (const file of createManagedWasiFiles(newName, napiConfig.targets)) {
1595
+ await prepareManagedTextWrite(
1596
+ plan,
1597
+ resolveManagedPath(projectRoot, file, 'Managed WASI file'),
1598
+ managedWasiRenames,
1599
+ managedPackageRenames,
133
1600
  )
134
1601
  }
135
- const oldWasiBindingPath = join(options.cwd, `${oldName}.wasi.cjs`)
136
- if (existsSync(oldWasiBindingPath)) {
137
- await rename(
138
- oldWasiBindingPath,
139
- join(options.cwd, `${options.binaryName}.wasi.cjs`),
1602
+
1603
+ for (const { target, directory } of targetDirectories) {
1604
+ if (!pathExists(directory)) {
1605
+ continue
1606
+ }
1607
+ if (target.platform === 'wasi') {
1608
+ for (const file of createManagedWasiFiles(newName, [target])) {
1609
+ await prepareManagedTextWrite(
1610
+ plan,
1611
+ resolveManagedPath(directory, file, 'Managed package WASI file'),
1612
+ managedWasiRenames,
1613
+ managedPackageRenames,
1614
+ )
1615
+ }
1616
+ }
1617
+ if (managedPackageRenames.size > 0) {
1618
+ await prepareManagedPackageReadme(
1619
+ plan,
1620
+ directory,
1621
+ oldPackageName,
1622
+ newPackageName,
1623
+ target,
1624
+ )
1625
+ }
1626
+ }
1627
+
1628
+ for (const entry of managedRootEntryNames) {
1629
+ const updatedEntry = replaceManifestReference(
1630
+ entry,
1631
+ managedWasiRenames,
1632
+ new Map(),
1633
+ )
1634
+ const path = await resolveProjectEntry(
1635
+ projectRoot,
1636
+ updatedEntry,
1637
+ 'Managed root entry',
140
1638
  )
1639
+ if (path) {
1640
+ await prepareManagedTextWrite(
1641
+ plan,
1642
+ path,
1643
+ managedWasiRenames,
1644
+ managedPackageRenames,
1645
+ )
1646
+ }
141
1647
  }
142
- const gitAttributesPath = join(options.cwd, '.gitattributes')
143
- if (existsSync(gitAttributesPath)) {
144
- const gitAttributesContent = await readFileAsync(
145
- gitAttributesPath,
146
- 'utf8',
1648
+
1649
+ if (managedWasiRenames.size > 0) {
1650
+ await prepareManagedTextWrite(
1651
+ plan,
1652
+ resolveManagedPath(
1653
+ projectRoot,
1654
+ '.gitattributes',
1655
+ 'Managed .gitattributes file',
1656
+ ),
1657
+ managedWasiRenames,
1658
+ new Map(),
147
1659
  )
148
- const gitAttributesData = gitAttributesContent
149
- .split('\n')
150
- .map((line) => {
151
- return line
152
- .replace(
153
- `${oldName}.wasi-browser.js`,
154
- `${options.binaryName}.wasi-browser.js`,
155
- )
156
- .replace(`${oldName}.wasi.cjs`, `${options.binaryName}.wasi.cjs`)
157
- })
158
- .join('\n')
159
- await writeFileAsync(gitAttributesPath, gitAttributesData)
160
1660
  }
161
1661
  }
1662
+
1663
+ await executeRenameTransaction(projectRoot, plan)
1664
+ }
1665
+
1666
+ export async function renameProject(userOptions: RenameOptions) {
1667
+ const options = applyDefaultRenameOptions(userOptions)
1668
+ const resolvedPaths = resolvePackageReconciliationPaths(
1669
+ options.cwd,
1670
+ options.packageJsonPath,
1671
+ [options.npmDir],
1672
+ )
1673
+ return withPackageFileSystemReconciliation(resolvedPaths, () =>
1674
+ renameProjectUnlocked(options, resolvedPaths),
1675
+ )
162
1676
  }