@napi-rs/cli 3.9.0 → 3.10.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.
- package/README.md +17 -4
- package/dist/cli.js +10359 -8729
- package/dist/index.cjs +10381 -8751
- package/dist/index.d.cts +83 -16
- package/dist/index.d.ts +83 -16
- package/dist/index.js +10359 -8729
- package/docs/wasi.md +318 -4
- package/package.json +5 -6
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +4003 -53
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/build-regressions.spec.ts +210 -3
- package/src/api/__tests__/build.spec.ts +2333 -3
- package/src/api/__tests__/create-npm-dirs.spec.ts +105 -3
- package/src/api/__tests__/pre-publish.spec.ts +153 -1
- package/src/api/__tests__/templates.spec.ts +1309 -1
- package/src/api/build.ts +756 -30
- package/src/api/create-npm-dirs.ts +23 -10
- package/src/api/new.ts +13 -18
- package/src/api/pre-publish.ts +34 -11
- package/src/api/rename.ts +10 -21
- package/src/api/templates/binding-target.ts +176 -0
- package/src/api/templates/index.ts +1 -0
- package/src/api/templates/js-binding.ts +54 -10
- package/src/api/templates/load-wasi-template.ts +661 -58
- package/src/api/templates/wasi-worker-template.ts +36 -31
- package/src/commands/build.ts +1 -1
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +18 -28
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
- package/src/utils/__tests__/misc.spec.ts +4 -0
- package/src/utils/__tests__/reconciliation.spec.ts +676 -0
- package/src/utils/__tests__/serialize.spec.ts +55 -0
- package/src/utils/__tests__/target.spec.ts +221 -0
- package/src/utils/__tests__/typegen.spec.ts +115 -0
- package/src/utils/config.ts +50 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +351 -79
- package/src/utils/serialize.ts +47 -0
- package/src/utils/target.ts +150 -1
- package/src/utils/typegen.ts +608 -42
package/src/utils/typegen.ts
CHANGED
|
@@ -5,9 +5,11 @@ import { sortBy } from 'es-toolkit'
|
|
|
5
5
|
import type {
|
|
6
6
|
CompilerHost,
|
|
7
7
|
CompilerOptions,
|
|
8
|
+
Declaration,
|
|
8
9
|
Diagnostic,
|
|
9
10
|
EntityName,
|
|
10
11
|
Identifier,
|
|
12
|
+
Node,
|
|
11
13
|
NodeArray,
|
|
12
14
|
SourceFile,
|
|
13
15
|
Statement,
|
|
@@ -206,23 +208,28 @@ function prettyPrint(
|
|
|
206
208
|
s += `export interface ${line.name} {\n${line.def}\n}`
|
|
207
209
|
break
|
|
208
210
|
|
|
209
|
-
case TypeDefKind.Type:
|
|
210
|
-
|
|
211
|
+
case TypeDefKind.Type: {
|
|
212
|
+
const def = line.def.trim()
|
|
213
|
+
s +=
|
|
214
|
+
def.startsWith('|') || def.includes('\n')
|
|
215
|
+
? `export type ${line.name} =\n${line.def}`
|
|
216
|
+
: `export type ${line.name} = ${def}`
|
|
211
217
|
break
|
|
218
|
+
}
|
|
212
219
|
|
|
213
220
|
case TypeDefKind.Enum: {
|
|
214
221
|
const enumName = constEnum ? 'const enum' : 'enum'
|
|
215
|
-
s += `${exportDeclare(ambient)} ${enumName} ${line.name}
|
|
222
|
+
s += `${exportDeclare(ambient)} ${enumName} ${line.name} ${enumBody(line.def)}`
|
|
216
223
|
break
|
|
217
224
|
}
|
|
218
225
|
|
|
219
226
|
case TypeDefKind.StringEnum: {
|
|
220
227
|
if (constEnum) {
|
|
221
|
-
s += `${exportDeclare(ambient)} const enum ${line.name}
|
|
228
|
+
s += `${exportDeclare(ambient)} const enum ${line.name} ${enumBody(line.def)}`
|
|
222
229
|
} else if (runtimeStringEnum) {
|
|
223
|
-
s += `${exportDeclare(ambient)} enum ${line.name}
|
|
230
|
+
s += `${exportDeclare(ambient)} enum ${line.name} ${enumBody(line.def)}`
|
|
224
231
|
} else {
|
|
225
|
-
s += `export type ${line.name} = ${line.def
|
|
232
|
+
s += `export type ${line.name} = ${stringEnumToUnion(line.def)}`
|
|
226
233
|
}
|
|
227
234
|
break
|
|
228
235
|
}
|
|
@@ -238,7 +245,7 @@ function prettyPrint(
|
|
|
238
245
|
// Runtime instances inherit from Iterator.prototype when it exists,
|
|
239
246
|
// but the generated constructor does not extend the global Iterator.
|
|
240
247
|
const [T, TResult, TNext] = iteratorTypes
|
|
241
|
-
const resultType =
|
|
248
|
+
const resultType = `${unionPart(TResult)} | undefined`
|
|
242
249
|
classDef +=
|
|
243
250
|
`\n[globalThis.Symbol.iterator](): this` +
|
|
244
251
|
`\nnext(...[value]: [] | [${TNext}]): globalThis.IteratorResult<${T}, ${resultType}>` +
|
|
@@ -256,7 +263,7 @@ function prettyPrint(
|
|
|
256
263
|
const [T, TResult, TNext] = line.asyncIterator
|
|
257
264
|
classDef += `\n[globalThis.Symbol.asyncIterator](): globalThis.${asyncGeneratorHelperName}<${line.name}, ${T}, ${TResult}, ${TNext}>`
|
|
258
265
|
}
|
|
259
|
-
s += `${exportDeclare(ambient)} class ${line.name}${extendsDef}
|
|
266
|
+
s += `${exportDeclare(ambient)} class ${line.name}${extendsDef} ${blockBody(classDef)}`
|
|
260
267
|
s += iteratorInterface
|
|
261
268
|
if (line.original_name && line.original_name !== line.name) {
|
|
262
269
|
s += `\nexport type ${line.original_name} = ${line.name}`
|
|
@@ -268,6 +275,10 @@ function prettyPrint(
|
|
|
268
275
|
s += `${exportDeclare(ambient)} ${line.def}`
|
|
269
276
|
break
|
|
270
277
|
|
|
278
|
+
case TypeDefKind.Const:
|
|
279
|
+
s += ambient ? line.def : declareExportedConst(line.def)
|
|
280
|
+
break
|
|
281
|
+
|
|
271
282
|
default:
|
|
272
283
|
s += line.def
|
|
273
284
|
}
|
|
@@ -283,6 +294,185 @@ function exportDeclare(ambient: boolean): string {
|
|
|
283
294
|
return 'export declare'
|
|
284
295
|
}
|
|
285
296
|
|
|
297
|
+
function blockBody(def: string): string {
|
|
298
|
+
const body = def.trim()
|
|
299
|
+
return body ? `{\n${def}\n}` : '{}'
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function enumBody(def: string): string {
|
|
303
|
+
const body = def.trim()
|
|
304
|
+
if (!body) {
|
|
305
|
+
return '{}'
|
|
306
|
+
}
|
|
307
|
+
const withTrailingComma = body.endsWith(',') ? def : `${def},`
|
|
308
|
+
return `{\n${withTrailingComma}\n}`
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function unionPart(type: string): string {
|
|
312
|
+
return type.includes('|') || type.includes('&') || type.includes('=>')
|
|
313
|
+
? `(${type})`
|
|
314
|
+
: type
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const EXPORT_CONST_PREFIX = 'export const '
|
|
318
|
+
|
|
319
|
+
function declareExportedConst(def: string): string {
|
|
320
|
+
return def.startsWith(EXPORT_CONST_PREFIX)
|
|
321
|
+
? `export declare const ${def.slice(EXPORT_CONST_PREFIX.length)}`
|
|
322
|
+
: def
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isWhitespace(character: string): boolean {
|
|
326
|
+
return (
|
|
327
|
+
character === ' ' ||
|
|
328
|
+
character === '\t' ||
|
|
329
|
+
character === '\n' ||
|
|
330
|
+
character === '\r'
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Walk a rust-emitted string-enum body (`Name = 'value', ...`) and collect
|
|
336
|
+
* the initializer literals. Comments and commas inside quotes are skipped
|
|
337
|
+
* by scanning rather than by splitting on `,`.
|
|
338
|
+
*/
|
|
339
|
+
function stringEnumToUnion(def: string): string {
|
|
340
|
+
const values: string[] = []
|
|
341
|
+
let index = 0
|
|
342
|
+
|
|
343
|
+
const eof = () => index >= def.length
|
|
344
|
+
const peek = (offset = 0) => def[index + offset]
|
|
345
|
+
|
|
346
|
+
const skipTrivia = () => {
|
|
347
|
+
while (!eof()) {
|
|
348
|
+
const character = peek()
|
|
349
|
+
if (character !== undefined && isWhitespace(character)) {
|
|
350
|
+
index += 1
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
if (character === '/' && peek(1) === '/') {
|
|
354
|
+
index += 2
|
|
355
|
+
while (!eof() && peek() !== '\n') {
|
|
356
|
+
index += 1
|
|
357
|
+
}
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
if (character === '/' && peek(1) === '*') {
|
|
361
|
+
index += 2
|
|
362
|
+
while (!eof() && !(peek() === '*' && peek(1) === '/')) {
|
|
363
|
+
index += 1
|
|
364
|
+
}
|
|
365
|
+
if (!eof()) {
|
|
366
|
+
index += 2
|
|
367
|
+
}
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const isNameTerminator = (character: string) =>
|
|
375
|
+
isWhitespace(character) ||
|
|
376
|
+
character === '=' ||
|
|
377
|
+
character === ',' ||
|
|
378
|
+
character === "'" ||
|
|
379
|
+
character === '"' ||
|
|
380
|
+
character === '/'
|
|
381
|
+
|
|
382
|
+
const parseName = () => {
|
|
383
|
+
if (peek() === "'" || peek() === '"') {
|
|
384
|
+
return parseString() !== undefined
|
|
385
|
+
}
|
|
386
|
+
const character = peek()
|
|
387
|
+
if (
|
|
388
|
+
character === undefined ||
|
|
389
|
+
isNameTerminator(character) ||
|
|
390
|
+
(character >= '0' && character <= '9')
|
|
391
|
+
) {
|
|
392
|
+
return false
|
|
393
|
+
}
|
|
394
|
+
index += 1
|
|
395
|
+
while (!eof()) {
|
|
396
|
+
const next = peek()
|
|
397
|
+
if (next === undefined || isNameTerminator(next)) {
|
|
398
|
+
break
|
|
399
|
+
}
|
|
400
|
+
index += 1
|
|
401
|
+
}
|
|
402
|
+
return true
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const parseString = () => {
|
|
406
|
+
const quote = peek()
|
|
407
|
+
if (quote !== "'" && quote !== '"') {
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
let end = index + 1
|
|
411
|
+
while (end < def.length) {
|
|
412
|
+
if (def[end] === '\\') {
|
|
413
|
+
end += 2
|
|
414
|
+
continue
|
|
415
|
+
}
|
|
416
|
+
if (def[end] === quote) {
|
|
417
|
+
const literal = def.slice(index, end + 1)
|
|
418
|
+
index = end + 1
|
|
419
|
+
return literal
|
|
420
|
+
}
|
|
421
|
+
end += 1
|
|
422
|
+
}
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const parseNumber = () => {
|
|
427
|
+
let end = index
|
|
428
|
+
if (def[end] === '-') {
|
|
429
|
+
end += 1
|
|
430
|
+
}
|
|
431
|
+
const firstDigit = def[end]
|
|
432
|
+
if (firstDigit === undefined || firstDigit < '0' || firstDigit > '9') {
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
end += 1
|
|
436
|
+
while (end < def.length) {
|
|
437
|
+
const digit = def[end]
|
|
438
|
+
if (digit === undefined || digit < '0' || digit > '9') {
|
|
439
|
+
break
|
|
440
|
+
}
|
|
441
|
+
end += 1
|
|
442
|
+
}
|
|
443
|
+
const literal = def.slice(index, end)
|
|
444
|
+
index = end
|
|
445
|
+
return literal
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
while (!eof()) {
|
|
449
|
+
skipTrivia()
|
|
450
|
+
if (eof()) {
|
|
451
|
+
break
|
|
452
|
+
}
|
|
453
|
+
if (peek() === ',') {
|
|
454
|
+
index += 1
|
|
455
|
+
continue
|
|
456
|
+
}
|
|
457
|
+
if (!parseName()) {
|
|
458
|
+
index += 1
|
|
459
|
+
continue
|
|
460
|
+
}
|
|
461
|
+
skipTrivia()
|
|
462
|
+
if (peek() !== '=') {
|
|
463
|
+
continue
|
|
464
|
+
}
|
|
465
|
+
index += 1
|
|
466
|
+
skipTrivia()
|
|
467
|
+
const value = parseString() ?? parseNumber()
|
|
468
|
+
if (value !== undefined) {
|
|
469
|
+
values.push(value)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return values.join(' | ')
|
|
474
|
+
}
|
|
475
|
+
|
|
286
476
|
/**
|
|
287
477
|
* Read the napi-derive-emitted intermediate type-def file and render its
|
|
288
478
|
* entries into the `index.d.ts` source string plus the list of names to
|
|
@@ -452,7 +642,7 @@ function renderTypeDefs(
|
|
|
452
642
|
})
|
|
453
643
|
.join('\n\n'),
|
|
454
644
|
)
|
|
455
|
-
.join('\n')
|
|
645
|
+
.join('\n\n')
|
|
456
646
|
const globalDeclarations = []
|
|
457
647
|
if (hasIteratorClass) {
|
|
458
648
|
globalDeclarations.push(ITERATOR_OBJECT_COMPATIBILITY_DECLARATION)
|
|
@@ -948,10 +1138,19 @@ function typeImportReferenceMeaning(
|
|
|
948
1138
|
}
|
|
949
1139
|
}
|
|
950
1140
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1141
|
+
/**
|
|
1142
|
+
* A program over one already-parsed declaration file and nothing else.
|
|
1143
|
+
*
|
|
1144
|
+
* `noLib` and `noResolve` are the point, not an optimization: the answers a
|
|
1145
|
+
* caller takes from this program have to depend on the source alone, never on
|
|
1146
|
+
* what happens to sit next to the output directory. Nothing outside the file
|
|
1147
|
+
* is loaded, so an unresolved import stays unresolved rather than resolving
|
|
1148
|
+
* differently from one machine to the next.
|
|
1149
|
+
*/
|
|
1150
|
+
function programOverDeclarationSource(
|
|
1151
|
+
sourceFile: SourceFile,
|
|
1152
|
+
source: string,
|
|
1153
|
+
): import('typescript').Program {
|
|
955
1154
|
const typeScript = loadTypeScript()
|
|
956
1155
|
const options: CompilerOptions = {
|
|
957
1156
|
module: typeScript.ModuleKind.ESNext,
|
|
@@ -961,13 +1160,6 @@ function createDeclarationProgram(source: string): {
|
|
|
961
1160
|
target: typeScript.ScriptTarget.Latest,
|
|
962
1161
|
types: [],
|
|
963
1162
|
}
|
|
964
|
-
const sourceFile = typeScript.createSourceFile(
|
|
965
|
-
IN_MEMORY_DECLARATION_FILE,
|
|
966
|
-
source,
|
|
967
|
-
options.target!,
|
|
968
|
-
true,
|
|
969
|
-
typeScript.ScriptKind.TS,
|
|
970
|
-
)
|
|
971
1163
|
const host: CompilerHost = {
|
|
972
1164
|
fileExists: (fileName) => fileName === IN_MEMORY_DECLARATION_FILE,
|
|
973
1165
|
getCanonicalFileName: (fileName) => fileName,
|
|
@@ -982,11 +1174,20 @@ function createDeclarationProgram(source: string): {
|
|
|
982
1174
|
useCaseSensitiveFileNames: () => true,
|
|
983
1175
|
writeFile: () => {},
|
|
984
1176
|
}
|
|
985
|
-
|
|
1177
|
+
return typeScript.createProgram({
|
|
986
1178
|
rootNames: [IN_MEMORY_DECLARATION_FILE],
|
|
987
1179
|
options,
|
|
988
1180
|
host,
|
|
989
1181
|
})
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function createDeclarationProgram(source: string): {
|
|
1185
|
+
program: import('typescript').Program
|
|
1186
|
+
sourceFile: SourceFile
|
|
1187
|
+
} {
|
|
1188
|
+
const typeScript = loadTypeScript()
|
|
1189
|
+
const sourceFile = parseDeclarationFile(source)
|
|
1190
|
+
const program = programOverDeclarationSource(sourceFile, source)
|
|
990
1191
|
const diagnostics = program.getSyntacticDiagnostics(sourceFile)
|
|
991
1192
|
if (diagnostics.length > 0) {
|
|
992
1193
|
throwDeclarationDiagnostics(typeScript, sourceFile, diagnostics)
|
|
@@ -994,6 +1195,24 @@ function createDeclarationProgram(source: string): {
|
|
|
994
1195
|
return { program, sourceFile }
|
|
995
1196
|
}
|
|
996
1197
|
|
|
1198
|
+
/**
|
|
1199
|
+
* Parse a declaration source without judging it. Unlike
|
|
1200
|
+
* {@link parseDeclarationSource}, a syntax error somewhere else in the file is
|
|
1201
|
+
* no reason to refuse to answer a question about it: this runs over whatever a
|
|
1202
|
+
* project put in its `--dts-header` and over declaration files kept from
|
|
1203
|
+
* earlier builds.
|
|
1204
|
+
*/
|
|
1205
|
+
function parseDeclarationFile(source: string): SourceFile {
|
|
1206
|
+
const typeScript = loadTypeScript()
|
|
1207
|
+
return typeScript.createSourceFile(
|
|
1208
|
+
IN_MEMORY_DECLARATION_FILE,
|
|
1209
|
+
source,
|
|
1210
|
+
typeScript.ScriptTarget.Latest,
|
|
1211
|
+
true,
|
|
1212
|
+
typeScript.ScriptKind.TS,
|
|
1213
|
+
)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
997
1216
|
function throwDeclarationDiagnostics(
|
|
998
1217
|
typeScript: TypeScriptModule,
|
|
999
1218
|
sourceFile: SourceFile,
|
|
@@ -1215,6 +1434,330 @@ export function rewriteUnboundNodeGlobalTypeQueries(source: string): string {
|
|
|
1215
1434
|
return rewritten
|
|
1216
1435
|
}
|
|
1217
1436
|
|
|
1437
|
+
/**
|
|
1438
|
+
* One top-level `export … const|let|var <name>` found by
|
|
1439
|
+
* {@link scanExportedName}.
|
|
1440
|
+
*/
|
|
1441
|
+
export interface ExportedVariableDeclaration {
|
|
1442
|
+
/**
|
|
1443
|
+
* Where the declaration block starts: the doc comment written directly above
|
|
1444
|
+
* the statement when there is one, so replacing the span swaps the comment
|
|
1445
|
+
* with it instead of stranding it.
|
|
1446
|
+
*/
|
|
1447
|
+
start: number
|
|
1448
|
+
/**
|
|
1449
|
+
* Just past the statement, the statement's own `;` included and trailing
|
|
1450
|
+
* trivia excluded. A caller that replaces `[start, end)` therefore has to
|
|
1451
|
+
* put that terminator back, or whatever followed on the same line runs
|
|
1452
|
+
* straight into the replacement.
|
|
1453
|
+
*/
|
|
1454
|
+
end: number
|
|
1455
|
+
/**
|
|
1456
|
+
* Where this declarator starts: the name, without the `export declare const`
|
|
1457
|
+
* in front of it.
|
|
1458
|
+
*/
|
|
1459
|
+
declaratorStart: number
|
|
1460
|
+
/** Just past this declarator, before any `,` that separates it from a sibling. */
|
|
1461
|
+
declaratorEnd: number
|
|
1462
|
+
/**
|
|
1463
|
+
* How many names the statement declares in all. More than one and
|
|
1464
|
+
* `[start, end)` covers names besides this one, so replacing that span would
|
|
1465
|
+
* delete them.
|
|
1466
|
+
*/
|
|
1467
|
+
declaratorCount: number
|
|
1468
|
+
/** The type annotation as written, or `undefined` when there is none. */
|
|
1469
|
+
type?: string
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/** What {@link scanExportedName} reads out of one declaration source. */
|
|
1473
|
+
export interface ExportedNameScan {
|
|
1474
|
+
/**
|
|
1475
|
+
* Whether the file exports by assignment (`export = x`) at the top level,
|
|
1476
|
+
* which is what a build without `napi-derive`'s `type-def` feature emits.
|
|
1477
|
+
* Such a file cannot carry a named export at all.
|
|
1478
|
+
*
|
|
1479
|
+
* `export default x` parses as the same node and does not count: only
|
|
1480
|
+
* `isExportEquals` does.
|
|
1481
|
+
*/
|
|
1482
|
+
exportsByAssignment: boolean
|
|
1483
|
+
/**
|
|
1484
|
+
* Whether this source already binds `name` in a way that leaves no room for
|
|
1485
|
+
* an added `export declare const name` — as an export, or as a top-level
|
|
1486
|
+
* declaration the const would redeclare.
|
|
1487
|
+
*
|
|
1488
|
+
* Not simply "is it exported": TypeScript merges a value with a declaration
|
|
1489
|
+
* that lives only in type space, and a caller that backed off there would
|
|
1490
|
+
* take away typed access to a real runtime export (TS2693 at every value
|
|
1491
|
+
* use) while preventing no collision at all. So an exported `type`,
|
|
1492
|
+
* `interface` or non-instantiated namespace is *not* an owner.
|
|
1493
|
+
*
|
|
1494
|
+
* Which leaves the question of what "value space" means for every shape a
|
|
1495
|
+
* declaration file can take, and that is TypeScript's question to answer,
|
|
1496
|
+
* not this CLI's — see {@link scanExportedName}. An owner is an export the
|
|
1497
|
+
* checker gives `SymbolFlags.Value` (a variable, `function`, `class`,
|
|
1498
|
+
* `enum`, an instantiated namespace — instantiated by the binder's rules,
|
|
1499
|
+
* which count an aliased member such as `export import x = …` that hand-
|
|
1500
|
+
* written recursion over the body kept missing) or `SymbolFlags.Alias`
|
|
1501
|
+
* (every `export { … }` clause, its `export type { … }` and
|
|
1502
|
+
* `export * as name from '…'` forms, and `export import name = …`; an alias
|
|
1503
|
+
* collides with a local declaration of the name however it is spelled —
|
|
1504
|
+
* TS2323, TS2440).
|
|
1505
|
+
*
|
|
1506
|
+
* The same two flags settle a binding that is not exported at all: a
|
|
1507
|
+
* declaration file that is not a module has no export table, and its
|
|
1508
|
+
* top-level `declare const name` is a global the generated export would
|
|
1509
|
+
* redeclare; inside a module an `import name = …` the file keeps to itself
|
|
1510
|
+
* conflicts too. A top-level `type` or `interface` does not, in either
|
|
1511
|
+
* place — and neither does a name the file puts in the *global* scope
|
|
1512
|
+
* rather than its own, through a `declare global { … }` member or the UMD
|
|
1513
|
+
* name of `export as namespace name`: the added export shadows it instead
|
|
1514
|
+
* of colliding with it.
|
|
1515
|
+
*
|
|
1516
|
+
* `export default` binds `default` rather than a name, and
|
|
1517
|
+
* `export * from '…'` is left unresolved on purpose, so it names nothing
|
|
1518
|
+
* here; neither counts.
|
|
1519
|
+
*/
|
|
1520
|
+
ownsName: boolean
|
|
1521
|
+
/**
|
|
1522
|
+
* Whether the file was bound to answer {@link ownsName}, or whether the
|
|
1523
|
+
* source was settled without it.
|
|
1524
|
+
*
|
|
1525
|
+
* Binding is skipped only for a source that can spell no such name at all —
|
|
1526
|
+
* see {@link scanExportedName}. Nothing in this CLI branches on it: it is
|
|
1527
|
+
* here so a test can hold that shortcut in place without timing anything,
|
|
1528
|
+
* which is the only way to notice it has quietly stopped applying.
|
|
1529
|
+
*/
|
|
1530
|
+
checked: boolean
|
|
1531
|
+
/**
|
|
1532
|
+
* The exported variable statements that declare `name`, in source order.
|
|
1533
|
+
* Only these carry a span a caller can rewrite; the other export forms above
|
|
1534
|
+
* have no declaration here to replace.
|
|
1535
|
+
*/
|
|
1536
|
+
declarations: ExportedVariableDeclaration[]
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* How a declaration source exports `name`, and whether it exports by
|
|
1541
|
+
* assignment instead.
|
|
1542
|
+
*
|
|
1543
|
+
* Parsed rather than pattern-matched, because a declaration file spells both
|
|
1544
|
+
* in places that are not an export of it. `napi-derive` copies a crate's
|
|
1545
|
+
* `js_doc` through verbatim, so a doc comment can name either; a
|
|
1546
|
+
* `--dts-header` may carry a commented-out example of the declaration or of
|
|
1547
|
+
* `export = binding`, or a member of a `declare namespace` / `declare module`
|
|
1548
|
+
* block, which is an export of that block and not of the file. Importing any
|
|
1549
|
+
* of those yields TS2305, and a commented-out `export =` is not an export
|
|
1550
|
+
* assignment at all.
|
|
1551
|
+
*
|
|
1552
|
+
* Ownership is decided by TypeScript's own binder rather than by a list of
|
|
1553
|
+
* shapes this CLI enumerates. Every attempt at the list missed something —
|
|
1554
|
+
* a namespace export clause, an `export import` member, an alias nested one
|
|
1555
|
+
* level deeper — because the question is really "what does this file export,
|
|
1556
|
+
* and in which declaration space", and only the checker knows. So the file is
|
|
1557
|
+
* bound and {@link ExportedNameScan.ownsName} is read off the export symbol's
|
|
1558
|
+
* flags.
|
|
1559
|
+
*
|
|
1560
|
+
* The syntax is still what finds the rewritable declaration and its span, and
|
|
1561
|
+
* one parse serves both: the program is built over the source file parsed
|
|
1562
|
+
* here, so binding costs no second parse.
|
|
1563
|
+
*/
|
|
1564
|
+
export function scanExportedName(
|
|
1565
|
+
source: string,
|
|
1566
|
+
name: string,
|
|
1567
|
+
): ExportedNameScan {
|
|
1568
|
+
const typeScript = loadTypeScript()
|
|
1569
|
+
const sourceFile = parseDeclarationFile(source)
|
|
1570
|
+
const declarations: ExportedVariableDeclaration[] = []
|
|
1571
|
+
let exportsByAssignment = false
|
|
1572
|
+
for (const statement of sourceFile.statements) {
|
|
1573
|
+
if (typeScript.isExportAssignment(statement)) {
|
|
1574
|
+
// `export = x`. `export default x` is the same node without the flag,
|
|
1575
|
+
// and it binds `default`, never `name`.
|
|
1576
|
+
exportsByAssignment ||= statement.isExportEquals === true
|
|
1577
|
+
continue
|
|
1578
|
+
}
|
|
1579
|
+
// Only an exported variable statement leaves a declaration a caller can
|
|
1580
|
+
// rewrite in place. Every other owner is found through the checker below,
|
|
1581
|
+
// which has no span to offer and needs none.
|
|
1582
|
+
if (
|
|
1583
|
+
!typeScript.isVariableStatement(statement) ||
|
|
1584
|
+
!statement.modifiers?.some(
|
|
1585
|
+
(modifier) => modifier.kind === typeScript.SyntaxKind.ExportKeyword,
|
|
1586
|
+
)
|
|
1587
|
+
) {
|
|
1588
|
+
continue
|
|
1589
|
+
}
|
|
1590
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1591
|
+
if (
|
|
1592
|
+
!typeScript.isIdentifier(declaration.name) ||
|
|
1593
|
+
declaration.name.text !== name
|
|
1594
|
+
) {
|
|
1595
|
+
continue
|
|
1596
|
+
}
|
|
1597
|
+
declarations.push({
|
|
1598
|
+
start: declarationBlockStart(source, sourceFile, statement),
|
|
1599
|
+
end: statement.end,
|
|
1600
|
+
declaratorStart: declaration.getStart(sourceFile),
|
|
1601
|
+
declaratorEnd: declaration.end,
|
|
1602
|
+
declaratorCount: statement.declarationList.declarations.length,
|
|
1603
|
+
type: declaration.type?.getText(sourceFile),
|
|
1604
|
+
})
|
|
1605
|
+
break
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const { ownsName, checked } = sourceBindsName(sourceFile, source, name)
|
|
1609
|
+
return { exportsByAssignment, ownsName, checked, declarations }
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* Whether anything in this source already binds `name` where a generated
|
|
1614
|
+
* `export declare const name` would land — as an export of the file, or as a
|
|
1615
|
+
* declaration at its top level that the const would redeclare — and whether
|
|
1616
|
+
* the file had to be bound to find out.
|
|
1617
|
+
*
|
|
1618
|
+
* Both questions are the checker's, and both are answered off the same
|
|
1619
|
+
* program. See {@link ExportedNameScan.ownsName}.
|
|
1620
|
+
*/
|
|
1621
|
+
function sourceBindsName(
|
|
1622
|
+
sourceFile: SourceFile,
|
|
1623
|
+
source: string,
|
|
1624
|
+
name: string,
|
|
1625
|
+
): { ownsName: boolean; checked: boolean } {
|
|
1626
|
+
const typeScript = loadTypeScript()
|
|
1627
|
+
// A binding of `name` in this file has to spell it here — but it need not
|
|
1628
|
+
// spell it in the characters the name is made of. An identifier may write
|
|
1629
|
+
// any of them as a `\uXXXX` or `\u{…}` escape; a string-literal export name
|
|
1630
|
+
// (`export { x as "…" }`) may use `\xXX` as well, and may be broken across
|
|
1631
|
+
// lines with a backslash-newline continuation. TypeScript resolves every one
|
|
1632
|
+
// of those to the same name where a text search sees nothing. Enumerating
|
|
1633
|
+
// the escapes is the game that was already lost once, so the shortcut asks
|
|
1634
|
+
// for less: a source carrying neither the name nor a backslash *anywhere*
|
|
1635
|
+
// cannot spell it, and that is what almost every header is. Everything else
|
|
1636
|
+
// goes to the checker, which has always been able to say. The other form a
|
|
1637
|
+
// text search would miss is `export * from '…'`, which the program below
|
|
1638
|
+
// leaves unresolved by design, so nothing is skipped there either.
|
|
1639
|
+
if (!source.includes(name) && !source.includes('\\')) {
|
|
1640
|
+
return { ownsName: false, checked: false }
|
|
1641
|
+
}
|
|
1642
|
+
// A value or an alias is what a `const` of the same name cannot be written
|
|
1643
|
+
// beside; a type-only declaration is what it merges with.
|
|
1644
|
+
const meaning = typeScript.SymbolFlags.Alias | typeScript.SymbolFlags.Value
|
|
1645
|
+
const checker = programOverDeclarationSource(
|
|
1646
|
+
sourceFile,
|
|
1647
|
+
source,
|
|
1648
|
+
).getTypeChecker()
|
|
1649
|
+
const moduleSymbol = checker.getSymbolAtLocation(sourceFile)
|
|
1650
|
+
const exported =
|
|
1651
|
+
moduleSymbol === undefined
|
|
1652
|
+
? undefined
|
|
1653
|
+
: checker
|
|
1654
|
+
.getExportsOfModule(moduleSymbol)
|
|
1655
|
+
.find((symbol) => symbol.name === name)
|
|
1656
|
+
if (exported !== undefined && (exported.flags & meaning) !== 0) {
|
|
1657
|
+
return { ownsName: true, checked: true }
|
|
1658
|
+
}
|
|
1659
|
+
// Not every binding that collides is an export. A declaration file that is
|
|
1660
|
+
// not a module has no export table at all, and its top-level `declare const`
|
|
1661
|
+
// is a global the generated export would redeclare (TS2451, TS2395). Inside
|
|
1662
|
+
// a module, an `import name = …` kept to the file is likewise no export and
|
|
1663
|
+
// still conflicts (TS2440). Ambient declaration files put most top-level
|
|
1664
|
+
// declarations in the export table by themselves, so these are the leftovers
|
|
1665
|
+
// rather than the common case — but they are the ones a rule written around
|
|
1666
|
+
// exports alone would miss.
|
|
1667
|
+
//
|
|
1668
|
+
// Scoped at the source file, so a binding nested inside a namespace or a
|
|
1669
|
+
// function body is correctly none of this file's business, and filtered to
|
|
1670
|
+
// the declarations that bind in this file's own scope — see
|
|
1671
|
+
// {@link declaresInFileScope}.
|
|
1672
|
+
return {
|
|
1673
|
+
ownsName: checker
|
|
1674
|
+
.getSymbolsInScope(sourceFile, meaning)
|
|
1675
|
+
.some(
|
|
1676
|
+
(symbol) =>
|
|
1677
|
+
symbol.name === name &&
|
|
1678
|
+
symbol.declarations?.some((declaration) =>
|
|
1679
|
+
declaresInFileScope(typeScript, declaration, sourceFile),
|
|
1680
|
+
) === true,
|
|
1681
|
+
),
|
|
1682
|
+
checked: true,
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* Whether `declaration` binds its name in `sourceFile`'s own scope — the scope
|
|
1688
|
+
* a generated `export declare const` would land in.
|
|
1689
|
+
*
|
|
1690
|
+
* `getSymbolsInScope` answers what is *visible* at a location, and the global
|
|
1691
|
+
* scope is visible everywhere, so a name a file declares into that scope comes
|
|
1692
|
+
* back from it while colliding with nothing the file itself adds. Written here
|
|
1693
|
+
* is therefore not enough; written here *and at this file's top level* is the
|
|
1694
|
+
* question:
|
|
1695
|
+
*
|
|
1696
|
+
* - A `declare global { … }` member is a global, reached through a
|
|
1697
|
+
* `ModuleDeclaration`. A module-scoped `export declare const` of the same
|
|
1698
|
+
* name shadows it rather than redeclaring it. The members of an ambient
|
|
1699
|
+
* `declare module '…'` block and of a `declare namespace` belong to that
|
|
1700
|
+
* module or namespace instead of to the file; the checker already keeps
|
|
1701
|
+
* those out of scope here, and the same walk covers them without depending
|
|
1702
|
+
* on that.
|
|
1703
|
+
* - `export as namespace name` declares a UMD global for script consumers,
|
|
1704
|
+
* not a binding in the file. Its `NamespaceExportDeclaration` is a child of
|
|
1705
|
+
* the source file and so passes the walk, and it too sits happily beside an
|
|
1706
|
+
* export of the same name.
|
|
1707
|
+
*
|
|
1708
|
+
* Everything else that reaches here does bind at the top level: a script
|
|
1709
|
+
* file's own `declare const` (appending the export makes the file a module and
|
|
1710
|
+
* puts both in the same scope), and a module's unexported `import name = …`.
|
|
1711
|
+
*/
|
|
1712
|
+
function declaresInFileScope(
|
|
1713
|
+
typeScript: TypeScriptModule,
|
|
1714
|
+
declaration: Declaration,
|
|
1715
|
+
sourceFile: SourceFile,
|
|
1716
|
+
): boolean {
|
|
1717
|
+
if (
|
|
1718
|
+
declaration.getSourceFile() !== sourceFile ||
|
|
1719
|
+
typeScript.isNamespaceExportDeclaration(declaration)
|
|
1720
|
+
) {
|
|
1721
|
+
return false
|
|
1722
|
+
}
|
|
1723
|
+
for (
|
|
1724
|
+
let node: Node | undefined = declaration.parent;
|
|
1725
|
+
node !== undefined && !typeScript.isSourceFile(node);
|
|
1726
|
+
node = node.parent
|
|
1727
|
+
) {
|
|
1728
|
+
if (typeScript.isModuleDeclaration(node)) {
|
|
1729
|
+
return false
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return true
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/**
|
|
1736
|
+
* Where a statement's block starts for replacement purposes: the doc comment
|
|
1737
|
+
* directly above it when one is there, otherwise the statement itself. Only a
|
|
1738
|
+
* `/** … *\/` comment separated from the statement by nothing but whitespace
|
|
1739
|
+
* counts, so a license banner further up is never swallowed.
|
|
1740
|
+
*/
|
|
1741
|
+
function declarationBlockStart(
|
|
1742
|
+
source: string,
|
|
1743
|
+
sourceFile: SourceFile,
|
|
1744
|
+
statement: Statement,
|
|
1745
|
+
): number {
|
|
1746
|
+
const typeScript = loadTypeScript()
|
|
1747
|
+
const start = statement.getStart(sourceFile)
|
|
1748
|
+
const comments =
|
|
1749
|
+
typeScript.getLeadingCommentRanges(source, statement.getFullStart()) ?? []
|
|
1750
|
+
for (const comment of comments) {
|
|
1751
|
+
if (
|
|
1752
|
+
source.startsWith('/**', comment.pos) &&
|
|
1753
|
+
source.slice(comment.end, start).trim() === ''
|
|
1754
|
+
) {
|
|
1755
|
+
return comment.pos
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
return start
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1218
1761
|
function parseDeclarationSource(source: string) {
|
|
1219
1762
|
const typeScript = loadTypeScript()
|
|
1220
1763
|
const parsed = typeScript.createSourceFile(
|
|
@@ -1424,43 +1967,66 @@ export function correctStringIdent(src: string, ident: number): string {
|
|
|
1424
1967
|
let bracketDepth = 0
|
|
1425
1968
|
const result = src
|
|
1426
1969
|
.split('\n')
|
|
1427
|
-
.map((
|
|
1428
|
-
line =
|
|
1970
|
+
.map((rawLine) => {
|
|
1971
|
+
const line = rawLine.trim()
|
|
1429
1972
|
if (line === '') {
|
|
1430
1973
|
return ''
|
|
1431
1974
|
}
|
|
1432
1975
|
|
|
1433
1976
|
const isInMultilineComment = line.startsWith('*')
|
|
1434
|
-
const isClosingBracket = line.endsWith('}')
|
|
1435
|
-
const isOpeningBracket = line.endsWith('{')
|
|
1436
1977
|
const isTypeDeclaration = line.endsWith('=')
|
|
1437
|
-
const
|
|
1978
|
+
const netBraces = isInMultilineComment ? 0 : netUnquotedBraces(line)
|
|
1438
1979
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
bracketDepth += 1
|
|
1442
|
-
rightIndent += (bracketDepth - 1) * 2
|
|
1443
|
-
} else {
|
|
1444
|
-
if (
|
|
1445
|
-
isClosingBracket &&
|
|
1446
|
-
bracketDepth > 0 &&
|
|
1447
|
-
!isInMultilineComment &&
|
|
1448
|
-
!isTypeVariant
|
|
1449
|
-
) {
|
|
1450
|
-
bracketDepth -= 1
|
|
1451
|
-
}
|
|
1452
|
-
rightIndent += bracketDepth * 2
|
|
1980
|
+
if (netBraces < 0 && !line.startsWith('|')) {
|
|
1981
|
+
bracketDepth = Math.max(0, bracketDepth + netBraces)
|
|
1453
1982
|
}
|
|
1454
1983
|
|
|
1984
|
+
let rightIndent = ident + bracketDepth * 2
|
|
1455
1985
|
if (isInMultilineComment) {
|
|
1456
1986
|
rightIndent += 1
|
|
1457
1987
|
}
|
|
1458
1988
|
|
|
1459
|
-
|
|
1989
|
+
if (netBraces > 0) {
|
|
1990
|
+
bracketDepth += netBraces
|
|
1991
|
+
} else if (
|
|
1992
|
+
isTypeDeclaration &&
|
|
1993
|
+
!isInMultilineComment &&
|
|
1994
|
+
netBraces === 0
|
|
1995
|
+
) {
|
|
1996
|
+
bracketDepth += 1
|
|
1997
|
+
}
|
|
1460
1998
|
|
|
1461
|
-
return
|
|
1999
|
+
return `${' '.repeat(rightIndent)}${line}`
|
|
1462
2000
|
})
|
|
1463
2001
|
.join('\n')
|
|
1464
2002
|
|
|
1465
2003
|
return result
|
|
1466
2004
|
}
|
|
2005
|
+
|
|
2006
|
+
function netUnquotedBraces(line: string): number {
|
|
2007
|
+
let net = 0
|
|
2008
|
+
let quote: "'" | '"' | '`' | undefined
|
|
2009
|
+
let escaped = false
|
|
2010
|
+
for (const character of line) {
|
|
2011
|
+
if (quote) {
|
|
2012
|
+
if (escaped) {
|
|
2013
|
+
escaped = false
|
|
2014
|
+
} else if (character === '\\') {
|
|
2015
|
+
escaped = true
|
|
2016
|
+
} else if (character === quote) {
|
|
2017
|
+
quote = undefined
|
|
2018
|
+
}
|
|
2019
|
+
continue
|
|
2020
|
+
}
|
|
2021
|
+
if (character === "'" || character === '"' || character === '`') {
|
|
2022
|
+
quote = character
|
|
2023
|
+
continue
|
|
2024
|
+
}
|
|
2025
|
+
if (character === '{') {
|
|
2026
|
+
net += 1
|
|
2027
|
+
} else if (character === '}') {
|
|
2028
|
+
net -= 1
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return net
|
|
2032
|
+
}
|