@swf2es/cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +202 -0
  2. package/LICENSE.third-party +389 -0
  3. package/README.md +32 -2
  4. package/dist/aot.d.ts +98 -0
  5. package/dist/aot.d.ts.map +1 -0
  6. package/dist/aot.js +145 -0
  7. package/dist/aot.js.map +1 -0
  8. package/dist/args.d.ts +31 -0
  9. package/dist/args.d.ts.map +1 -0
  10. package/dist/args.js +37 -0
  11. package/dist/args.js.map +1 -0
  12. package/dist/commands/compile.d.ts +3 -0
  13. package/dist/commands/compile.d.ts.map +1 -0
  14. package/dist/commands/compile.js +233 -0
  15. package/dist/commands/compile.js.map +1 -0
  16. package/dist/commands/help.d.ts +6 -0
  17. package/dist/commands/help.d.ts.map +1 -0
  18. package/dist/commands/help.js +52 -0
  19. package/dist/commands/help.js.map +1 -0
  20. package/dist/commands/run.d.ts +3 -0
  21. package/dist/commands/run.d.ts.map +1 -0
  22. package/dist/commands/run.js +122 -0
  23. package/dist/commands/run.js.map +1 -0
  24. package/dist/files.d.ts +10 -0
  25. package/dist/files.d.ts.map +1 -0
  26. package/dist/files.js +35 -0
  27. package/dist/files.js.map +1 -0
  28. package/dist/index.d.ts +5 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +5 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/main.d.ts +3 -0
  33. package/dist/main.d.ts.map +1 -0
  34. package/dist/main.js +45 -0
  35. package/dist/main.js.map +1 -0
  36. package/dist/output.d.ts +7 -0
  37. package/dist/output.d.ts.map +1 -0
  38. package/dist/output.js +20 -0
  39. package/dist/output.js.map +1 -0
  40. package/dist/shell-memories.d.ts +2 -0
  41. package/dist/shell-memories.d.ts.map +1 -0
  42. package/dist/shell-memories.js +32 -0
  43. package/dist/shell-memories.js.map +1 -0
  44. package/dist/shell-thread.d.ts +2 -0
  45. package/dist/shell-thread.d.ts.map +1 -0
  46. package/dist/shell-thread.js +49 -0
  47. package/dist/shell-thread.js.map +1 -0
  48. package/dist/shell-toplevel-library.d.ts +2 -0
  49. package/dist/shell-toplevel-library.d.ts.map +1 -0
  50. package/dist/shell-toplevel-library.js +22 -0
  51. package/dist/shell-toplevel-library.js.map +1 -0
  52. package/dist/shell.d.ts +62 -0
  53. package/dist/shell.d.ts.map +1 -0
  54. package/dist/shell.js +135 -0
  55. package/dist/shell.js.map +1 -0
  56. package/dist/threads.d.ts +85 -0
  57. package/dist/threads.d.ts.map +1 -0
  58. package/dist/threads.js +145 -0
  59. package/dist/threads.js.map +1 -0
  60. package/package.json +46 -3
  61. package/src/aot.ts +239 -0
  62. package/src/args.ts +53 -0
  63. package/src/commands/compile.ts +286 -0
  64. package/src/commands/help.ts +60 -0
  65. package/src/commands/run.ts +131 -0
  66. package/src/files.ts +38 -0
  67. package/src/index.ts +25 -0
  68. package/src/main.ts +51 -0
  69. package/src/output.ts +23 -0
  70. package/src/shell-memories.ts +35 -0
  71. package/src/shell-thread.ts +54 -0
  72. package/src/shell-toplevel-library.ts +28 -0
  73. package/src/shell.ts +211 -0
  74. package/src/threads.ts +223 -0
