@swifttui/build 0.0.6
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/AGENTS.md +44 -0
- package/README.md +46 -0
- package/cli.ts +98 -0
- package/index.ts +4 -0
- package/package.json +28 -0
- package/src/build/buildAppWasm.test.ts +178 -0
- package/src/build/buildAppWasm.ts +107 -0
- package/src/build/buildSwiftTUIWebApp.ts +22 -0
- package/src/build/generateSceneManifest.ts +46 -0
- package/src/build/optimizePackagedWasm.ts +20 -0
- package/src/build/resolveSwiftArtifacts.test.ts +90 -0
- package/src/build/resolveSwiftArtifacts.ts +253 -0
- package/src/build/runCommand.ts +81 -0
- package/src/build/stripPackagedWasm.ts +19 -0
- package/src/build/swiftCommandPrefix.ts +9 -0
- package/src/build/wasmTypeDiagnostics.ts +313 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { runCommand } from "./runCommand.ts";
|
|
4
|
+
import { swiftCommandPrefix } from "./swiftCommandPrefix.ts";
|
|
5
|
+
|
|
6
|
+
export interface ResolveSwiftArtifactsOptions {
|
|
7
|
+
configuration?: WasmBuildConfiguration;
|
|
8
|
+
extraLinkerFlags?: readonly string[];
|
|
9
|
+
extraSwiftBuildArgs?: readonly string[];
|
|
10
|
+
extraSwiftcFlags?: readonly string[];
|
|
11
|
+
initialMemory?: number | string;
|
|
12
|
+
maxMemory?: number | string;
|
|
13
|
+
packagePath: string;
|
|
14
|
+
product: string;
|
|
15
|
+
stackSize?: number | string;
|
|
16
|
+
swiftCommand?: readonly string[];
|
|
17
|
+
swiftSDK?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type WasmBuildConfiguration = "debug" | "release";
|
|
21
|
+
|
|
22
|
+
export interface SwiftArtifactPaths {
|
|
23
|
+
binPath: string;
|
|
24
|
+
wasmPath: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const requiredWasmSwiftFlags = [
|
|
28
|
+
"-Xswiftc",
|
|
29
|
+
"-Osize",
|
|
30
|
+
"-Xswiftc",
|
|
31
|
+
"-Xfrontend",
|
|
32
|
+
"-Xswiftc",
|
|
33
|
+
"-disable-llvm-merge-functions-pass",
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
export const defaultWasmSwiftSDK = "swift-6.3.1-RELEASE_wasm";
|
|
37
|
+
export const defaultInitialMemory = "536870912";
|
|
38
|
+
export const defaultMaxMemory = "4294967296";
|
|
39
|
+
export const defaultStackSize = "1048576";
|
|
40
|
+
|
|
41
|
+
export async function resolveSwiftArtifacts(
|
|
42
|
+
options: ResolveSwiftArtifactsOptions
|
|
43
|
+
): Promise<SwiftArtifactPaths> {
|
|
44
|
+
const configuration = options.configuration ?? "release";
|
|
45
|
+
const swiftlyWorkingDirectory = await resolveSwiftlyWorkingDirectory(options.packagePath);
|
|
46
|
+
const swiftCommand = [...(options.swiftCommand ?? swiftCommandPrefix())];
|
|
47
|
+
const environment = {
|
|
48
|
+
...process.env
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// The browser WebAssembly API rejects function types with more than 1000
|
|
52
|
+
// parameters. Swift's wasm release builds can trip that limit when LLVM's
|
|
53
|
+
// merge-functions pass combines large outlined-copy helpers. `-Osize` helps,
|
|
54
|
+
// but some Darwin CI runners still reproduce the failure unless we also
|
|
55
|
+
// disable that merge pass explicitly.
|
|
56
|
+
const swiftBuildArgs = [
|
|
57
|
+
"build",
|
|
58
|
+
"--package-path",
|
|
59
|
+
options.packagePath,
|
|
60
|
+
"--swift-sdk",
|
|
61
|
+
options.swiftSDK ?? defaultWasmSwiftSDK,
|
|
62
|
+
"-c",
|
|
63
|
+
configuration,
|
|
64
|
+
...requiredSwiftFlags(configuration),
|
|
65
|
+
...swiftcFlags(options.extraSwiftcFlags),
|
|
66
|
+
"-Xlinker",
|
|
67
|
+
`--initial-memory=${options.initialMemory ?? defaultInitialMemory}`,
|
|
68
|
+
"-Xlinker",
|
|
69
|
+
`--max-memory=${options.maxMemory ?? defaultMaxMemory}`,
|
|
70
|
+
"-Xlinker",
|
|
71
|
+
"-z",
|
|
72
|
+
"-Xlinker",
|
|
73
|
+
`stack-size=${options.stackSize ?? defaultStackSize}`,
|
|
74
|
+
...linkerFlags(options.extraLinkerFlags),
|
|
75
|
+
...(options.extraSwiftBuildArgs ?? []),
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
if (configuration === "release") {
|
|
79
|
+
confirmRequiredWasmFlags(swiftBuildArgs);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const buildCommand = [
|
|
83
|
+
...swiftCommand,
|
|
84
|
+
...swiftBuildArgs,
|
|
85
|
+
"--product",
|
|
86
|
+
options.product,
|
|
87
|
+
];
|
|
88
|
+
const showBinPathCommand = [
|
|
89
|
+
...swiftCommand,
|
|
90
|
+
...swiftBuildArgs,
|
|
91
|
+
"--show-bin-path",
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
logWasmBuildConfiguration({
|
|
95
|
+
configuration,
|
|
96
|
+
packagePath: options.packagePath,
|
|
97
|
+
product: options.product,
|
|
98
|
+
swiftlyWorkingDirectory,
|
|
99
|
+
buildCommand,
|
|
100
|
+
showBinPathCommand,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await runCommand(buildCommand, {
|
|
104
|
+
cwd: swiftlyWorkingDirectory,
|
|
105
|
+
env: environment,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const binPath = await runCommand(showBinPathCommand, {
|
|
109
|
+
cwd: swiftlyWorkingDirectory,
|
|
110
|
+
env: environment,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const wasmPath = join(binPath.trim(), `${options.product}.wasm`);
|
|
114
|
+
return {
|
|
115
|
+
binPath: binPath.trim(),
|
|
116
|
+
wasmPath,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface WasmBuildConfigurationLog {
|
|
121
|
+
configuration?: WasmBuildConfiguration;
|
|
122
|
+
packagePath: string;
|
|
123
|
+
product: string;
|
|
124
|
+
swiftlyWorkingDirectory: string;
|
|
125
|
+
buildCommand: string[];
|
|
126
|
+
showBinPathCommand: string[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function hasRequiredWasmFlags(args: readonly string[]): boolean {
|
|
130
|
+
return containsSubsequence(args, requiredWasmSwiftFlags);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function confirmRequiredWasmFlags(args: readonly string[]): void {
|
|
134
|
+
if (hasRequiredWasmFlags(args)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
throw new Error(
|
|
139
|
+
`missing required wasm Swift flags: ${requiredWasmSwiftFlags.join(" ")}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function requiredSwiftFlags(configuration: WasmBuildConfiguration): readonly string[] {
|
|
144
|
+
switch (configuration) {
|
|
145
|
+
case "debug":
|
|
146
|
+
return [];
|
|
147
|
+
case "release":
|
|
148
|
+
return requiredWasmSwiftFlags;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function swiftcFlags(flags: readonly string[] | undefined): string[] {
|
|
153
|
+
return (flags ?? []).flatMap((flag) => ["-Xswiftc", flag]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function linkerFlags(flags: readonly string[] | undefined): string[] {
|
|
157
|
+
return (flags ?? []).flatMap((flag) => ["-Xlinker", flag]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function logWasmBuildConfiguration(config: WasmBuildConfigurationLog): void {
|
|
161
|
+
for (const line of wasmBuildConfigurationLogLines(config)) {
|
|
162
|
+
console.error(line);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function wasmBuildConfigurationLogLines(
|
|
167
|
+
config: WasmBuildConfigurationLog
|
|
168
|
+
): string[] {
|
|
169
|
+
const configuration = config.configuration ?? "release";
|
|
170
|
+
return [
|
|
171
|
+
`WASM_BUILD_CONFIGURATION_NAME=${configuration}`,
|
|
172
|
+
`WASM_REQUIRED_FLAGS_CONFIRMED=${configuration === "release" ? "true" : "skipped"}`,
|
|
173
|
+
`WASM_REQUIRED_FLAGS=${requiredWasmSwiftFlags.join(" ")}`,
|
|
174
|
+
`WASM_REQUIRED_FLAGS_JSON=${JSON.stringify([...requiredWasmSwiftFlags])}`,
|
|
175
|
+
`WASM_BUILD_COMMAND=${formatCommandForLogs(config.buildCommand)}`,
|
|
176
|
+
`WASM_BUILD_COMMAND_ARGS_JSON=${JSON.stringify(config.buildCommand)}`,
|
|
177
|
+
`WASM_SHOW_BIN_PATH_COMMAND=${formatCommandForLogs(config.showBinPathCommand)}`,
|
|
178
|
+
`WASM_SHOW_BIN_PATH_COMMAND_ARGS_JSON=${JSON.stringify(config.showBinPathCommand)}`,
|
|
179
|
+
`WASM_BUILD_CONFIGURATION ${JSON.stringify({
|
|
180
|
+
packagePath: config.packagePath,
|
|
181
|
+
product: config.product,
|
|
182
|
+
configuration,
|
|
183
|
+
swiftlyWorkingDirectory: config.swiftlyWorkingDirectory,
|
|
184
|
+
requiredFlags: [...requiredWasmSwiftFlags],
|
|
185
|
+
buildCommand: formatCommandForLogs(config.buildCommand),
|
|
186
|
+
showBinPathCommand: formatCommandForLogs(config.showBinPathCommand),
|
|
187
|
+
})}`,
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function formatCommandForLogs(args: readonly string[]): string {
|
|
192
|
+
return args.map(shellQuote).join(" ");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function containsSubsequence(
|
|
196
|
+
args: readonly string[],
|
|
197
|
+
expected: readonly string[]
|
|
198
|
+
): boolean {
|
|
199
|
+
if (expected.length == 0) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (let index = 0; index <= args.length - expected.length; index += 1) {
|
|
204
|
+
let matches = true;
|
|
205
|
+
for (let expectedIndex = 0; expectedIndex < expected.length; expectedIndex += 1) {
|
|
206
|
+
if (args[index + expectedIndex] !== expected[expectedIndex]) {
|
|
207
|
+
matches = false;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (matches) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function shellQuote(arg: string): string {
|
|
220
|
+
if (/^[A-Za-z0-9_./:=+-]+$/.test(arg)) {
|
|
221
|
+
return arg;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function resolveSwiftlyWorkingDirectory(
|
|
228
|
+
startPath: string
|
|
229
|
+
): Promise<string> {
|
|
230
|
+
let currentPath = resolve(startPath);
|
|
231
|
+
|
|
232
|
+
while (true) {
|
|
233
|
+
if (await fileExists(join(currentPath, ".swift-version"))) {
|
|
234
|
+
return currentPath;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const parentPath = dirname(currentPath);
|
|
238
|
+
if (parentPath === currentPath) {
|
|
239
|
+
return resolve(startPath);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
currentPath = parentPath;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
247
|
+
try {
|
|
248
|
+
await access(path);
|
|
249
|
+
return true;
|
|
250
|
+
} catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { accessSync } from "node:fs";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export interface RunCommandOptions {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
env?: Record<string, string | undefined>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function runCommand(
|
|
11
|
+
cmd: string[],
|
|
12
|
+
options: RunCommandOptions = {}
|
|
13
|
+
): Promise<string> {
|
|
14
|
+
const executable = cmd[0];
|
|
15
|
+
if (!executable) {
|
|
16
|
+
throw new Error("cannot run an empty command");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const proc = spawn(executable, cmd.slice(1), {
|
|
20
|
+
cwd: options.cwd,
|
|
21
|
+
env: normalizeEnvironment(options.env),
|
|
22
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const stdoutChunks: Buffer[] = [];
|
|
26
|
+
const stderrChunks: Buffer[] = [];
|
|
27
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
28
|
+
stdoutChunks.push(chunk);
|
|
29
|
+
});
|
|
30
|
+
proc.stderr.on("data", (chunk: Buffer) => {
|
|
31
|
+
stderrChunks.push(chunk);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
35
|
+
proc.on("error", reject);
|
|
36
|
+
proc.on("close", resolve);
|
|
37
|
+
});
|
|
38
|
+
const stdout = Buffer.concat(stdoutChunks).toString();
|
|
39
|
+
const stderr = Buffer.concat(stderrChunks).toString();
|
|
40
|
+
|
|
41
|
+
if (exitCode !== 0) {
|
|
42
|
+
throw new Error([stdout, stderr].filter(Boolean).join("\n").trim() || `command failed: ${cmd.join(" ")}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return stdout;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function findExecutable(
|
|
49
|
+
name: string,
|
|
50
|
+
pathValue: string | undefined = process.env.PATH
|
|
51
|
+
): string | undefined {
|
|
52
|
+
for (const directory of pathValue?.split(delimiter) ?? []) {
|
|
53
|
+
if (!directory) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const candidate = join(directory, name);
|
|
57
|
+
try {
|
|
58
|
+
accessSync(candidate);
|
|
59
|
+
return candidate;
|
|
60
|
+
} catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeEnvironment(
|
|
68
|
+
env: Record<string, string | undefined> | undefined
|
|
69
|
+
): NodeJS.ProcessEnv | undefined {
|
|
70
|
+
if (!env) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const normalized: NodeJS.ProcessEnv = {};
|
|
75
|
+
for (const [key, value] of Object.entries(env)) {
|
|
76
|
+
if (value !== undefined) {
|
|
77
|
+
normalized[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { findExecutable, runCommand } from "./runCommand.ts";
|
|
2
|
+
|
|
3
|
+
export async function stripPackagedWasm(
|
|
4
|
+
wasmPath: string
|
|
5
|
+
): Promise<void> {
|
|
6
|
+
const objcopyPath = findExecutable("llvm-objcopy");
|
|
7
|
+
if (!objcopyPath) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
"missing llvm-objcopy in PATH; install the swiftly-managed toolchain before packaging wasm"
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
await runCommand([
|
|
14
|
+
objcopyPath,
|
|
15
|
+
"--strip-debug",
|
|
16
|
+
"--remove-section=name",
|
|
17
|
+
wasmPath,
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const wasmMagic = [0x00, 0x61, 0x73, 0x6d] as const;
|
|
4
|
+
const wasmVersion = [0x01, 0x00, 0x00, 0x00] as const;
|
|
5
|
+
const typeSectionID = 1;
|
|
6
|
+
const functionTypeTag = 0x60;
|
|
7
|
+
const browserMaxTypeParameterCount = 1000;
|
|
8
|
+
|
|
9
|
+
interface SectionHeader {
|
|
10
|
+
id: number;
|
|
11
|
+
size: number;
|
|
12
|
+
startOffset: number;
|
|
13
|
+
nextOffset: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ReadUnsignedLEB128Result {
|
|
17
|
+
nextOffset: number;
|
|
18
|
+
value: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface WasmTypeSummary {
|
|
22
|
+
maxTypeParameterCount?: number;
|
|
23
|
+
maxTypeParameterTypeIndex?: number;
|
|
24
|
+
note?: string;
|
|
25
|
+
overBrowserLimitTypes: number[];
|
|
26
|
+
topParameterCounts: Array<{
|
|
27
|
+
parameterCount: number;
|
|
28
|
+
typeIndex: number;
|
|
29
|
+
}>;
|
|
30
|
+
typeCount?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatWasmTypeDiagnostics(
|
|
34
|
+
bytes: Uint8Array
|
|
35
|
+
): string {
|
|
36
|
+
const summary = summarizeWasmTypes(bytes);
|
|
37
|
+
const components = [
|
|
38
|
+
`size=${bytes.byteLength} bytes`,
|
|
39
|
+
`sha256=${createHash("sha256").update(bytes).digest("hex")}`,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
if (summary.typeCount !== undefined) {
|
|
43
|
+
components.push(`typeCount=${summary.typeCount}`);
|
|
44
|
+
}
|
|
45
|
+
if (summary.maxTypeParameterCount !== undefined) {
|
|
46
|
+
components.push(`maxTypeParameterCount=${summary.maxTypeParameterCount}`);
|
|
47
|
+
}
|
|
48
|
+
if (summary.maxTypeParameterTypeIndex !== undefined) {
|
|
49
|
+
components.push(`maxTypeParameterTypeIndex=${summary.maxTypeParameterTypeIndex}`);
|
|
50
|
+
}
|
|
51
|
+
if (summary.overBrowserLimitTypes.length > 0) {
|
|
52
|
+
components.push(`overBrowserLimitTypes=${summary.overBrowserLimitTypes.join(",")}`);
|
|
53
|
+
}
|
|
54
|
+
if (summary.topParameterCounts.length > 0) {
|
|
55
|
+
components.push(
|
|
56
|
+
`largestTypes=${summary.topParameterCounts.map(formatLargestTypeSummary).join(";")}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (summary.note) {
|
|
60
|
+
components.push(`note=${summary.note}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return `wasm diagnostics: ${components.join(", ")}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatLargestTypeSummary(
|
|
67
|
+
summary: { parameterCount: number; typeIndex: number }
|
|
68
|
+
): string {
|
|
69
|
+
return `${summary.typeIndex}:${summary.parameterCount}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function summarizeWasmTypes(
|
|
73
|
+
bytes: Uint8Array
|
|
74
|
+
): WasmTypeSummary {
|
|
75
|
+
if (!hasExpectedPrefix(bytes, 0, wasmMagic)) {
|
|
76
|
+
return {
|
|
77
|
+
note: "missing wasm magic header",
|
|
78
|
+
overBrowserLimitTypes: [],
|
|
79
|
+
topParameterCounts: [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (!hasExpectedPrefix(bytes, wasmMagic.length, wasmVersion)) {
|
|
83
|
+
return {
|
|
84
|
+
note: "missing wasm version header",
|
|
85
|
+
overBrowserLimitTypes: [],
|
|
86
|
+
topParameterCounts: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let offset = 8;
|
|
91
|
+
while (offset < bytes.length) {
|
|
92
|
+
let sectionHeader: SectionHeader;
|
|
93
|
+
try {
|
|
94
|
+
sectionHeader = readSectionHeader(bytes, offset);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
note: `failed to read section header at byte ${offset}: ${describeError(error)}`,
|
|
98
|
+
overBrowserLimitTypes: [],
|
|
99
|
+
topParameterCounts: [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (sectionHeader.id === typeSectionID) {
|
|
104
|
+
return summarizeTypeSection(bytes, sectionHeader.startOffset, sectionHeader.nextOffset);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
offset = sectionHeader.nextOffset;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
note: "module has no type section",
|
|
112
|
+
overBrowserLimitTypes: [],
|
|
113
|
+
topParameterCounts: [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function summarizeTypeSection(
|
|
118
|
+
bytes: Uint8Array,
|
|
119
|
+
startOffset: number,
|
|
120
|
+
endOffset: number
|
|
121
|
+
): WasmTypeSummary {
|
|
122
|
+
let offset = startOffset;
|
|
123
|
+
let typeCount: number;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const result = readUnsignedLEB128(bytes, offset);
|
|
127
|
+
typeCount = result.value;
|
|
128
|
+
offset = result.nextOffset;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return {
|
|
131
|
+
note: `failed to read type vector length: ${describeError(error)}`,
|
|
132
|
+
overBrowserLimitTypes: [],
|
|
133
|
+
topParameterCounts: [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let maxTypeParameterCount = -1;
|
|
138
|
+
let maxTypeParameterTypeIndex = -1;
|
|
139
|
+
const overBrowserLimitTypes: number[] = [];
|
|
140
|
+
const topParameterCounts: Array<{ parameterCount: number; typeIndex: number }> = [];
|
|
141
|
+
|
|
142
|
+
for (let typeIndex = 0; typeIndex < typeCount; typeIndex += 1) {
|
|
143
|
+
if (offset >= endOffset) {
|
|
144
|
+
return {
|
|
145
|
+
note: `type section ended early while reading type ${typeIndex}`,
|
|
146
|
+
overBrowserLimitTypes,
|
|
147
|
+
topParameterCounts,
|
|
148
|
+
typeCount,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const typeTag = bytes[offset];
|
|
153
|
+
offset += 1;
|
|
154
|
+
if (typeTag !== functionTypeTag) {
|
|
155
|
+
return {
|
|
156
|
+
note: `unexpected type tag 0x${typeTag.toString(16)} at type ${typeIndex}`,
|
|
157
|
+
overBrowserLimitTypes,
|
|
158
|
+
topParameterCounts,
|
|
159
|
+
typeCount,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let parameterCount: number;
|
|
164
|
+
try {
|
|
165
|
+
const result = readUnsignedLEB128(bytes, offset);
|
|
166
|
+
parameterCount = result.value;
|
|
167
|
+
offset = result.nextOffset;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return {
|
|
170
|
+
note: `failed to read parameter count for type ${typeIndex}: ${describeError(error)}`,
|
|
171
|
+
overBrowserLimitTypes,
|
|
172
|
+
topParameterCounts,
|
|
173
|
+
typeCount,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (parameterCount > maxTypeParameterCount) {
|
|
178
|
+
maxTypeParameterCount = parameterCount;
|
|
179
|
+
maxTypeParameterTypeIndex = typeIndex;
|
|
180
|
+
}
|
|
181
|
+
if (parameterCount > browserMaxTypeParameterCount) {
|
|
182
|
+
overBrowserLimitTypes.push(typeIndex);
|
|
183
|
+
}
|
|
184
|
+
updateLargestTypeSummaries(topParameterCounts, typeIndex, parameterCount);
|
|
185
|
+
|
|
186
|
+
offset += parameterCount;
|
|
187
|
+
if (offset > endOffset) {
|
|
188
|
+
return {
|
|
189
|
+
note: `parameter vector for type ${typeIndex} overruns type section`,
|
|
190
|
+
overBrowserLimitTypes,
|
|
191
|
+
topParameterCounts,
|
|
192
|
+
typeCount,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let resultCount: number;
|
|
197
|
+
try {
|
|
198
|
+
const result = readUnsignedLEB128(bytes, offset);
|
|
199
|
+
resultCount = result.value;
|
|
200
|
+
offset = result.nextOffset;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
note: `failed to read result count for type ${typeIndex}: ${describeError(error)}`,
|
|
204
|
+
overBrowserLimitTypes,
|
|
205
|
+
topParameterCounts,
|
|
206
|
+
typeCount,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
offset += resultCount;
|
|
211
|
+
if (offset > endOffset) {
|
|
212
|
+
return {
|
|
213
|
+
note: `result vector for type ${typeIndex} overruns type section`,
|
|
214
|
+
overBrowserLimitTypes,
|
|
215
|
+
topParameterCounts,
|
|
216
|
+
typeCount,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
maxTypeParameterCount: maxTypeParameterCount >= 0 ? maxTypeParameterCount : undefined,
|
|
223
|
+
maxTypeParameterTypeIndex: maxTypeParameterTypeIndex >= 0 ? maxTypeParameterTypeIndex : undefined,
|
|
224
|
+
overBrowserLimitTypes,
|
|
225
|
+
topParameterCounts,
|
|
226
|
+
typeCount,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function updateLargestTypeSummaries(
|
|
231
|
+
summaries: Array<{ parameterCount: number; typeIndex: number }>,
|
|
232
|
+
typeIndex: number,
|
|
233
|
+
parameterCount: number
|
|
234
|
+
): void {
|
|
235
|
+
summaries.push({ parameterCount, typeIndex });
|
|
236
|
+
summaries.sort((left, right) => {
|
|
237
|
+
if (right.parameterCount === left.parameterCount) {
|
|
238
|
+
return left.typeIndex - right.typeIndex;
|
|
239
|
+
}
|
|
240
|
+
return right.parameterCount - left.parameterCount;
|
|
241
|
+
});
|
|
242
|
+
summaries.splice(5);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function readSectionHeader(
|
|
246
|
+
bytes: Uint8Array,
|
|
247
|
+
offset: number
|
|
248
|
+
): SectionHeader {
|
|
249
|
+
if (offset >= bytes.length) {
|
|
250
|
+
throw new Error("unexpected end of file");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const id = bytes[offset];
|
|
254
|
+
const size = readUnsignedLEB128(bytes, offset + 1);
|
|
255
|
+
const startOffset = size.nextOffset;
|
|
256
|
+
const nextOffset = startOffset + size.value;
|
|
257
|
+
if (nextOffset > bytes.length) {
|
|
258
|
+
throw new Error(`section ${id} overruns file bounds`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
id,
|
|
263
|
+
size: size.value,
|
|
264
|
+
startOffset,
|
|
265
|
+
nextOffset,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function readUnsignedLEB128(
|
|
270
|
+
bytes: Uint8Array,
|
|
271
|
+
offset: number
|
|
272
|
+
): ReadUnsignedLEB128Result {
|
|
273
|
+
let shift = 0;
|
|
274
|
+
let value = 0;
|
|
275
|
+
let nextOffset = offset;
|
|
276
|
+
|
|
277
|
+
while (nextOffset < bytes.length) {
|
|
278
|
+
const byte = bytes[nextOffset];
|
|
279
|
+
nextOffset += 1;
|
|
280
|
+
value |= (byte & 0x7f) << shift;
|
|
281
|
+
if ((byte & 0x80) === 0) {
|
|
282
|
+
return {
|
|
283
|
+
nextOffset,
|
|
284
|
+
value,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
shift += 7;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
throw new Error("unterminated LEB128 value");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function hasExpectedPrefix(
|
|
294
|
+
bytes: Uint8Array,
|
|
295
|
+
startOffset: number,
|
|
296
|
+
expected: readonly number[]
|
|
297
|
+
): boolean {
|
|
298
|
+
if (startOffset + expected.length > bytes.length) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
303
|
+
if (bytes[startOffset + index] !== expected[index]) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function describeError(error: unknown): string {
|
|
312
|
+
return error instanceof Error ? error.message : String(error);
|
|
313
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "Preserve",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"verbatimModuleSyntax": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
"noUncheckedIndexedAccess": true,
|
|
16
|
+
"noImplicitOverride": true,
|
|
17
|
+
"noUnusedLocals": false,
|
|
18
|
+
"noUnusedParameters": false,
|
|
19
|
+
"noPropertyAccessFromIndexSignature": false
|
|
20
|
+
}
|
|
21
|
+
}
|