@napi-rs/cli 3.10.3 → 3.10.5
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/dist/cli.js +1310 -55
- package/dist/index.cjs +1313 -58
- package/dist/index.d.cts +19 -7
- package/dist/index.d.ts +19 -7
- package/dist/index.js +1310 -55
- package/docs/build.md +1 -0
- package/docs/wasi.md +149 -8
- package/package.json +2 -2
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +2498 -132
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/build.spec.ts +257 -0
- package/src/api/__tests__/templates.spec.ts +266 -6
- package/src/api/build.ts +57 -6
- package/src/api/templates/load-wasi-template.ts +807 -24
- package/src/commands/__tests__/build.spec.ts +6 -0
- package/src/def/build.ts +13 -0
- package/src/utils/__tests__/ohos-selfsign.spec.ts +285 -0
- package/src/utils/__tests__/reconciliation.spec.ts +19 -0
- package/src/utils/config.ts +10 -6
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +5 -4
- package/src/utils/ohos-selfsign.ts +646 -0
- package/src/utils/typegen.ts +221 -2
|
Binary file
|
|
@@ -831,6 +831,263 @@ test('a fresh WASI declaration narrows the inherited root union', async (t) => {
|
|
|
831
831
|
const ROOT_BINDING_TARGET_DECLARATION =
|
|
832
832
|
"export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'"
|
|
833
833
|
|
|
834
|
+
test('an ESM root declaration derives the WASI declaration unless it cannot cross', async (t) => {
|
|
835
|
+
const { tmpDir, projectDir, typeDefDir } = t.context
|
|
836
|
+
await writeFile(
|
|
837
|
+
join(typeDefDir, 'sum.type'),
|
|
838
|
+
'{"kind":"fn","name":"sum","def":"function sum(a: number, b: number): number"}\n',
|
|
839
|
+
)
|
|
840
|
+
const { dts } = await generateTypeDef({
|
|
841
|
+
typeDefDir,
|
|
842
|
+
cwd: projectDir,
|
|
843
|
+
declareBindingTarget: true,
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
// A `"type": "module"` package's `index.d.ts` is an ESM declaration, and so
|
|
847
|
+
// is an explicit `.d.mts` — but the generated typedef carries nothing a
|
|
848
|
+
// `.d.cts` cannot re-declare, so the WASI declaration derives verbatim.
|
|
849
|
+
// (napi-rs#3531: an unconditional refusal failed every WASI build in a
|
|
850
|
+
// module package, including `--esm` ones.)
|
|
851
|
+
const sourcePath = join(projectDir, 'index.d.ts')
|
|
852
|
+
const destinationPath = join(projectDir, 'pkg.wasi.d.cts')
|
|
853
|
+
for (const esmSourcePath of [sourcePath, join(projectDir, 'index.d.mts')]) {
|
|
854
|
+
t.is(
|
|
855
|
+
prepareWasiBindingTypeDef(
|
|
856
|
+
dts,
|
|
857
|
+
esmSourcePath,
|
|
858
|
+
destinationPath,
|
|
859
|
+
true,
|
|
860
|
+
'module',
|
|
861
|
+
),
|
|
862
|
+
dts,
|
|
863
|
+
)
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// The threadless flavor still gets its node-global rewrites on the way, and
|
|
867
|
+
// what it emits is valid TypeScript beside the ESM source it came from.
|
|
868
|
+
const threadless = prepareWasiBindingTypeDef(
|
|
869
|
+
dts,
|
|
870
|
+
sourcePath,
|
|
871
|
+
destinationPath,
|
|
872
|
+
false,
|
|
873
|
+
'module',
|
|
874
|
+
)
|
|
875
|
+
const codes = await semanticDiagnosticCodes(join(tmpDir, 'esm-derived'), {
|
|
876
|
+
'index.d.ts': dts,
|
|
877
|
+
'pkg.wasi.d.cts': threadless,
|
|
878
|
+
})
|
|
879
|
+
t.deepEqual(codes['pkg.wasi.d.cts'], [])
|
|
880
|
+
|
|
881
|
+
// Only the constructs a CommonJS declaration cannot carry still refuse —
|
|
882
|
+
// each one named in the error, from `.d.ts` and `.d.mts` sources alike.
|
|
883
|
+
const hazards: Array<[what: string, source: string, barrier: string]> = [
|
|
884
|
+
[
|
|
885
|
+
'an `export default` declaration',
|
|
886
|
+
'export declare function sum(a: number): number\nexport default sum\n',
|
|
887
|
+
'an `export default` declaration',
|
|
888
|
+
],
|
|
889
|
+
[
|
|
890
|
+
'a default class declaration',
|
|
891
|
+
'export default class Sum {}\n',
|
|
892
|
+
'an `export default` declaration',
|
|
893
|
+
],
|
|
894
|
+
[
|
|
895
|
+
'a default function declaration',
|
|
896
|
+
'export default function sum(a: number): number\n',
|
|
897
|
+
'an `export default` declaration',
|
|
898
|
+
],
|
|
899
|
+
[
|
|
900
|
+
'a default interface declaration',
|
|
901
|
+
'export default interface Sum { a: number }\n',
|
|
902
|
+
'an `export default` declaration',
|
|
903
|
+
],
|
|
904
|
+
[
|
|
905
|
+
'a default namespace declaration',
|
|
906
|
+
'export default namespace Sum { const a: number }\n',
|
|
907
|
+
'an `export default` declaration',
|
|
908
|
+
],
|
|
909
|
+
[
|
|
910
|
+
'a `default` re-export',
|
|
911
|
+
'declare const sum: number\nexport { sum as default }\n',
|
|
912
|
+
'a `default` re-export',
|
|
913
|
+
],
|
|
914
|
+
[
|
|
915
|
+
'a `default` re-export from a package',
|
|
916
|
+
"export { sum as default } from 'sums'\n",
|
|
917
|
+
'a `default` re-export',
|
|
918
|
+
],
|
|
919
|
+
[
|
|
920
|
+
'a type-only `default` re-export',
|
|
921
|
+
'type Sum = number\nexport type { Sum as default }\n',
|
|
922
|
+
'a `default` re-export',
|
|
923
|
+
],
|
|
924
|
+
[
|
|
925
|
+
'a `default` namespace re-export',
|
|
926
|
+
"export * as default from 'sums'\n",
|
|
927
|
+
'a `default` re-export',
|
|
928
|
+
],
|
|
929
|
+
[
|
|
930
|
+
'a relative import specifier',
|
|
931
|
+
"import { sum } from './sum.js'\nexport declare function f(): typeof sum\n",
|
|
932
|
+
"a relative './sum.js' specifier",
|
|
933
|
+
],
|
|
934
|
+
[
|
|
935
|
+
'an extensionless relative specifier',
|
|
936
|
+
"import { sum } from './sum'\nexport declare function f(): typeof sum\n",
|
|
937
|
+
"a relative './sum' specifier",
|
|
938
|
+
],
|
|
939
|
+
[
|
|
940
|
+
'a relative export specifier',
|
|
941
|
+
"export { sum } from './sum.js'\n",
|
|
942
|
+
"a relative './sum.js' specifier",
|
|
943
|
+
],
|
|
944
|
+
[
|
|
945
|
+
'a relative export-star specifier',
|
|
946
|
+
"export * from './sums.js'\n",
|
|
947
|
+
"a relative './sums.js' specifier",
|
|
948
|
+
],
|
|
949
|
+
[
|
|
950
|
+
'a relative import type specifier',
|
|
951
|
+
"import type { Sum } from './sum.js'\nexport declare function f(s: Sum): void\n",
|
|
952
|
+
"a relative './sum.js' specifier",
|
|
953
|
+
],
|
|
954
|
+
[
|
|
955
|
+
'a relative import() type',
|
|
956
|
+
"export declare function f(): import('./sum.js').Sum\n",
|
|
957
|
+
"a relative './sum.js' specifier",
|
|
958
|
+
],
|
|
959
|
+
[
|
|
960
|
+
'a relative import-equals specifier',
|
|
961
|
+
"import sum = require('./sum.js')\nexport declare function f(): typeof sum\n",
|
|
962
|
+
"a relative './sum.js' specifier",
|
|
963
|
+
],
|
|
964
|
+
[
|
|
965
|
+
'a relative ambient module specifier',
|
|
966
|
+
"declare module './sum.js' { export const sum: number }\n",
|
|
967
|
+
"a relative './sum.js' specifier",
|
|
968
|
+
],
|
|
969
|
+
[
|
|
970
|
+
'a relative types reference',
|
|
971
|
+
"/// <reference types='./sum' />\nexport declare const sum: number\n",
|
|
972
|
+
'a relative \'/// <reference types="./sum" />\' directive',
|
|
973
|
+
],
|
|
974
|
+
[
|
|
975
|
+
'attributes on a runtime statement',
|
|
976
|
+
"import sum from './sum.json' with { type: 'json' }\nexport declare const s: typeof sum\n",
|
|
977
|
+
'`with` attributes on a runtime module statement',
|
|
978
|
+
],
|
|
979
|
+
]
|
|
980
|
+
const esmSources: Array<
|
|
981
|
+
[path: string, packageType: 'module' | 'commonjs' | undefined]
|
|
982
|
+
> = [
|
|
983
|
+
[sourcePath, 'module'],
|
|
984
|
+
[join(projectDir, 'index.d.mts'), undefined],
|
|
985
|
+
]
|
|
986
|
+
for (const [what, source, barrier] of hazards) {
|
|
987
|
+
for (const [esmSourcePath, packageType] of esmSources) {
|
|
988
|
+
const error = t.throws(
|
|
989
|
+
() =>
|
|
990
|
+
prepareWasiBindingTypeDef(
|
|
991
|
+
source,
|
|
992
|
+
esmSourcePath,
|
|
993
|
+
destinationPath,
|
|
994
|
+
true,
|
|
995
|
+
packageType,
|
|
996
|
+
),
|
|
997
|
+
{ instanceOf: Error },
|
|
998
|
+
)
|
|
999
|
+
t.true(
|
|
1000
|
+
error?.message.includes(barrier),
|
|
1001
|
+
`${what} names the offending construct, got: ${error?.message}`,
|
|
1002
|
+
)
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Everything else a module package can write crosses honestly.
|
|
1007
|
+
const honest: Record<string, string> = {
|
|
1008
|
+
'bare and node: imports':
|
|
1009
|
+
"import { Buffer } from 'buffer'\nimport type { ReadableStream } from 'node:stream/web'\nexport declare function f(b: Buffer, s: ReadableStream): void\n",
|
|
1010
|
+
'.mjs specifiers':
|
|
1011
|
+
"import { sum } from './sum.mjs'\nexport declare function f(): typeof sum\n",
|
|
1012
|
+
'.cjs specifiers':
|
|
1013
|
+
"import { sum } from './sum.cjs'\nexport declare function f(): typeof sum\n",
|
|
1014
|
+
'an export assignment':
|
|
1015
|
+
'declare const binding: { sum(a: number): number }\nexport = binding\n',
|
|
1016
|
+
'a declare global block':
|
|
1017
|
+
'declare global { namespace Legacy { const v: number } }\nexport {}\n',
|
|
1018
|
+
'a bare ambient module':
|
|
1019
|
+
"declare module 'legacy' { export const sum: number }\n",
|
|
1020
|
+
'a .mjs ambient module':
|
|
1021
|
+
"declare module './sum.mjs' { export const sum: number }\n",
|
|
1022
|
+
'a default inside an ambient module':
|
|
1023
|
+
"declare module 'legacy' { const sum: number\nexport default sum }\n",
|
|
1024
|
+
'a type-only import with resolution-mode':
|
|
1025
|
+
"import type { Sum } from './sum.js' with { 'resolution-mode': 'import' }\nexport declare function f(s: Sum): void\n",
|
|
1026
|
+
'a type-only import pinned to require mode':
|
|
1027
|
+
"import type { Sum } from './sum.js' with { 'resolution-mode': 'require' }\nexport declare function f(s: Sum): void\n",
|
|
1028
|
+
'a type-only export with resolution-mode':
|
|
1029
|
+
"export type { Sum } from './sum.js' with { 'resolution-mode': 'import' }\n",
|
|
1030
|
+
'a json specifier':
|
|
1031
|
+
"import sum from './sum.json'\nexport declare const s: typeof sum\n",
|
|
1032
|
+
'a path reference':
|
|
1033
|
+
"/// <reference path='./sum.d.ts' />\nexport declare const sum: number\n",
|
|
1034
|
+
'a bare import() type':
|
|
1035
|
+
"export declare function f(): import('buffer').Buffer\n",
|
|
1036
|
+
'an import() type pinned to import mode':
|
|
1037
|
+
"export declare function f(): import('./sum.js', { with: { 'resolution-mode': 'import' } }).Sum\n",
|
|
1038
|
+
'a namespace export':
|
|
1039
|
+
'export declare namespace Legacy { const sum: number }\n',
|
|
1040
|
+
}
|
|
1041
|
+
for (const [what, source] of Object.entries(honest)) {
|
|
1042
|
+
t.notThrows(
|
|
1043
|
+
() =>
|
|
1044
|
+
prepareWasiBindingTypeDef(
|
|
1045
|
+
source,
|
|
1046
|
+
sourcePath,
|
|
1047
|
+
destinationPath,
|
|
1048
|
+
true,
|
|
1049
|
+
'module',
|
|
1050
|
+
),
|
|
1051
|
+
what,
|
|
1052
|
+
)
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Nothing is scanned when the source is already CommonJS — a `.d.cts` (or a
|
|
1056
|
+
// `.d.ts` in a `commonjs` package) reads the same beside its destination, so
|
|
1057
|
+
// every hazard above carries over verbatim.
|
|
1058
|
+
for (const [, source] of hazards) {
|
|
1059
|
+
for (const [cjsSourcePath, packageType] of [
|
|
1060
|
+
[join(projectDir, 'index.d.cts'), 'module'],
|
|
1061
|
+
[sourcePath, 'commonjs'],
|
|
1062
|
+
[sourcePath, undefined],
|
|
1063
|
+
] as const) {
|
|
1064
|
+
t.notThrows(() =>
|
|
1065
|
+
prepareWasiBindingTypeDef(
|
|
1066
|
+
source,
|
|
1067
|
+
cjsSourcePath,
|
|
1068
|
+
destinationPath,
|
|
1069
|
+
true,
|
|
1070
|
+
packageType,
|
|
1071
|
+
),
|
|
1072
|
+
)
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// A specifier that can cross still rebases onto the destination directory
|
|
1077
|
+
// — including a `declare module` augmentation name, which targets the file
|
|
1078
|
+
// it spells out.
|
|
1079
|
+
const relocated = prepareWasiBindingTypeDef(
|
|
1080
|
+
"/// <reference path='./sum.d.ts' />\ndeclare module './sum.mjs' { export const sum: number }\nexport declare function f(): import('./sum.mjs').Sum\nexport { sum } from './sum.mjs'\n",
|
|
1081
|
+
join(projectDir, 'types', 'index.d.mts'),
|
|
1082
|
+
destinationPath,
|
|
1083
|
+
true,
|
|
1084
|
+
)
|
|
1085
|
+
t.true(relocated.includes("declare module './types/sum.mjs'"))
|
|
1086
|
+
t.true(relocated.includes("import('./types/sum.mjs')"))
|
|
1087
|
+
t.true(relocated.includes("from './types/sum.mjs'"))
|
|
1088
|
+
t.true(relocated.includes("reference path='./types/sum.d.ts'"))
|
|
1089
|
+
})
|
|
1090
|
+
|
|
834
1091
|
test('a crate without type defs still declares the binding target', async (t) => {
|
|
835
1092
|
const { projectDir, typeDefDir } = t.context
|
|
836
1093
|
|
|
@@ -1123,6 +1123,241 @@ for (const { name, code } of eagerWasiLoaderCases) {
|
|
|
1123
1123
|
})
|
|
1124
1124
|
}
|
|
1125
1125
|
|
|
1126
|
+
// `napi_prepare_wasm_env_cleanup` waits: it returns only once the addon's async
|
|
1127
|
+
// runtime has quiesced, and on a threaded artifact the work it waits for can be
|
|
1128
|
+
// waiting for a JavaScript turn from the very thread the export runs on — which
|
|
1129
|
+
// never comes, because that thread is inside the export. The addon exposes the
|
|
1130
|
+
// same teardown as `…_begin` / `napi_wasm_runtime_work_pending` / `…_finish` so
|
|
1131
|
+
// a loader can put real turns in the middle. Every shape that can yield has to
|
|
1132
|
+
// use it, and every shape has to keep working against an addon that has no such
|
|
1133
|
+
// exports.
|
|
1134
|
+
const TWO_PHASE_BARRIER_BY_FLAVOR = {
|
|
1135
|
+
eager: {
|
|
1136
|
+
detect:
|
|
1137
|
+
/if \(typeof begin !== 'function' \|\|\s*typeof finish !== 'function'\) \{/,
|
|
1138
|
+
poll: '}, __WASM_RUNTIME_WORK_POLL_INTERVAL_MS)',
|
|
1139
|
+
finish: '.then(finishCleanup, finishCleanup)',
|
|
1140
|
+
fallback: '__prepareWasmEnvCleanup()',
|
|
1141
|
+
report: '__reportUnreachedWasmEnvSettlements()',
|
|
1142
|
+
// The closer a caller that cannot yield uses to end a parked handshake,
|
|
1143
|
+
// and the point at which the poll publishes it.
|
|
1144
|
+
parked: '__finishParkedWasmEnvCleanup',
|
|
1145
|
+
publishParked: '__finishParkedWasmEnvCleanup = finishCleanup',
|
|
1146
|
+
clearParked: '__finishParkedWasmEnvCleanup = undefined',
|
|
1147
|
+
},
|
|
1148
|
+
deferred: {
|
|
1149
|
+
detect:
|
|
1150
|
+
/if \(typeof __begin !== 'function' \|\|\s*typeof __finish !== 'function'\) \{/,
|
|
1151
|
+
poll: '}, __WASM_RUNTIME_WORK_POLL_INTERVAL_MS)',
|
|
1152
|
+
finish: `return __pollWasmRuntimeWork(__workPending).then(
|
|
1153
|
+
__finishEnvCleanup,
|
|
1154
|
+
__finishEnvCleanup,
|
|
1155
|
+
)`,
|
|
1156
|
+
fallback: '__prepareEnvCleanup()',
|
|
1157
|
+
report: '__reportUnreachedSettlements()',
|
|
1158
|
+
parked: '__finishParkedEnvCleanup',
|
|
1159
|
+
publishParked: '__finishParkedEnvCleanup = __finishEnvCleanup',
|
|
1160
|
+
clearParked: '__finishParkedEnvCleanup = undefined',
|
|
1161
|
+
},
|
|
1162
|
+
} as const
|
|
1163
|
+
|
|
1164
|
+
for (const { name, code } of wasiLoaderCases) {
|
|
1165
|
+
const barrier =
|
|
1166
|
+
TWO_PHASE_BARRIER_BY_FLAVOR[
|
|
1167
|
+
code.includes(EAGER_ROLLBACK_SIGNATURE) ? 'eager' : 'deferred'
|
|
1168
|
+
]
|
|
1169
|
+
test(`WASI loader polls the two-phase wasm env cleanup: ${name}`, (t) => {
|
|
1170
|
+
t.true(
|
|
1171
|
+
code.includes('napi_prepare_wasm_env_cleanup_begin'),
|
|
1172
|
+
'loader must start the teardown without joining',
|
|
1173
|
+
)
|
|
1174
|
+
t.true(
|
|
1175
|
+
code.includes('napi_wasm_runtime_work_pending'),
|
|
1176
|
+
'loader must poll the runtime between the two halves; without it there is nothing to wait on',
|
|
1177
|
+
)
|
|
1178
|
+
t.true(
|
|
1179
|
+
code.includes('napi_prepare_wasm_env_cleanup_finish'),
|
|
1180
|
+
'loader must still join: finish is the call that owes quiescence',
|
|
1181
|
+
)
|
|
1182
|
+
// Optional, exactly like every other export in this teardown: an addon
|
|
1183
|
+
// built against a napi crate that predates the split keeps the single
|
|
1184
|
+
// blocking call.
|
|
1185
|
+
t.regex(code, barrier.detect, 'the split must be feature-detected')
|
|
1186
|
+
t.true(
|
|
1187
|
+
code.includes(barrier.fallback),
|
|
1188
|
+
'a loader that finds no split must fall back to the single call',
|
|
1189
|
+
)
|
|
1190
|
+
// Unbounded, and deliberately so. The only way to end the poll early is to
|
|
1191
|
+
// call `…_finish`, which joins on the JavaScript thread — the very thread
|
|
1192
|
+
// the work it joins may be waiting for a turn from — so a budget would not
|
|
1193
|
+
// end that wait, it would only move it somewhere the thread can no longer
|
|
1194
|
+
// be reached. Same reason the async-work drain has no deadline. A host that
|
|
1195
|
+
// breaks the "no blocking closure may wait on a JavaScript turn" rule keeps
|
|
1196
|
+
// its disposal promise pending instead of wedging the thread.
|
|
1197
|
+
t.false(
|
|
1198
|
+
code.includes('__WASM_RUNTIME_DRAIN_TURNS'),
|
|
1199
|
+
'the poll must not carry a turn budget',
|
|
1200
|
+
)
|
|
1201
|
+
t.is(
|
|
1202
|
+
code.split('for (;;) {').length - 1,
|
|
1203
|
+
1,
|
|
1204
|
+
'the poll must be an unbounded loop',
|
|
1205
|
+
)
|
|
1206
|
+
t.true(
|
|
1207
|
+
code.includes(barrier.poll),
|
|
1208
|
+
'the poll must yield with a real referenced timer, not a macrotask spin',
|
|
1209
|
+
)
|
|
1210
|
+
// `__scheduleTimer` falls back to the macrotask scheduler when `setTimeout`
|
|
1211
|
+
// is missing or throws — but not when it is present, returns a handle and
|
|
1212
|
+
// never fires (fake timers; a host whose timers belong to an IO context
|
|
1213
|
+
// that is gone). With no budget left to bail the poll out, that host would
|
|
1214
|
+
// park it forever. Arm both until timers have actually arrived, then pace
|
|
1215
|
+
// on the timer alone rather than spinning the zero-delay queue.
|
|
1216
|
+
//
|
|
1217
|
+
// Whether they arrive is a property of the poll, never of the module: a
|
|
1218
|
+
// host can lose its timers between two disposals, and in the deferred
|
|
1219
|
+
// shape every instance shares this module — one healthy instance must not
|
|
1220
|
+
// disarm the fallback for the next one.
|
|
1221
|
+
t.false(
|
|
1222
|
+
code.includes('let __wasmRuntimePollTimerArrived'),
|
|
1223
|
+
'the pacing state must not outlive the poll that learned it',
|
|
1224
|
+
)
|
|
1225
|
+
t.is(
|
|
1226
|
+
code.split('function __createWasmRuntimePollPace()').length - 1,
|
|
1227
|
+
1,
|
|
1228
|
+
'one definition of the pacing state',
|
|
1229
|
+
)
|
|
1230
|
+
t.is(
|
|
1231
|
+
code.split('__createWasmRuntimePollPace()').length - 1,
|
|
1232
|
+
2,
|
|
1233
|
+
'and exactly one caller: the poll loop, which owns it for its own run',
|
|
1234
|
+
)
|
|
1235
|
+
const yieldStart = code.indexOf('function __yieldWasmRuntimePollTurn(')
|
|
1236
|
+
t.true(yieldStart > 0, 'the poll must yield through one shared turn helper')
|
|
1237
|
+
const yieldTurn = code.slice(yieldStart, code.indexOf('\n}\n', yieldStart))
|
|
1238
|
+
t.true(
|
|
1239
|
+
yieldTurn.includes('__scheduleTimer(') &&
|
|
1240
|
+
yieldTurn.includes('__scheduleMacrotask('),
|
|
1241
|
+
'an undecided turn must arm both primitives, so an inert setTimeout cannot park the poll',
|
|
1242
|
+
)
|
|
1243
|
+
t.true(
|
|
1244
|
+
yieldTurn.includes('pace.arrivals++'),
|
|
1245
|
+
'the timer callback must count that timers work on this host',
|
|
1246
|
+
)
|
|
1247
|
+
t.true(
|
|
1248
|
+
yieldTurn.includes(
|
|
1249
|
+
'pace.arrivals < __WASM_RUNTIME_WORK_POLL_TRUSTED_ARRIVALS',
|
|
1250
|
+
),
|
|
1251
|
+
'and one arrival must not be enough: it can be a timer armed before the host stopped running them',
|
|
1252
|
+
)
|
|
1253
|
+
// A poll that has settled onto the timer alone has nothing left to fall
|
|
1254
|
+
// back on if the timers stop mid-poll — the turn that armed the dead timer
|
|
1255
|
+
// is the turn that parks, and a parked poll schedules nothing that could
|
|
1256
|
+
// notice. Every turn keeps a longer timer outstanding for that.
|
|
1257
|
+
t.true(
|
|
1258
|
+
yieldTurn.includes('__armWasmRuntimePollStallBackup('),
|
|
1259
|
+
'every turn must keep a stall backup outstanding, armed while the timers still work',
|
|
1260
|
+
)
|
|
1261
|
+
const backupStart = code.indexOf(
|
|
1262
|
+
'function __armWasmRuntimePollStallBackup(',
|
|
1263
|
+
)
|
|
1264
|
+
t.true(backupStart > 0, 'the backup must be one shared helper')
|
|
1265
|
+
const stallBackup = code.slice(
|
|
1266
|
+
backupStart,
|
|
1267
|
+
code.indexOf('\n}\n', backupStart),
|
|
1268
|
+
)
|
|
1269
|
+
t.true(
|
|
1270
|
+
stallBackup.includes('pace.arrivals = 0'),
|
|
1271
|
+
'a turn the backup has to end proves the timers stopped: the poll goes back to arming both',
|
|
1272
|
+
)
|
|
1273
|
+
t.true(
|
|
1274
|
+
stallBackup.includes('pace.settleTurn'),
|
|
1275
|
+
'and the backup must end whichever turn is parked, not the one that armed it',
|
|
1276
|
+
)
|
|
1277
|
+
// By due time, never by how long the parked turn has been waiting. A host
|
|
1278
|
+
// runs its timers in due order, so a backup that runs while a turn due a
|
|
1279
|
+
// window earlier is still parked proves that turn's timer was dropped —
|
|
1280
|
+
// and a healthy turn, whose timer runs first and clears `settleTurn`, is
|
|
1281
|
+
// never touched. Measuring the wait instead has a phase hole: arms spaced
|
|
1282
|
+
// further apart than the window leave the turn that parks between them
|
|
1283
|
+
// with no backup young enough to rescue it.
|
|
1284
|
+
t.regex(
|
|
1285
|
+
stallBackup,
|
|
1286
|
+
/pace\.turnTimerDueAt > (?:__)?dueAt - __WASM_RUNTIME_WORK_POLL_STALL_MS/,
|
|
1287
|
+
'the backup must judge by due time, so there is no phase to fall through',
|
|
1288
|
+
)
|
|
1289
|
+
t.false(
|
|
1290
|
+
stallBackup.includes('Date.now() -'),
|
|
1291
|
+
'and it must not measure how long the parked turn has waited',
|
|
1292
|
+
)
|
|
1293
|
+
t.true(
|
|
1294
|
+
yieldTurn.includes('pace.settleTurn = undefined'),
|
|
1295
|
+
'a turn that ends must stop being the parked one, or a later backup reads a due time already answered',
|
|
1296
|
+
)
|
|
1297
|
+
t.regex(
|
|
1298
|
+
yieldTurn,
|
|
1299
|
+
/if \(__settled\??\) \{\s*return\s*\}|if \(settled\) \{\s*return\s*\}/,
|
|
1300
|
+
'whichever primitive loses the race must resolve nothing: one poll per turn',
|
|
1301
|
+
)
|
|
1302
|
+
t.true(
|
|
1303
|
+
code.includes(barrier.finish),
|
|
1304
|
+
'finish must run whether the poll ended, timed out or could not run at all',
|
|
1305
|
+
)
|
|
1306
|
+
// The one caller that cannot yield is the raw `Context.destroy()` the
|
|
1307
|
+
// wrapper intercepts, and the queue it leaves behind is discarded by the
|
|
1308
|
+
// destroy that follows. Say so, once, without throwing.
|
|
1309
|
+
t.true(
|
|
1310
|
+
code.includes(barrier.report),
|
|
1311
|
+
'the single-call barrier must report settlements nothing can reach any more',
|
|
1312
|
+
)
|
|
1313
|
+
// The window between the halves spans real event-loop turns, so a caller
|
|
1314
|
+
// that cannot yield — the CJS 'exit' teardown, the managed beforeExit one —
|
|
1315
|
+
// can land in the middle of one. It cannot wait for the poll; it has to
|
|
1316
|
+
// close the handshake itself, because `…_finish` is the call that joins and
|
|
1317
|
+
// lowers the barrier. Without this the context is destroyed with the
|
|
1318
|
+
// barrier still raised, the runtime never joined and the destroy recorded
|
|
1319
|
+
// as done.
|
|
1320
|
+
t.is(
|
|
1321
|
+
code.split(barrier.publishParked).length - 1,
|
|
1322
|
+
1,
|
|
1323
|
+
'the poll must publish a closer before it yields',
|
|
1324
|
+
)
|
|
1325
|
+
t.is(
|
|
1326
|
+
code.split(barrier.clearParked).length - 1,
|
|
1327
|
+
1,
|
|
1328
|
+
'finishing the handshake must retract that closer exactly once',
|
|
1329
|
+
)
|
|
1330
|
+
const parkedIndex = code.indexOf(`const ${barrier.parked}`)
|
|
1331
|
+
t.true(
|
|
1332
|
+
parkedIndex > 0 || code.includes(`let ${barrier.parked}`),
|
|
1333
|
+
'the closer must live outside the barrier, where a non-yielding caller can reach it',
|
|
1334
|
+
)
|
|
1335
|
+
// And the single call is where it is reached: every teardown path runs the
|
|
1336
|
+
// barrier through it before destroying, so closing a parked handshake there
|
|
1337
|
+
// covers all of them at once.
|
|
1338
|
+
const isEager = barrier.parked === '__finishParkedWasmEnvCleanup'
|
|
1339
|
+
const prepareStart = code.indexOf(
|
|
1340
|
+
isEager
|
|
1341
|
+
? `function ${barrier.fallback.replace('()', '')}() {`
|
|
1342
|
+
: `const ${barrier.fallback.replace('()', '')} = () => {`,
|
|
1343
|
+
)
|
|
1344
|
+
t.true(prepareStart > 0, 'the single-call barrier must be one function')
|
|
1345
|
+
const prepareBody = code.slice(
|
|
1346
|
+
prepareStart,
|
|
1347
|
+
code.indexOf(isEager ? '\n}\n' : '\n }\n', prepareStart),
|
|
1348
|
+
)
|
|
1349
|
+
const closerIndex = prepareBody.indexOf(barrier.parked)
|
|
1350
|
+
t.true(
|
|
1351
|
+
closerIndex > 0,
|
|
1352
|
+
'the single call must close a parked handshake instead of skipping the barrier',
|
|
1353
|
+
)
|
|
1354
|
+
t.true(
|
|
1355
|
+
closerIndex < prepareBody.indexOf('napi_prepare_wasm_env_cleanup'),
|
|
1356
|
+
'and it must close it before looking the single-call export up: a parked handshake is already begun',
|
|
1357
|
+
)
|
|
1358
|
+
})
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1126
1361
|
// The loaders order their own teardown barrier-then-destroy, but the emnapi
|
|
1127
1362
|
// context is a live object: an embedder or test harness holding it, or emnapi's
|
|
1128
1363
|
// own `beforeExit` auto-destroy on a host where `suppressDestroy()` is absent,
|
|
@@ -1140,14 +1375,22 @@ const CONTEXT_DESTROY_WRAP_SIGNATURE =
|
|
|
1140
1375
|
const preparingBarrierGuards = {
|
|
1141
1376
|
shared: {
|
|
1142
1377
|
probe: '__isPreparingWasmEnvCleanup',
|
|
1378
|
+
// Both barrier entry points read it: the single call and the two-phase
|
|
1379
|
+
// form, which keeps it raised across the turns it yields. The single call
|
|
1380
|
+
// splits the check in two, because a handshake parked between the halves is
|
|
1381
|
+
// one it closes rather than refuses — see the two-phase test above — so the
|
|
1382
|
+
// shape below is the reentrancy half alone.
|
|
1383
|
+
entryGuard: ` if (__emnapiWasmEnvCleanupPreparing) {
|
|
1384
|
+
return
|
|
1385
|
+
}`,
|
|
1386
|
+
twoPhaseEntryGuard: ` if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
|
|
1387
|
+
return
|
|
1388
|
+
}`,
|
|
1143
1389
|
snippets: [
|
|
1144
1390
|
'let __emnapiWasmEnvCleanupPreparing = false',
|
|
1145
1391
|
`function __isPreparingWasmEnvCleanup() {
|
|
1146
1392
|
return __emnapiWasmEnvCleanupPreparing
|
|
1147
1393
|
}`,
|
|
1148
|
-
` if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
|
|
1149
|
-
return
|
|
1150
|
-
}`,
|
|
1151
1394
|
` __emnapiWasmEnvCleanupPreparing = true
|
|
1152
1395
|
try {
|
|
1153
1396
|
prepare()
|
|
@@ -1158,12 +1401,15 @@ const preparingBarrierGuards = {
|
|
|
1158
1401
|
},
|
|
1159
1402
|
deferred: {
|
|
1160
1403
|
probe: '__isPreparingEnvCleanup',
|
|
1404
|
+
entryGuard: ` if (__wasmEnvCleanupPreparing) {
|
|
1405
|
+
return
|
|
1406
|
+
}`,
|
|
1407
|
+
twoPhaseEntryGuard: ` if (__wasmEnvCleanupPrepared || __wasmEnvCleanupPreparing) {
|
|
1408
|
+
return
|
|
1409
|
+
}`,
|
|
1161
1410
|
snippets: [
|
|
1162
1411
|
'let __wasmEnvCleanupPreparing = false',
|
|
1163
1412
|
'const __isPreparingEnvCleanup = () => __wasmEnvCleanupPreparing',
|
|
1164
|
-
` if (__wasmEnvCleanupPrepared || __wasmEnvCleanupPreparing) {
|
|
1165
|
-
return
|
|
1166
|
-
}`,
|
|
1167
1413
|
` __wasmEnvCleanupPreparing = true
|
|
1168
1414
|
try {
|
|
1169
1415
|
__prepareWasmEnvCleanup()
|
|
@@ -1267,6 +1513,20 @@ for (const { name, code, prepare, guard } of wrappedContextCreationCases) {
|
|
|
1267
1513
|
`barrier must carry its in-flight guard exactly once: ${snippet}`,
|
|
1268
1514
|
)
|
|
1269
1515
|
}
|
|
1516
|
+
// Both barrier entry points — the single call and the two-phase form —
|
|
1517
|
+
// refuse to re-enter a barrier already in flight. The two-phase form still
|
|
1518
|
+
// tests the pair in one condition; the single call tests the reentrancy
|
|
1519
|
+
// half on its own, after it has dealt with a parked handshake.
|
|
1520
|
+
t.is(
|
|
1521
|
+
code.split(guard.entryGuard).length - 1,
|
|
1522
|
+
1,
|
|
1523
|
+
'the single call must refuse to re-enter a barrier already in flight',
|
|
1524
|
+
)
|
|
1525
|
+
t.is(
|
|
1526
|
+
code.split(guard.twoPhaseEntryGuard).length - 1,
|
|
1527
|
+
1,
|
|
1528
|
+
'the two-phase barrier must refuse to re-enter a barrier already in flight',
|
|
1529
|
+
)
|
|
1270
1530
|
t.is(
|
|
1271
1531
|
code.split(NESTED_DESTROY_NO_OP).length - 1,
|
|
1272
1532
|
1,
|
package/src/api/build.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { BuildOptions as RawBuildOptions } from '../def/build.js'
|
|
|
21
21
|
import {
|
|
22
22
|
CLI_VERSION,
|
|
23
23
|
commitFileSystemTransaction,
|
|
24
|
+
commonJsDeclarationBarrier,
|
|
24
25
|
copyFileAtomic,
|
|
25
26
|
type Crate,
|
|
26
27
|
debugFactory,
|
|
@@ -41,7 +42,9 @@ import {
|
|
|
41
42
|
removeNodeStreamWebTypeImports,
|
|
42
43
|
rewriteUnboundNodeGlobalTypeQueries,
|
|
43
44
|
rewriteTypeImportReferences,
|
|
45
|
+
checkSelfsign,
|
|
44
46
|
scanExportedName,
|
|
47
|
+
signFileAtomic,
|
|
45
48
|
type Target,
|
|
46
49
|
targetToEnvVar,
|
|
47
50
|
tryInstallCargoBinary,
|
|
@@ -253,7 +256,7 @@ export function checkAsyncRuntimeHostContract({
|
|
|
253
256
|
}
|
|
254
257
|
if (!asyncRuntime && missingHostExports.length === 0) {
|
|
255
258
|
return {
|
|
256
|
-
warning: `${packageName} exports the napi-async-runtime host contract but napi.wasm.asyncRuntime is not enabled. The generated WASI loaders will not install the CurrentThread task and timer hosts, so
|
|
259
|
+
warning: `${packageName} exports the napi-async-runtime host contract but napi.wasm.asyncRuntime is not enabled. The generated WASI loaders will not install the CurrentThread task and timer hosts, so a binding running the CurrentThread flavor (the default on every wasm target) will make no progress unless the hosts are installed by hand. A wasm32-wasip1-threads binding that configures MultiThread does not need them.`,
|
|
257
260
|
}
|
|
258
261
|
}
|
|
259
262
|
return {}
|
|
@@ -795,6 +798,16 @@ export function bindingTargetDeclarationPredicate(input: {
|
|
|
795
798
|
(input.rootLoaderCandidate && (input.hasWasiFallback || exports.length > 0))
|
|
796
799
|
}
|
|
797
800
|
|
|
801
|
+
/**
|
|
802
|
+
* The `.d.cts` content for a WASI flavor loader, derived from the root
|
|
803
|
+
* declaration this build wrote.
|
|
804
|
+
*
|
|
805
|
+
* An ESM source (`*.d.mts`, or `*.d.ts` in a `"type": "module"` package) is
|
|
806
|
+
* re-declared verbatim when nothing in it changes meaning under CommonJS
|
|
807
|
+
* interpretation — which is everything a generated typedef and most
|
|
808
|
+
* `--dts-header`s contain. {@link commonJsDeclarationBarrier} names the
|
|
809
|
+
* constructs that still refuse, and why.
|
|
810
|
+
*/
|
|
798
811
|
export function prepareWasiBindingTypeDef(
|
|
799
812
|
source: string,
|
|
800
813
|
sourcePath: string,
|
|
@@ -803,12 +816,15 @@ export function prepareWasiBindingTypeDef(
|
|
|
803
816
|
packageType?: 'module' | 'commonjs',
|
|
804
817
|
) {
|
|
805
818
|
if (
|
|
806
|
-
sourcePath.endsWith('.
|
|
807
|
-
(sourcePath.endsWith('.
|
|
819
|
+
sourcePath.endsWith('.mts') ||
|
|
820
|
+
(sourcePath.endsWith('.ts') && packageType === 'module')
|
|
808
821
|
) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
822
|
+
const barrier = commonJsDeclarationBarrier(source)
|
|
823
|
+
if (barrier !== undefined) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
`Cannot emit the CommonJS WASI declaration ${destinationPath} from the ESM declaration ${sourcePath}: ${barrier} cannot be re-declared in a CommonJS declaration. Use a .d.cts --dts path for WASI builds from ESM sources.`,
|
|
826
|
+
)
|
|
827
|
+
}
|
|
812
828
|
}
|
|
813
829
|
const targetSource = hasThreads
|
|
814
830
|
? source
|
|
@@ -1124,6 +1140,7 @@ export async function buildProject(rawOptions: BuildOptions) {
|
|
|
1124
1140
|
|
|
1125
1141
|
const options: ParsedBuildOptions = {
|
|
1126
1142
|
dtsCache: true,
|
|
1143
|
+
ohosSign: true,
|
|
1127
1144
|
...rawOptions,
|
|
1128
1145
|
format: resolveBuildFormat(rawOptions),
|
|
1129
1146
|
cwd: rawOptions.cwd ?? process.cwd(),
|
|
@@ -2452,6 +2469,40 @@ class Builder {
|
|
|
2452
2469
|
} else {
|
|
2453
2470
|
await copyFileAtomic(src, dest)
|
|
2454
2471
|
artifactReplaced = true
|
|
2472
|
+
// OpenHarmony devices refuse to load unsigned `.so` files, so inject
|
|
2473
|
+
// the fs-verity `.codesign` self-signature unless `--no-ohos-sign`.
|
|
2474
|
+
if (
|
|
2475
|
+
this.target.platform === 'openharmony' &&
|
|
2476
|
+
this.options.ohosSign !== false
|
|
2477
|
+
) {
|
|
2478
|
+
debug('Self-signing OpenHarmony artifact:')
|
|
2479
|
+
debug(' %i', dest)
|
|
2480
|
+
try {
|
|
2481
|
+
// Force mode: the OHOS SDK linker can already self-sign artifacts
|
|
2482
|
+
// when its opt-in code-signing is enabled — strip that signature
|
|
2483
|
+
// and re-sign rather than failing on the existing `.codesign`
|
|
2484
|
+
// section.
|
|
2485
|
+
signFileAtomic(dest, true)
|
|
2486
|
+
// Re-verify the signature we just wrote so a corrupt or
|
|
2487
|
+
// mis-injected `.codesign` section is surfaced immediately.
|
|
2488
|
+
const { ok, reason } = checkSelfsign(readFileSync(dest))
|
|
2489
|
+
if (!ok) {
|
|
2490
|
+
debug.warn(
|
|
2491
|
+
`OpenHarmony self-sign verification failed: ${dest}: ${reason}`,
|
|
2492
|
+
)
|
|
2493
|
+
}
|
|
2494
|
+
} catch (e) {
|
|
2495
|
+
// Self-signing is best-effort: HarmonyOS only ships on aarch64,
|
|
2496
|
+
// and the algorithm (like the official binary-sign-tool) is only
|
|
2497
|
+
// exercised against aarch64 artifacts — warn instead of failing
|
|
2498
|
+
// the whole build when e.g. an x86_64 artifact cannot be signed.
|
|
2499
|
+
// signElf rejects before writing anything, so `dest` still holds
|
|
2500
|
+
// the copied artifact.
|
|
2501
|
+
debug.warn(
|
|
2502
|
+
`Skip OpenHarmony self-signing: ${dest}: ${(e as Error).message}`,
|
|
2503
|
+
)
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2455
2506
|
}
|
|
2456
2507
|
this.outputs.push({
|
|
2457
2508
|
kind: dest.endsWith('.node') ? 'node' : isWasm ? 'wasm' : 'exe',
|