@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 ADDED
@@ -0,0 +1,44 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for agentic assistants working in **`@swifttui/build`**. Keep this
4
+ concise; [`README.md`](README.md) is the full reference.
5
+
6
+ ## What this package is
7
+
8
+ The **build/packaging tooling** for SwiftTUI browser apps — the sibling of
9
+ [`@swifttui/web`](../web) (which owns the runtime). It captures the Swift app's
10
+ scene manifest and packages its WASI/wasm artifact for the browser. Exposes the
11
+ `swifttui-web` CLI (see `bin` in `package.json`) and a programmatic `index.ts`.
12
+
13
+ Keep the split clean: **packaging/build steps live here; browser-safe runtime
14
+ APIs live in `@swifttui/web`.** This package depends on `@swifttui/web`.
15
+
16
+ ## Toolchains
17
+
18
+ - **Bun** for the CLI, bundling, and tests.
19
+ - **`swiftly`** Swift 6.3.1 for the wasm build it invokes
20
+ (`swiftly run swift ...`), not bare `swift`.
21
+
22
+ ## Commands
23
+
24
+ ```bash
25
+ bun test # package tests
26
+ bun run build:manifest -- --app <Exe> # capture TUIGUI_MODE=manifest output
27
+ bun run build:wasm -- --app <Exe> # copy + validate the app's wasm
28
+ bun run build -- --app <Exe> # manifest + wasm
29
+ ```
30
+
31
+ `build:wasm`/`build` default to `--configuration release`; pass
32
+ `--configuration debug` for local debug wasm.
33
+
34
+ ## Gotcha
35
+
36
+ WASI release builds need specific flags (`-Osize` plus
37
+ `-disable-llvm-merge-functions-pass`) to stay under the browser WebAssembly
38
+ API's 1000-parameter limit. The canonical command lives in this package's build
39
+ code — don't hand-roll the swift invocation. See
40
+ [`WebExample`](../../../swift-tui-examples/WebExample) for the full rationale.
41
+
42
+ ## Conventions
43
+
44
+ `AGENTS.md` is the real file; `CLAUDE.md` is a symlink to it. Edit `AGENTS.md`.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # `@swifttui/build`
2
+
3
+ Build-time package for SwiftTUI browser deployment.
4
+
5
+ This package owns manifest generation, Swift WASI builds, browser
6
+ `WebAssembly.compile` validation, and wasm packaging. It intentionally sits
7
+ outside `@swifttui/web` so browser runtime imports do not pull in Swift process
8
+ spawning, Node filesystem APIs, or wasm packaging helpers.
9
+
10
+ Publication status: the package name is reserved for the first public web
11
+ release. Until it is published to npm or attached as a public release tarball,
12
+ use the source checkout and the `swift-tui-examples/WebExample` template.
13
+
14
+ ## API
15
+
16
+ ```ts
17
+ import { buildSwiftTUIWebApp } from "@swifttui/build";
18
+
19
+ await buildSwiftTUIWebApp({
20
+ packagePath: ".",
21
+ product: "MyApp",
22
+ outputDirectory: "dist",
23
+ });
24
+ ```
25
+
26
+ Toolchain defaults match the repo:
27
+
28
+ - Swift command: `swiftly run swift` when `swiftly` is on `PATH`, otherwise
29
+ `swift`
30
+ - SDK: `swift-6.3.1-RELEASE_wasm`
31
+ - Release Swift flags:
32
+ `-Xswiftc -Osize -Xswiftc -Xfrontend -Xswiftc -disable-llvm-merge-functions-pass`
33
+ - Initial memory: `536870912`
34
+ - Max memory: `4294967296`
35
+ - Stack size: `1048576`
36
+
37
+ Callers can override `swiftCommand`, `swiftSDK`, `configuration`,
38
+ `initialMemory`, `maxMemory`, `stackSize`, `extraSwiftcFlags`,
39
+ `extraLinkerFlags`, and `extraSwiftBuildArgs`.
40
+
41
+ ## Scripts
42
+
43
+ - `bun test`
44
+ - `bun run build:manifest -- --app <AppExecutable>`
45
+ - `bun run build:wasm -- --app <AppExecutable>`
46
+ - `bun run build -- --app <AppExecutable>`
package/cli.ts ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { resolve } from "node:path";
4
+ import {
5
+ buildAppWasm,
6
+ buildSwiftTUIWebApp,
7
+ generateSceneManifest,
8
+ type WasmBuildConfiguration,
9
+ } from "./index.ts";
10
+
11
+ void runCli(process.argv.slice(2));
12
+
13
+ async function runCli(argv: string[]): Promise<void> {
14
+ const command = argv[0] ?? "build";
15
+ const flags = parseFlags(argv.slice(1));
16
+ const packagePath = resolve(flags["package-path"] ?? "../../");
17
+ const distPath = resolve(flags["dist"] ?? "./dist");
18
+ const appExecutable = flags.app ?? flags.product ?? flags["app-product"] ?? "";
19
+ const configuration = parseWasmBuildConfiguration(flags.configuration ?? "release");
20
+
21
+ switch (command) {
22
+ case "build:manifest":
23
+ assertAppExecutable(appExecutable);
24
+ await generateSceneManifest({
25
+ packagePath,
26
+ outputPath: resolve(distPath, "scene-manifest.json"),
27
+ appExecutable,
28
+ });
29
+ return;
30
+ case "build:wasm":
31
+ assertAppExecutable(appExecutable);
32
+ await buildAppWasm({
33
+ configuration,
34
+ packagePath,
35
+ outputDirectory: distPath,
36
+ product: appExecutable,
37
+ });
38
+ return;
39
+ case "build":
40
+ assertAppExecutable(appExecutable);
41
+ await buildSwiftTUIWebApp({
42
+ configuration,
43
+ packagePath,
44
+ outputDirectory: distPath,
45
+ product: appExecutable,
46
+ });
47
+ return;
48
+ default:
49
+ throw new Error(`unknown command: ${command}`);
50
+ }
51
+ }
52
+
53
+ function parseFlags(
54
+ argv: string[]
55
+ ): Record<string, string> {
56
+ const flags: Record<string, string> = {};
57
+ for (let index = 0; index < argv.length; index += 1) {
58
+ const value = argv[index];
59
+ if (!value.startsWith("--")) {
60
+ continue;
61
+ }
62
+ const equalsIndex = value.indexOf("=");
63
+ if (equalsIndex !== -1) {
64
+ flags[value.slice(2, equalsIndex)] = value.slice(equalsIndex + 1);
65
+ continue;
66
+ }
67
+ const name = value.slice(2);
68
+ const next = argv[index + 1];
69
+ if (next && !next.startsWith("--")) {
70
+ flags[name] = next;
71
+ index += 1;
72
+ } else {
73
+ flags[name] = "true";
74
+ }
75
+ }
76
+ return flags;
77
+ }
78
+
79
+ function parseWasmBuildConfiguration(
80
+ value: string
81
+ ): WasmBuildConfiguration {
82
+ switch (value) {
83
+ case "debug":
84
+ return "debug";
85
+ case "release":
86
+ return "release";
87
+ default:
88
+ throw new Error(`unsupported wasm build configuration: ${value}`);
89
+ }
90
+ }
91
+
92
+ function assertAppExecutable(
93
+ value: string
94
+ ): asserts value is string {
95
+ if (!value) {
96
+ throw new Error("missing --app or --product flag");
97
+ }
98
+ }
package/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./src/build/buildAppWasm.ts";
2
+ export * from "./src/build/buildSwiftTUIWebApp.ts";
3
+ export * from "./src/build/generateSceneManifest.ts";
4
+ export * from "./src/build/resolveSwiftArtifacts.ts";
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@swifttui/build",
3
+ "version": "0.0.6",
4
+ "license": "MIT",
5
+ "module": "index.ts",
6
+ "bin": {
7
+ "swifttui-web": "./cli.ts"
8
+ },
9
+ "exports": {
10
+ ".": "./index.ts"
11
+ },
12
+ "type": "module",
13
+ "scripts": {
14
+ "build:manifest": "bun run cli.ts build:manifest",
15
+ "build:wasm": "bun run cli.ts build:wasm",
16
+ "build": "bun run cli.ts build",
17
+ "test": "bun test"
18
+ },
19
+ "dependencies": {
20
+ "@swifttui/web": "0.0.6"
21
+ },
22
+ "devDependencies": {
23
+ "@types/bun": "1.3.13"
24
+ },
25
+ "peerDependencies": {
26
+ "typescript": "^5"
27
+ }
28
+ }
@@ -0,0 +1,178 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { packageBrowserValidatedWasm } from "./buildAppWasm.ts";
6
+
7
+ const minimalWasmBytes = new Uint8Array([
8
+ 0x00, 0x61, 0x73, 0x6d,
9
+ 0x01, 0x00, 0x00, 0x00,
10
+ ]);
11
+
12
+ const temporaryDirectories: string[] = [];
13
+
14
+ afterEach(async () => {
15
+ for (const directory of temporaryDirectories.splice(0)) {
16
+ await rm(directory, { recursive: true, force: true });
17
+ }
18
+ });
19
+
20
+ test("falls back to the original wasm when strip tooling throws", async () => {
21
+ const fixture = await createFixture();
22
+ const warnings: string[] = [];
23
+
24
+ await packageBrowserValidatedWasm({
25
+ optimize: async () => {},
26
+ sourceWasmPath: fixture.sourceWasmPath,
27
+ outputWasmPath: fixture.outputWasmPath,
28
+ strip: async () => {
29
+ throw new Error("missing llvm-objcopy");
30
+ },
31
+ onWarning: (warning) => warnings.push(warning),
32
+ });
33
+
34
+ expect(await Bun.file(fixture.outputWasmPath).bytes())
35
+ .toEqual(minimalWasmBytes);
36
+ expect(warnings).toHaveLength(1);
37
+ expect(warnings[0]).toContain("keeping unstripped wasm");
38
+ expect(warnings[0]).toContain("missing llvm-objcopy");
39
+ await WebAssembly.compile(await Bun.file(fixture.outputWasmPath).arrayBuffer());
40
+ });
41
+
42
+ test("falls back to the original wasm when stripping corrupts the artifact", async () => {
43
+ const fixture = await createFixture();
44
+ const warnings: string[] = [];
45
+
46
+ await packageBrowserValidatedWasm({
47
+ optimize: async () => {},
48
+ sourceWasmPath: fixture.sourceWasmPath,
49
+ outputWasmPath: fixture.outputWasmPath,
50
+ strip: async (wasmPath) => {
51
+ await Bun.write(wasmPath, new Uint8Array([0x00, 0x61, 0x73, 0x6d]));
52
+ },
53
+ onWarning: (warning) => warnings.push(warning),
54
+ });
55
+
56
+ expect(await Bun.file(fixture.outputWasmPath).bytes())
57
+ .toEqual(minimalWasmBytes);
58
+ expect(warnings).toHaveLength(1);
59
+ expect(warnings[0])
60
+ .toContain("stripped wasm does not parse in browser WebAssembly");
61
+ await WebAssembly.compile(await Bun.file(fixture.outputWasmPath).arrayBuffer());
62
+ });
63
+
64
+ test("fails when the source wasm itself is not browser-parseable", async () => {
65
+ const fixture = await createFixture(buildHugeFunctionTypeWasm(1001));
66
+
67
+ await expect(
68
+ packageBrowserValidatedWasm({
69
+ optimize: async () => {},
70
+ sourceWasmPath: fixture.sourceWasmPath,
71
+ outputWasmPath: fixture.outputWasmPath,
72
+ strip: async () => {},
73
+ })
74
+ ).rejects.toThrow("generated wasm does not parse in browser WebAssembly");
75
+ await expect(
76
+ packageBrowserValidatedWasm({
77
+ optimize: async () => {},
78
+ sourceWasmPath: fixture.sourceWasmPath,
79
+ outputWasmPath: fixture.outputWasmPath,
80
+ strip: async () => {},
81
+ })
82
+ ).rejects.toThrow("maxTypeParameterCount=1001");
83
+ await expect(
84
+ packageBrowserValidatedWasm({
85
+ optimize: async () => {},
86
+ sourceWasmPath: fixture.sourceWasmPath,
87
+ outputWasmPath: fixture.outputWasmPath,
88
+ strip: async () => {},
89
+ })
90
+ ).rejects.toThrow("overBrowserLimitTypes=0");
91
+ });
92
+
93
+ test("uses optimized wasm when the raw compiler output is not browser-parseable", async () => {
94
+ const fixture = await createFixture(buildHugeFunctionTypeWasm(1001));
95
+
96
+ await packageBrowserValidatedWasm({
97
+ optimize: async (wasmPath) => {
98
+ await Bun.write(wasmPath, minimalWasmBytes);
99
+ },
100
+ sourceWasmPath: fixture.sourceWasmPath,
101
+ outputWasmPath: fixture.outputWasmPath,
102
+ strip: async () => {},
103
+ });
104
+
105
+ expect(await Bun.file(fixture.outputWasmPath).bytes())
106
+ .toEqual(minimalWasmBytes);
107
+ });
108
+
109
+ test("reports the optimization failure when the raw wasm is still invalid", async () => {
110
+ const fixture = await createFixture(buildHugeFunctionTypeWasm(1001));
111
+
112
+ await expect(
113
+ packageBrowserValidatedWasm({
114
+ optimize: async () => {
115
+ throw new Error("missing wasm-opt");
116
+ },
117
+ sourceWasmPath: fixture.sourceWasmPath,
118
+ outputWasmPath: fixture.outputWasmPath,
119
+ strip: async () => {},
120
+ })
121
+ ).rejects.toThrow("wasm optimization step failed: missing wasm-opt");
122
+ });
123
+
124
+ async function createFixture(
125
+ sourceBytes: Uint8Array = minimalWasmBytes
126
+ ): Promise<{ sourceWasmPath: string; outputWasmPath: string }> {
127
+ const directory = await mkdtemp(join(tmpdir(), "webhost-wasm-"));
128
+ temporaryDirectories.push(directory);
129
+
130
+ const sourceWasmPath = join(directory, "source.wasm");
131
+ const outputWasmPath = join(directory, "output.wasm");
132
+ await Bun.write(sourceWasmPath, sourceBytes);
133
+
134
+ return {
135
+ sourceWasmPath,
136
+ outputWasmPath,
137
+ };
138
+ }
139
+
140
+ function buildHugeFunctionTypeWasm(
141
+ parameterCount: number
142
+ ): Uint8Array {
143
+ const payload = [
144
+ ...encodeUnsignedLEB128(1),
145
+ 0x60,
146
+ ...encodeUnsignedLEB128(parameterCount),
147
+ ...new Array<number>(parameterCount).fill(0x7f),
148
+ 0x00,
149
+ ];
150
+
151
+ return new Uint8Array([
152
+ ...minimalWasmBytes,
153
+ 0x01,
154
+ ...encodeUnsignedLEB128(payload.length),
155
+ ...payload,
156
+ ]);
157
+ }
158
+
159
+ function encodeUnsignedLEB128(
160
+ value: number
161
+ ): number[] {
162
+ if (value < 0) {
163
+ throw new Error("LEB128 values must be non-negative");
164
+ }
165
+
166
+ const bytes: number[] = [];
167
+ let remaining = value;
168
+ do {
169
+ let byte = remaining & 0x7f;
170
+ remaining >>>= 7;
171
+ if (remaining !== 0) {
172
+ byte |= 0x80;
173
+ }
174
+ bytes.push(byte);
175
+ } while (remaining !== 0);
176
+
177
+ return bytes;
178
+ }
@@ -0,0 +1,107 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { optimizePackagedWasm } from "./optimizePackagedWasm.ts";
4
+ import {
5
+ resolveSwiftArtifacts,
6
+ type ResolveSwiftArtifactsOptions,
7
+ type SwiftArtifactPaths,
8
+ type WasmBuildConfiguration,
9
+ } from "./resolveSwiftArtifacts.ts";
10
+ import { stripPackagedWasm } from "./stripPackagedWasm.ts";
11
+ import { formatWasmTypeDiagnostics } from "./wasmTypeDiagnostics.ts";
12
+
13
+ export interface BuildAppWasmOptions extends ResolveSwiftArtifactsOptions {
14
+ configuration?: WasmBuildConfiguration;
15
+ packagePath: string;
16
+ outputDirectory: string;
17
+ product: string;
18
+ }
19
+
20
+ export async function buildAppWasm(
21
+ options: BuildAppWasmOptions
22
+ ): Promise<SwiftArtifactPaths> {
23
+ const artifacts = await resolveSwiftArtifacts(options);
24
+
25
+ const packagedWasmPath = join(options.outputDirectory, "assets", "app.wasm");
26
+ await mkdir(join(options.outputDirectory, "assets"), { recursive: true });
27
+ await rm(packagedWasmPath, { force: true });
28
+ await packageBrowserValidatedWasm({
29
+ sourceWasmPath: artifacts.wasmPath,
30
+ outputWasmPath: packagedWasmPath,
31
+ });
32
+ return artifacts;
33
+ }
34
+
35
+ interface PackageBrowserValidatedWasmOptions {
36
+ optimize?: (wasmPath: string) => Promise<void>;
37
+ sourceWasmPath: string;
38
+ outputWasmPath: string;
39
+ strip?: (wasmPath: string) => Promise<void>;
40
+ onWarning?: (message: string) => void;
41
+ }
42
+
43
+ export async function packageBrowserValidatedWasm(
44
+ options: PackageBrowserValidatedWasmOptions
45
+ ): Promise<void> {
46
+ const sourceBytes = await readFile(options.sourceWasmPath);
47
+ await writeFile(options.outputWasmPath, sourceBytes);
48
+
49
+ const optimize = options.optimize ?? optimizePackagedWasm;
50
+ try {
51
+ await optimize(options.outputWasmPath);
52
+ await validateBrowserWasm(options.outputWasmPath, "optimized wasm");
53
+ } catch (error) {
54
+ await writeFile(options.outputWasmPath, sourceBytes);
55
+ const message = error instanceof Error ? error.message : String(error);
56
+
57
+ try {
58
+ await validateBrowserWasm(options.outputWasmPath, "generated wasm");
59
+ } catch (rawError) {
60
+ const rawMessage = rawError instanceof Error ? rawError.message : String(rawError);
61
+ throw new Error([
62
+ rawMessage,
63
+ `wasm optimization step failed: ${message}`,
64
+ ].join("\n"));
65
+ }
66
+
67
+ const warning = [
68
+ `warning: keeping unoptimized wasm at ${options.outputWasmPath}`,
69
+ `wasm optimization step failed or did not produce browser-parseable output: ${message}`,
70
+ ].join("\n");
71
+ (options.onWarning ?? console.warn)(warning);
72
+ }
73
+
74
+ const strip = options.strip ?? stripPackagedWasm;
75
+
76
+ try {
77
+ await strip(options.outputWasmPath);
78
+ await validateBrowserWasm(options.outputWasmPath, "stripped wasm");
79
+ } catch (error) {
80
+ // Stripping is a size optimization only. Keep the known-good raw wasm
81
+ // whenever toolchain-specific objcopy output fails browser validation.
82
+ await writeFile(options.outputWasmPath, sourceBytes);
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ const warning = [
85
+ `warning: keeping unstripped wasm at ${options.outputWasmPath}`,
86
+ `strip step failed browser validation or tooling requirements: ${message}`,
87
+ ].join("\n");
88
+ (options.onWarning ?? console.warn)(warning);
89
+ }
90
+ }
91
+
92
+ async function validateBrowserWasm(
93
+ wasmPath: string,
94
+ description: string
95
+ ): Promise<void> {
96
+ const bytes = await readFile(wasmPath);
97
+ try {
98
+ // Validate against the same JS API the browser uses before we publish it.
99
+ await WebAssembly.compile(bytes);
100
+ } catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ throw new Error([
103
+ `${description} does not parse in browser WebAssembly (${wasmPath}): ${message}`,
104
+ formatWasmTypeDiagnostics(bytes),
105
+ ].join("\n"));
106
+ }
107
+ }
@@ -0,0 +1,22 @@
1
+ import { mkdir, rm } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { buildAppWasm, type BuildAppWasmOptions } from "./buildAppWasm.ts";
4
+ import { generateSceneManifest } from "./generateSceneManifest.ts";
5
+
6
+ export interface BuildSwiftTUIWebAppOptions extends BuildAppWasmOptions {
7
+ appExecutable?: string;
8
+ }
9
+
10
+ export async function buildSwiftTUIWebApp(
11
+ options: BuildSwiftTUIWebAppOptions
12
+ ): Promise<void> {
13
+ await rm(options.outputDirectory, { recursive: true, force: true });
14
+ await mkdir(options.outputDirectory, { recursive: true });
15
+ await generateSceneManifest({
16
+ packagePath: options.packagePath,
17
+ outputPath: join(options.outputDirectory, "scene-manifest.json"),
18
+ appExecutable: options.appExecutable ?? options.product,
19
+ swiftCommand: options.swiftCommand,
20
+ });
21
+ await buildAppWasm(options);
22
+ }
@@ -0,0 +1,46 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import {
4
+ loadWebHostSceneManifest,
5
+ webTUISceneManifestToJSON,
6
+ type WebHostSceneManifest,
7
+ } from "@swifttui/web/manifest";
8
+ import { runCommand } from "./runCommand.ts";
9
+ import { swiftCommandPrefix } from "./swiftCommandPrefix.ts";
10
+
11
+ export interface GenerateSceneManifestOptions {
12
+ packagePath: string;
13
+ outputPath: string;
14
+ appExecutable: string;
15
+ swiftCommand?: readonly string[];
16
+ }
17
+
18
+ export async function generateSceneManifest(
19
+ options: GenerateSceneManifestOptions
20
+ ): Promise<WebHostSceneManifest> {
21
+ const output = await runManifestCommand(options);
22
+ const manifest = await loadWebHostSceneManifest(output.trim());
23
+ await mkdir(dirname(options.outputPath), { recursive: true });
24
+ await writeFile(options.outputPath, webTUISceneManifestToJSON(manifest));
25
+ return manifest;
26
+ }
27
+
28
+ async function runManifestCommand(
29
+ options: GenerateSceneManifestOptions
30
+ ): Promise<string> {
31
+ return await runCommand(
32
+ [
33
+ ...(options.swiftCommand ?? swiftCommandPrefix()),
34
+ "run",
35
+ "--package-path",
36
+ options.packagePath,
37
+ options.appExecutable,
38
+ ],
39
+ {
40
+ env: {
41
+ ...process.env,
42
+ TUIGUI_MODE: "manifest",
43
+ },
44
+ }
45
+ );
46
+ }
@@ -0,0 +1,20 @@
1
+ import { findExecutable, runCommand } from "./runCommand.ts";
2
+
3
+ export async function optimizePackagedWasm(
4
+ wasmPath: string
5
+ ): Promise<void> {
6
+ const wasmOptPath = findExecutable("wasm-opt");
7
+ if (!wasmOptPath) {
8
+ throw new Error(
9
+ "missing wasm-opt in PATH; install Binaryen so wasm packaging is deterministic across environments"
10
+ );
11
+ }
12
+
13
+ await runCommand([
14
+ wasmOptPath,
15
+ "-Os",
16
+ wasmPath,
17
+ "-o",
18
+ wasmPath,
19
+ ]);
20
+ }
@@ -0,0 +1,90 @@
1
+ import { expect, test } from "bun:test";
2
+ import {
3
+ formatCommandForLogs,
4
+ hasRequiredWasmFlags,
5
+ requiredWasmSwiftFlags,
6
+ wasmBuildConfigurationLogLines,
7
+ } from "./resolveSwiftArtifacts.ts";
8
+
9
+ test("detects the required wasm Swift flag sequence", () => {
10
+ expect(
11
+ hasRequiredWasmFlags([
12
+ "build",
13
+ "--swift-sdk",
14
+ "swift-6.3.1-RELEASE_wasm",
15
+ "-c",
16
+ "release",
17
+ ...requiredWasmSwiftFlags,
18
+ "-Xlinker",
19
+ "--initial-memory=1",
20
+ ])
21
+ ).toBe(true);
22
+
23
+ expect(
24
+ hasRequiredWasmFlags([
25
+ "build",
26
+ "--swift-sdk",
27
+ "swift-6.3.1-RELEASE_wasm",
28
+ "-c",
29
+ "release",
30
+ "-Xswiftc",
31
+ "-Osize",
32
+ "-Xswiftc",
33
+ "-disable-llvm-merge-functions-pass",
34
+ ])
35
+ ).toBe(false);
36
+ });
37
+
38
+ test("formats commands for readable CI logs", () => {
39
+ expect(
40
+ formatCommandForLogs([
41
+ "swiftly",
42
+ "run",
43
+ "swift",
44
+ "build",
45
+ "--package-path",
46
+ "/tmp/My Project",
47
+ "-Xlinker",
48
+ "stack-size=1048576",
49
+ ])
50
+ ).toBe(
51
+ "swiftly run swift build --package-path '/tmp/My Project' -Xlinker stack-size=1048576"
52
+ );
53
+ });
54
+
55
+ test("emits explicit CI log lines for flag confirmation and commands", () => {
56
+ const lines = wasmBuildConfigurationLogLines({
57
+ configuration: "release",
58
+ packagePath: "/tmp/pkg",
59
+ product: "WebExampleApp",
60
+ swiftlyWorkingDirectory: "/tmp",
61
+ buildCommand: ["swiftly", "run", "swift", "build", "--product", "WebExampleApp"],
62
+ showBinPathCommand: ["swiftly", "run", "swift", "build", "--show-bin-path"],
63
+ });
64
+
65
+ expect(lines).toContain("WASM_REQUIRED_FLAGS_CONFIRMED=true");
66
+ expect(lines).toContain("WASM_BUILD_CONFIGURATION_NAME=release");
67
+ expect(lines).toContain(
68
+ "WASM_REQUIRED_FLAGS=-Xswiftc -Osize -Xswiftc -Xfrontend -Xswiftc -disable-llvm-merge-functions-pass"
69
+ );
70
+ expect(lines).toContain(
71
+ 'WASM_BUILD_COMMAND_ARGS_JSON=["swiftly","run","swift","build","--product","WebExampleApp"]'
72
+ );
73
+ expect(lines).toContain(
74
+ 'WASM_SHOW_BIN_PATH_COMMAND_ARGS_JSON=["swiftly","run","swift","build","--show-bin-path"]'
75
+ );
76
+ });
77
+
78
+ test("marks required release flags as skipped for debug wasm builds", () => {
79
+ const lines = wasmBuildConfigurationLogLines({
80
+ configuration: "debug",
81
+ packagePath: "/tmp/pkg",
82
+ product: "WebExampleApp",
83
+ swiftlyWorkingDirectory: "/tmp",
84
+ buildCommand: ["swiftly", "run", "swift", "build", "-c", "debug"],
85
+ showBinPathCommand: ["swiftly", "run", "swift", "build", "-c", "debug", "--show-bin-path"],
86
+ });
87
+
88
+ expect(lines).toContain("WASM_BUILD_CONFIGURATION_NAME=debug");
89
+ expect(lines).toContain("WASM_REQUIRED_FLAGS_CONFIRMED=skipped");
90
+ });