package/src/aot.ts ADDED
@@ -0,0 +1,239 @@
1
+ // Ahead-of-time compilation: a SWF's ABCs compiled as the player compiles
2
+ // them for a SWF it loads as its main movie, so that each module is byte
3
+ // for byte what the browser would compile (the JIT/AOT invariant in
4
+ // docs/architecture/overview.md). The steps are Code's in packages/player, in the
5
+ // same order: the libraries added to the root domain, each compiled as it
6
+ // is added, then the SWF's DoABCs all added to a child of the root before
7
+ // any of them compiles. An AVM1 SWF's action blocks compile to one module
8
+ // of their own (compileActionsAhead), which needs no library.
9
+ import { createHash } from "node:crypto";
10
+ import {
11
+ AIR_API_VERSION,
12
+ API_VERSION,
13
+ actionsKey,
14
+ type Codegen,
15
+ type LibraryDigest,
16
+ libraryLog,
17
+ moduleKey,
18
+ } from "@swf2es/codegen";
19
+ import { type ActionList, isAs3, readActions, readDoAbc, readSwf, tags } from "@swf2es/format";
20
+ import { avm2 } from "@swf2es/runtime";
21
+
22
+ export { AIR_API_VERSION, API_VERSION };
23
+
24
+ export interface AbcInput {
25
+ /** DoABC2's name, "" for a DoABC or a bare ABC. */
26
+ name: string;
27
+ /** DoABC2's lazy flag: its scripts run on first use, not when it loads. */
28
+ lazy: boolean;
29
+ abc: Uint8Array;
30
+ }
31
+
32
+ export interface Input {
33
+ kind: "swf" | "abc";
34
+ /** The SWF's version, 0 for a bare ABC. */
35
+ version: number;
36
+ abcs: AbcInput[];
37
+ /** An AVM1 SWF's action blocks; null for an AS3 SWF or an ABC. */
38
+ avm1: Avm1Input | null;
39
+ }
40
+
41
+ export interface Avm1Input {
42
+ /** The SWF's decompressed bytes, which the blocks' ranges are of. */
43
+ bytes: Uint8Array;
44
+ actions: ActionList;
45
+ }
46
+
47
+ /** An AVM1 SWF's module. */
48
+ export interface ActionsModule {
49
+ /** The ES module's source. */
50
+ module: string;
51
+ /** SHA-256 of the SWF's decompressed bytes, as the key names it. */
52
+ hash: string;
53
+ /**
54
+ * The SHA-256 of its key, codegen's actionsKey, as the player's module
55
+ * cache is to look it up; null for a compiler without an identity.
56
+ */
57
+ key: string | null;
58
+ /** How many blocks it has an entry for. */
59
+ blocks: number;
60
+ }
61
+
62
+ export interface Module {
63
+ name: string;
64
+ /** SHA-256 of the ABC, as the player and the cache key name it. */
65
+ hash: string;
66
+ lazy: boolean;
67
+ /** The ES module's source. */
68
+ module: string;
69
+ /**
70
+ * What compiling it fixed in the compiler's domain, which the player
71
+ * replays in its place (Codegen.compileModuleLogged); null where it
72
+ * cannot be.
73
+ */
74
+ log: string | null;
75
+ /**
76
+ * The SHA-256 of its key, as the player's module cache looks it up when
77
+ * it loads the SWF as its main movie (codegen's moduleKey); null for a
78
+ * compiler without an identity, or a module without a log.
79
+ */
80
+ key: string | null;
81
+ }
82
+
83
+ const SIGNATURES = new Set(["FWS", "CWS", "ZWS"]);
84
+
85
+ /** Input the command cannot compile, with why. */
86
+ export class InputError extends Error {}
87
+
88
+ /**
89
+ * The ABCs of a SWF, in tag order as the player links them, or a bare ABC
90
+ * as one; for an AVM1 SWF its action blocks instead, as the player runs
91
+ * its code as AVM1 and never compiles its DoABCs.
92
+ */
93
+ export function readInput(bytes: Uint8Array): Input {
94
+ const signature = String.fromCharCode(...bytes.subarray(0, 3));
95
+ if (!SIGNATURES.has(signature)) {
96
+ // ABC headers name a minor and major version: 46 for Flash, 47 for AIR float.
97
+ if (bytes.length < 4 || (bytes[2] !== 46 && bytes[2] !== 47) || bytes[3] !== 0) {
98
+ throw new InputError("neither a SWF nor an ABC");
99
+ }
100
+
101
+ return { kind: "abc", version: 0, abcs: [{ name: "", lazy: false, abc: bytes }], avm1: null };
102
+ }
103
+
104
+ let swf: ReturnType<typeof readSwf>;
105
+ try {
106
+ swf = readSwf(bytes);
107
+ } catch (e) {
108
+ throw new InputError(`a truncated or corrupt SWF (${(e as Error).message})`);
109
+ }
110
+
111
+ const version = swf.header.version;
112
+ if (!isAs3(swf)) {
113
+ return {
114
+ kind: "swf",
115
+ version,
116
+ abcs: [],
117
+ avm1: { bytes: swf.bytes, actions: readActions(swf) },
118
+ };
119
+ }
120
+
121
+ // A DoABC cut short would reach codegen as a malformed ABC and read as
122
+ // the code's error; a SWF cut elsewhere the player plays, as far as it goes.
123
+ const last = swf.tags.at(-1);
124
+ if (swf.truncated && (last?.code === tags.DoABC || last?.code === tags.DoABC2)) {
125
+ throw new InputError("a truncated SWF: the file ends inside a DoABC tag");
126
+ }
127
+
128
+ const abcs: AbcInput[] = [];
129
+ for (const t of swf.tags) {
130
+ if (t.code === tags.DoABC || t.code === tags.DoABC2) {
131
+ abcs.push(readDoAbc(swf.bytes, t));
132
+ }
133
+ }
134
+
135
+ return { kind: "swf", version, abcs, avm1: null };
136
+ }
137
+
138
+ export function sha256(bytes: Uint8Array): string {
139
+ return createHash("sha256").update(bytes).digest("hex");
140
+ }
141
+
142
+ export class RejectedError extends Error {
143
+ constructor(
144
+ readonly what: string,
145
+ readonly verifyError: number,
146
+ /** A library's, not the input's. */
147
+ readonly library: boolean,
148
+ ) {
149
+ super(`${what} was rejected: VerifyError #${verifyError}`);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * The libraries' modules, those asked for by `keepLibraries`, and the
155
+ * ABCs' modules, in order, compiled at `apiVersion`: Flash Player's, or
156
+ * AIR_API_VERSION for a host that plays them as AIR does (the player's
157
+ * `air`), whose keys are its alone. The libraries compile whether kept or
158
+ * not: the player compiles each, and what a compile resolves of an ABC is
159
+ * what later ABCs compile against.
160
+ */
161
+ export function compileAhead(
162
+ codegen: Codegen,
163
+ libraries: { name: string; abc: Uint8Array }[],
164
+ abcs: AbcInput[],
165
+ keepLibraries = false,
166
+ apiVersion = API_VERSION,
167
+ ): { libraries: Module[]; libraryInputs: { name: string; abc: Uint8Array }[]; abcs: Module[] } {
168
+ codegen.reset(apiVersion);
169
+ if (
170
+ abcs.some(({ abc }) => {
171
+ const version = codegen.abcVersion(abc);
172
+ return version?.major === 47 && version.minor >= 16;
173
+ }) &&
174
+ !avm2.hasFloatLibrary(libraries.map((lib) => lib.abc))
175
+ ) {
176
+ libraries = [...libraries, { name: "float", abc: avm2.floatLibrary }];
177
+ }
178
+
179
+ const hashes: string[] = [];
180
+ const builtins: boolean[] = [];
181
+ const add = (what: string, abc: Uint8Array, builtin: boolean, domain: number) => {
182
+ const error = codegen.add(abc, builtin, domain);
183
+ if (error) {
184
+ throw new RejectedError(what, error, builtin);
185
+ }
186
+
187
+ hashes.push(sha256(abc));
188
+ builtins.push(builtin);
189
+ return hashes.length - 1;
190
+ };
191
+ // Keyed before it compiles, as the player keys it before it asks its cache.
192
+ const compile = (index: number, libraryDigest: LibraryDigest | null) => {
193
+ const keyText =
194
+ codegen.identity === null
195
+ ? null
196
+ : moduleKey(codegen, index, { hashes, builtins }, libraryDigest);
197
+ const { module, log } = codegen.compileModuleLogged(hashes, index);
198
+ const key = keyText === null || log === null ? null : sha256Text(keyText);
199
+ return { hash: hashes[index], module, log, key };
200
+ };
201
+
202
+ const libraryModules: Module[] = [];
203
+ for (const { name, abc } of libraries) {
204
+ const index = add(`library ${name}`, abc, true, 0);
205
+ const compiled = compile(index, null);
206
+ if (keepLibraries) {
207
+ libraryModules.push({ name, lazy: false, ...compiled });
208
+ }
209
+ }
210
+
211
+ // The main movie's application domain, a child of the root's, as the player's mainDomain.
212
+ const domain = codegen.childDomain(0);
213
+ const indices = abcs.map(({ name, abc }, i) =>
214
+ add(`ABC ${i}${name ? ` (${name})` : ""}`, abc, false, domain),
215
+ );
216
+ // The libraries' part of the log, by its digest, as the player's keys name it once a SWF is added.
217
+ const count = libraries.length;
218
+ const digest: LibraryDigest | null =
219
+ indices.length > 0
220
+ ? { count, digest: sha256Text(libraryLog(codegen, indices[0], count)) }
221
+ : null;
222
+ const modules = abcs.map(({ name, lazy }, i) => ({ name, lazy, ...compile(indices[i], digest) }));
223
+
224
+ return { libraries: libraryModules, libraryInputs: libraries, abcs: modules };
225
+ }
226
+
227
+ /** An AVM1 SWF's action blocks' module, as the player is to compile it, and its key. */
228
+ export function compileActionsAhead(codegen: Codegen, input: Avm1Input): ActionsModule {
229
+ const { bytes, actions } = input;
230
+ const hash = sha256(bytes);
231
+ const key =
232
+ codegen.identity === null ? null : sha256Text(actionsKey(codegen, hash, actions.version));
233
+ const module = codegen.compileActionsModule(bytes, actions);
234
+ return { module, hash, key, blocks: actions.blocks.length };
235
+ }
236
+
237
+ function sha256Text(text: string): string {
238
+ return createHash("sha256").update(text, "utf8").digest("hex");
239
+ }
package/src/args.ts ADDED
@@ -0,0 +1,53 @@
1
+ // What every command shares of its command line: the options all take,
2
+ // and a mistake in it reported as a usage error.
3
+ import { COMPILER_VERSION } from "@swf2es/codegen";
4
+ import { usageError } from "./output.js";
5
+
6
+ export interface Command {
7
+ name: string;
8
+ /** One line for the list of commands. */
9
+ summary: string;
10
+ usage: string;
11
+ main(argv: string[]): Promise<void>;
12
+ }
13
+
14
+ /** The options every command takes, which parseArgs' options spread. */
15
+ export const COMMON_OPTIONS = {
16
+ help: { type: "boolean", short: "h" },
17
+ version: { type: "boolean", short: "v" },
18
+ } as const;
19
+
20
+ export const COMMON_USAGE = ` -h, --help show this
21
+ -v, --version print the compiler's version`;
22
+
23
+ export function printVersion(): void {
24
+ console.log(`swf2es ${COMPILER_VERSION}`);
25
+ }
26
+
27
+ /**
28
+ * `command`'s arguments as `parse` reads them, a mistake in them a usage
29
+ * error; null once -h or -v has been answered, and the command is done.
30
+ */
31
+ export function parse<T extends { values: { help?: boolean; version?: boolean } }>(
32
+ command: Command,
33
+ read: () => T,
34
+ ): T | null {
35
+ let parsed: T;
36
+ try {
37
+ parsed = read();
38
+ } catch (e) {
39
+ usageError((e as Error).message, command.name);
40
+ }
41
+
42
+ if (parsed.values.help) {
43
+ console.log(command.usage);
44
+ return null;
45
+ }
46
+
47
+ if (parsed.values.version) {
48
+ printVersion();
49
+ return null;
50
+ }
51
+
52
+ return parsed;
53
+ }
@@ -0,0 +1,286 @@
1
+ // swf2es compile: a SWF's or an ABC's modules, ahead of time, as the player
2
+ // compiles them for its main movie, with a manifest; an AVM1 SWF's one
3
+ // module of its action blocks.
4
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { basename, extname, join } from "node:path";
6
+ import { parseArgs } from "node:util";
7
+ import { COMPILER_VERSION, createCodegen } from "@swf2es/codegen";
8
+ import { airLibrary, builtinLibrary, playerglobalLibrary } from "@swf2es/player/libraries";
9
+ import {
10
+ AIR_API_VERSION,
11
+ API_VERSION,
12
+ type Avm1Input,
13
+ compileActionsAhead,
14
+ compileAhead,
15
+ InputError,
16
+ type Module,
17
+ RejectedError,
18
+ readInput,
19
+ sha256,
20
+ } from "../aot.js";
21
+ import { COMMON_OPTIONS, COMMON_USAGE, type Command, parse } from "../args.js";
22
+ import { codegenModule, read, readLibraries, writing } from "../files.js";
23
+ import { fail, usageError } from "../output.js";
24
+
25
+ const USAGE = `usage: swf2es compile <file.swf|file.abc> [options]
26
+ swf2es <file.swf|file.abc> [options]
27
+
28
+ Compiles the ABCs of a SWF (its DoABC and DoABC2 tags, in order), or a bare
29
+ ABC, to ES modules as the player compiles them for a SWF it loads as its
30
+ main movie, byte for byte. Writes a .js file per ABC, a .log file of what
31
+ its compile fixed, which the player replays in its place, and a
32
+ manifest.json naming them, their ABCs' hashes, the keys the player's
33
+ module cache looks them up by, and the compiler. An AVM1 SWF (one whose
34
+ FileAttributes has no ActionScript 3 flag) compiles to one module of its
35
+ action blocks, avm1.js, which takes no library.
36
+
37
+ options:
38
+ -o, --out <dir> where to write (default: <file name>.swf2es)
39
+ --lib <file.abc> a library the code links against, loaded into the
40
+ root domain in the order given; repeat for each.
41
+ Default: what the player loads, which swf2es ships:
42
+ avmplus' builtin.abc and the player's playerglobal,
43
+ and with --air AIR's library after them
44
+ --emit-libraries write the libraries' modules too
45
+ --air compile for a player that plays the SWF as AIR does
46
+ (its \`air\` option), AIR's API defined; a player
47
+ in the other mode looks none of the modules up.
48
+ Refused for an AVM1 SWF, which has no API versions
49
+ -q, --quiet print nothing but errors
50
+ ${COMMON_USAGE}
51
+
52
+ exit status: 0 on success, 2 for a mistake in the command line, 1 for
53
+ anything else (an unreadable or rejected input or library, or an output
54
+ that cannot be written)`;
55
+
56
+ const OPTIONS = {
57
+ out: { type: "string", short: "o" },
58
+ lib: { type: "string", multiple: true },
59
+ "emit-libraries": { type: "boolean" },
60
+ air: { type: "boolean" },
61
+ quiet: { type: "boolean", short: "q" },
62
+ ...COMMON_OPTIONS,
63
+ } as const;
64
+
65
+ /**
66
+ * manifest.json's format: 2 added each module's log, key and lengths, and
67
+ * named the compiler by its stamped identity rather than its file's hash.
68
+ * An AVM1 SWF's `avm1` entry, which a reader of the ABCs' entries need not
69
+ * know, came later without a new version.
70
+ */
71
+ const MANIFEST_VERSION = 2;
72
+
73
+ /** What a previous run wrote to `out`, as its manifest names it, so that no stale module stays beside a new manifest. */
74
+ async function removeModules(out: string): Promise<void> {
75
+ let old: { libraries?: unknown; abcs?: unknown; avm1?: unknown } | null;
76
+ try {
77
+ old = JSON.parse(await readFile(join(out, "manifest.json"), "utf8"));
78
+ } catch {
79
+ return;
80
+ }
81
+
82
+ const entries = [old?.libraries, old?.abcs, [old?.avm1]].flatMap((list) =>
83
+ Array.isArray(list) ? list : [],
84
+ );
85
+ // Only names this command writes: a manifest edited by hand names nothing else to delete.
86
+ for (const entry of entries) {
87
+ const name = (entry as { module?: unknown } | null)?.module;
88
+ if (typeof name === "string" && /^((abc|lib)-\d+|avm1)\.js$/.test(name)) {
89
+ for (const file of [name, name.replace(/\.js$/, ".log")]) {
90
+ await writing(join(out, file), () => rm(join(out, file), { force: true }));
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * The libraries the player loads, as @swf2es/web gives them, AIR's after
98
+ * the rest for an AIR player; the manifest's names tell swf2es's
99
+ * playerglobal from Flash Player's.
100
+ */
101
+ function playerLibraries(air = false): { name: string; abc: Uint8Array }[] {
102
+ const libraries = [
103
+ { name: "builtin", abc: builtinLibrary },
104
+ { name: "playerglobal-swf2es", abc: playerglobalLibrary },
105
+ ];
106
+ return air ? [...libraries, { name: "air", abc: airLibrary }] : libraries;
107
+ }
108
+
109
+ export const compile: Command = {
110
+ name: "compile",
111
+ summary: "compile a SWF's or an ABC's modules ahead of time, with a manifest",
112
+ usage: USAGE,
113
+ async main(argv) {
114
+ const parsed = parse(compile, () =>
115
+ parseArgs({ args: argv, allowPositionals: true, options: OPTIONS }),
116
+ );
117
+ if (!parsed) {
118
+ return;
119
+ }
120
+
121
+ const { values, positionals } = parsed;
122
+ if (positionals.length !== 1) {
123
+ usageError(`expected one input file, got ${positionals.length}`, "compile");
124
+ }
125
+
126
+ const file = positionals[0];
127
+ const bytes = await read(file, "input");
128
+ let input: ReturnType<typeof readInput>;
129
+ try {
130
+ input = readInput(bytes);
131
+ } catch (e) {
132
+ fail(e instanceof InputError ? `${file}: ${e.message}` : String((e as Error).stack ?? e));
133
+ }
134
+
135
+ const out = values.out ?? `${basename(file, extname(file))}.swf2es`;
136
+ if (input.avm1) {
137
+ // AVM1 has no API versions: its module is the same for every player.
138
+ if (values.air) {
139
+ usageError(`${file} is an AVM1 SWF, which --air does not apply to`, "compile");
140
+ }
141
+
142
+ await compileAvm1(file, bytes, input.avm1, out, values.quiet ?? false);
143
+ return;
144
+ }
145
+
146
+ const libs = values.lib ? await readLibraries(values.lib) : playerLibraries(values.air);
147
+ if (input.abcs.length === 0) {
148
+ fail(`${file}: no DoABC or DoABC2 tag, so no ActionScript 3 to compile`);
149
+ }
150
+
151
+ const codegen = await createCodegen(await codegenModule());
152
+ const started = performance.now();
153
+ let compiled: ReturnType<typeof compileAhead>;
154
+ try {
155
+ compiled = compileAhead(
156
+ codegen,
157
+ libs,
158
+ input.abcs,
159
+ values["emit-libraries"],
160
+ values.air ? AIR_API_VERSION : API_VERSION,
161
+ );
162
+ } catch (e) {
163
+ if (!(e instanceof RejectedError)) {
164
+ fail(String((e as Error).stack ?? e));
165
+ }
166
+
167
+ fail(e.library ? e.message : `${file}: ${e.message}`);
168
+ }
169
+
170
+ const ms = performance.now() - started;
171
+ await writing(out, () => mkdir(out, { recursive: true }));
172
+ await removeModules(out);
173
+ let size = 0;
174
+ const write = async (file: string, text: string) => {
175
+ await writing(join(out, file), () => writeFile(join(out, file), text));
176
+ size += Buffer.byteLength(text);
177
+ return file;
178
+ };
179
+ // A module, its log beside it, and what a host's precompiled module cache
180
+ // looks it up by and checks it with: the key and the two's lengths.
181
+ const entry = async (name: string, m: Module) => {
182
+ const module = await write(`${name}.js`, m.module);
183
+ if (m.log === null || m.key === null) {
184
+ return { module };
185
+ }
186
+
187
+ const log = await write(`${name}.log`, m.log);
188
+ return { module, log, key: m.key, lengths: [m.module.length, m.log.length] };
189
+ };
190
+
191
+ // The compiler is named by the identity stamped into its binary, which
192
+ // the keys name too, so that a host can tell what another build wrote;
193
+ // an ABC by its hash, as its module names it, with the ABCs it was
194
+ // linked against.
195
+ const manifest = {
196
+ manifestVersion: MANIFEST_VERSION,
197
+ compiler: { name: "swf2es", version: COMPILER_VERSION, identity: codegen.identity },
198
+ // The version the modules were compiled at, which their keys name too.
199
+ apiVersion: codegen.apiVersion,
200
+ input: {
201
+ file: basename(file),
202
+ kind: input.kind,
203
+ version: input.version,
204
+ sha256: sha256(bytes),
205
+ },
206
+ libraries: await Promise.all(
207
+ compiled.libraryInputs.map(async ({ name, abc }, i) => {
208
+ const m = compiled.libraries[i];
209
+ const lib = { name, sha256: sha256(abc) };
210
+ return m ? { ...lib, ...(await entry(`lib-${i}`, m)) } : lib;
211
+ }),
212
+ ),
213
+ abcs: await Promise.all(
214
+ compiled.abcs.map(async (m, i) => ({
215
+ name: m.name,
216
+ sha256: m.hash,
217
+ lazy: m.lazy,
218
+ ...(await entry(`abc-${i}`, m)),
219
+ })),
220
+ ),
221
+ };
222
+ const manifestPath = join(out, "manifest.json");
223
+ await writing(manifestPath, () =>
224
+ writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`),
225
+ );
226
+ if (!values.quiet) {
227
+ const count = compiled.abcs.length + compiled.libraries.length;
228
+ console.log(
229
+ `${file}: ${count} module${count === 1 ? "" : "s"}, ${size} bytes, ` +
230
+ `compiled in ${Math.round(ms)} ms, in ${out}`,
231
+ );
232
+ }
233
+ },
234
+ };
235
+
236
+ /**
237
+ * An AVM1 SWF's module, avm1.js, and its manifest: the module's key, as
238
+ * the player's module cache is to look it up, the SHA-256 of the SWF's
239
+ * decompressed bytes it names, its length and how many blocks it has.
240
+ */
241
+ async function compileAvm1(
242
+ file: string,
243
+ bytes: Uint8Array,
244
+ avm1: Avm1Input,
245
+ out: string,
246
+ quiet: boolean,
247
+ ): Promise<void> {
248
+ const codegen = await createCodegen(await codegenModule());
249
+ const started = performance.now();
250
+ const compiled = compileActionsAhead(codegen, avm1);
251
+ const ms = performance.now() - started;
252
+ await writing(out, () => mkdir(out, { recursive: true }));
253
+ await removeModules(out);
254
+ const module = "avm1.js";
255
+ await writing(join(out, module), () => writeFile(join(out, module), compiled.module));
256
+ const manifest = {
257
+ manifestVersion: MANIFEST_VERSION,
258
+ compiler: { name: "swf2es", version: COMPILER_VERSION, identity: codegen.identity },
259
+ apiVersion: API_VERSION,
260
+ input: {
261
+ file: basename(file),
262
+ kind: "swf",
263
+ version: avm1.actions.version,
264
+ sha256: sha256(bytes),
265
+ },
266
+ libraries: [],
267
+ abcs: [],
268
+ avm1: {
269
+ module,
270
+ sha256: compiled.hash,
271
+ blocks: compiled.blocks,
272
+ ...(compiled.key === null ? {} : { key: compiled.key, length: compiled.module.length }),
273
+ },
274
+ };
275
+ const manifestPath = join(out, "manifest.json");
276
+ await writing(manifestPath, () =>
277
+ writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`),
278
+ );
279
+ if (!quiet) {
280
+ const size = Buffer.byteLength(compiled.module);
281
+ const blocks = `${compiled.blocks} AVM1 block${compiled.blocks === 1 ? "" : "s"}`;
282
+ console.log(
283
+ `${file}: ${blocks}, 1 module, ${size} bytes, compiled in ${Math.round(ms)} ms, in ${out}`,
284
+ );
285
+ }
286
+ }
@@ -0,0 +1,60 @@
1
+ // swf2es help: the commands, or one command's usage.
2
+ import { parseArgs } from "node:util";
3
+ import { COMMON_OPTIONS, COMMON_USAGE, type Command, parse } from "../args.js";
4
+ import { usageError } from "../output.js";
5
+ import { compile } from "./compile.js";
6
+ import { run } from "./run.js";
7
+
8
+ export const help: Command = {
9
+ name: "help",
10
+ summary: "show the commands, or a command's usage",
11
+ usage: `usage: swf2es help [command]
12
+
13
+ Shows the commands, or how to run one.
14
+
15
+ options:
16
+ ${COMMON_USAGE}`,
17
+ async main(argv) {
18
+ const parsed = parse(help, () =>
19
+ parseArgs({ args: argv, allowPositionals: true, options: COMMON_OPTIONS }),
20
+ );
21
+ if (!parsed) {
22
+ return;
23
+ }
24
+
25
+ const { positionals } = parsed;
26
+ if (positionals.length > 1) {
27
+ usageError(`expected one command at most, got ${positionals.length}`);
28
+ }
29
+
30
+ if (positionals.length === 0) {
31
+ console.log(usage());
32
+ return;
33
+ }
34
+
35
+ const command = commands.get(positionals[0]);
36
+ if (!command) {
37
+ usageError(`no command ${positionals[0]}`);
38
+ }
39
+
40
+ console.log(command.usage);
41
+ },
42
+ };
43
+
44
+ export const commands = new Map<string, Command>([compile, run, help].map((c) => [c.name, c]));
45
+
46
+ /** The command's own usage: its commands, and the bare form compile keeps. */
47
+ export function usage(): string {
48
+ const width = Math.max(...[...commands.keys()].map((n) => n.length));
49
+ const list = [...commands.values()].map((c) => ` ${c.name.padEnd(width)} ${c.summary}`);
50
+ return `usage: swf2es <command> [options]
51
+ swf2es <file.swf|file.abc> [options] (as swf2es compile)
52
+
53
+ commands:
54
+ ${list.join("\n")}
55
+
56
+ options:
57
+ ${COMMON_USAGE}
58
+
59
+ Run swf2es help <command> for a command's options.`;
60
+ }