@gtkx/cli 2.0.0-beta.1 → 2.0.0-beta.3
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/builder.d.ts +1 -0
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +17 -5
- package/dist/builder.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -1
- package/dist/cli.js.map +1 -1
- package/dist/codegen/reference.d.ts.map +1 -1
- package/dist/codegen/reference.js +1 -0
- package/dist/codegen/reference.js.map +1 -1
- package/dist/codegen/run-codegen.d.ts +4 -2
- package/dist/codegen/run-codegen.d.ts.map +1 -1
- package/dist/codegen/run-codegen.js +15 -16
- package/dist/codegen/run-codegen.js.map +1 -1
- package/dist/codegen/store-resolver.d.ts +2 -1
- package/dist/codegen/store-resolver.d.ts.map +1 -1
- package/dist/codegen/store-resolver.js +4 -3
- package/dist/codegen/store-resolver.js.map +1 -1
- package/dist/commands/build.d.ts +5 -0
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +83 -10
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/codegen.d.ts +1 -0
- package/dist/commands/codegen.d.ts.map +1 -1
- package/dist/commands/codegen.js +6 -5
- package/dist/commands/codegen.js.map +1 -1
- package/dist/commands/deploy.d.ts +1 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +4 -2
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +9 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +29 -4
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +1 -0
- package/dist/commands/docs.js.map +1 -1
- package/dist/deploy/freedesktop/metainfo.d.ts.map +1 -1
- package/dist/deploy/freedesktop/metainfo.js +41 -2
- package/dist/deploy/freedesktop/metainfo.js.map +1 -1
- package/dist/deploy/freedesktop/validate.d.ts.map +1 -1
- package/dist/deploy/freedesktop/validate.js +10 -4
- package/dist/deploy/freedesktop/validate.js.map +1 -1
- package/dist/deploy/nfpm/config.d.ts +3 -2
- package/dist/deploy/nfpm/config.d.ts.map +1 -1
- package/dist/deploy/nfpm/config.js +7 -4
- package/dist/deploy/nfpm/config.js.map +1 -1
- package/dist/deploy/nfpm/contents.d.ts.map +1 -1
- package/dist/deploy/nfpm/contents.js +8 -0
- package/dist/deploy/nfpm/contents.js.map +1 -1
- package/dist/deploy/node-runtime/elf.d.ts +2 -1
- package/dist/deploy/node-runtime/elf.d.ts.map +1 -1
- package/dist/deploy/node-runtime/elf.js +32 -5
- package/dist/deploy/node-runtime/elf.js.map +1 -1
- package/dist/deploy/node-runtime/index.d.ts +2 -1
- package/dist/deploy/node-runtime/index.d.ts.map +1 -1
- package/dist/deploy/node-runtime/index.js +75 -5
- package/dist/deploy/node-runtime/index.js.map +1 -1
- package/dist/deploy/notices/node-runtime.js +2 -2
- package/dist/deploy/notices/node-runtime.js.map +1 -1
- package/dist/deploy/payload/launcher.d.ts.map +1 -1
- package/dist/deploy/payload/launcher.js +14 -1
- package/dist/deploy/payload/launcher.js.map +1 -1
- package/dist/deploy/payload/stage.js +1 -1
- package/dist/deploy/payload/stage.js.map +1 -1
- package/dist/deploy/run-deploy.d.ts +1 -0
- package/dist/deploy/run-deploy.d.ts.map +1 -1
- package/dist/deploy/run-deploy.js +100 -14
- package/dist/deploy/run-deploy.js.map +1 -1
- package/dist/deploy/settings/build-manifest.d.ts +6 -1
- package/dist/deploy/settings/build-manifest.d.ts.map +1 -1
- package/dist/deploy/settings/build-manifest.js +15 -4
- package/dist/deploy/settings/build-manifest.js.map +1 -1
- package/dist/deploy/settings/extra-files.d.ts +1 -1
- package/dist/deploy/settings/extra-files.d.ts.map +1 -1
- package/dist/deploy/settings/extra-files.js +22 -1
- package/dist/deploy/settings/extra-files.js.map +1 -1
- package/dist/deploy/settings/index.d.ts +1 -0
- package/dist/deploy/settings/index.d.ts.map +1 -1
- package/dist/deploy/settings/index.js +2 -1
- package/dist/deploy/settings/index.js.map +1 -1
- package/dist/deploy/settings/paths.d.ts +2 -1
- package/dist/deploy/settings/paths.d.ts.map +1 -1
- package/dist/deploy/settings/paths.js +113 -5
- package/dist/deploy/settings/paths.js.map +1 -1
- package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
- package/dist/deploy/targets/flatpak-source.js +11 -2
- package/dist/deploy/targets/flatpak-source.js.map +1 -1
- package/dist/deploy/targets/nfpm-target.d.ts.map +1 -1
- package/dist/deploy/targets/nfpm-target.js +12 -7
- package/dist/deploy/targets/nfpm-target.js.map +1 -1
- package/dist/deploy/types.d.ts +1 -0
- package/dist/deploy/types.d.ts.map +1 -1
- package/dist/deploy/types.js.map +1 -1
- package/dist/dev/entry-env.d.ts +2 -1
- package/dist/dev/entry-env.d.ts.map +1 -1
- package/dist/dev/entry-env.js +2 -1
- package/dist/dev/entry-env.js.map +1 -1
- package/dist/dev/headless.d.ts +3 -0
- package/dist/dev/headless.d.ts.map +1 -0
- package/dist/dev/headless.js +35 -0
- package/dist/dev/headless.js.map +1 -0
- package/dist/dev/runner-deps.d.ts +1 -1
- package/dist/dev/runner-deps.d.ts.map +1 -1
- package/dist/dev/runner-deps.js +3 -3
- package/dist/dev/runner-deps.js.map +1 -1
- package/dist/dev/runner-main.d.ts.map +1 -1
- package/dist/dev/runner-main.js +8 -3
- package/dist/dev/runner-main.js.map +1 -1
- package/dist/dev/supervisor.d.ts +1 -0
- package/dist/dev/supervisor.d.ts.map +1 -1
- package/dist/dev/supervisor.js +4 -3
- package/dist/dev/supervisor.js.map +1 -1
- package/dist/i18n/catalogs.d.ts.map +1 -1
- package/dist/i18n/catalogs.js +1 -0
- package/dist/i18n/catalogs.js.map +1 -1
- package/dist/i18n/source-messages.d.ts.map +1 -1
- package/dist/i18n/source-messages.js +960 -8
- package/dist/i18n/source-messages.js.map +1 -1
- package/dist/internal/build-manifest.d.ts +5 -2
- package/dist/internal/build-manifest.d.ts.map +1 -1
- package/dist/internal/build-manifest.js +15 -2
- package/dist/internal/build-manifest.js.map +1 -1
- package/dist/internal/build-output.d.ts +8 -0
- package/dist/internal/build-output.d.ts.map +1 -0
- package/dist/internal/build-output.js +79 -0
- package/dist/internal/build-output.js.map +1 -0
- package/dist/internal/docs-elements.d.ts +1 -0
- package/dist/internal/docs-elements.d.ts.map +1 -1
- package/dist/internal/docs-elements.js +5 -4
- package/dist/internal/docs-elements.js.map +1 -1
- package/dist/internal/entry-arg.d.ts +4 -1
- package/dist/internal/entry-arg.d.ts.map +1 -1
- package/dist/internal/entry-arg.js +7 -1
- package/dist/internal/entry-arg.js.map +1 -1
- package/dist/internal/errors.d.ts +2 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +1 -1
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/output-directory.d.ts +15 -0
- package/dist/internal/output-directory.d.ts.map +1 -0
- package/dist/internal/output-directory.js +490 -0
- package/dist/internal/output-directory.js.map +1 -0
- package/dist/internal/parent-process.d.ts +12 -0
- package/dist/internal/parent-process.d.ts.map +1 -0
- package/dist/internal/parent-process.js +74 -0
- package/dist/internal/parent-process.js.map +1 -0
- package/dist/internal/prepare-project.d.ts +2 -0
- package/dist/internal/prepare-project.d.ts.map +1 -1
- package/dist/internal/prepare-project.js +3 -3
- package/dist/internal/prepare-project.js.map +1 -1
- package/dist/internal/source-imports.js +1 -1
- package/dist/internal/source-imports.js.map +1 -1
- package/dist/mcp/client.js +2 -2
- package/dist/mcp/client.js.map +1 -1
- package/dist/vite-plugins/build-manifest.d.ts +5 -1
- package/dist/vite-plugins/build-manifest.d.ts.map +1 -1
- package/dist/vite-plugins/build-manifest.js +2 -1
- package/dist/vite-plugins/build-manifest.js.map +1 -1
- package/dist/vite-plugins/ensure-store.d.ts +1 -1
- package/dist/vite-plugins/ensure-store.d.ts.map +1 -1
- package/dist/vite-plugins/ensure-store.js +2 -2
- package/dist/vite-plugins/ensure-store.js.map +1 -1
- package/dist/vite-plugins/i18n.d.ts.map +1 -1
- package/dist/vite-plugins/i18n.js +58 -6
- package/dist/vite-plugins/i18n.js.map +1 -1
- package/dist/vite-plugins/index.d.ts +8 -1
- package/dist/vite-plugins/index.d.ts.map +1 -1
- package/dist/vite-plugins/index.js +6 -2
- package/dist/vite-plugins/index.js.map +1 -1
- package/dist/vite-plugins/native.d.ts.map +1 -1
- package/dist/vite-plugins/native.js +0 -4
- package/dist/vite-plugins/native.js.map +1 -1
- package/dist/vite-plugins/self-contained.d.ts.map +1 -1
- package/dist/vite-plugins/self-contained.js +159 -158
- package/dist/vite-plugins/self-contained.js.map +1 -1
- package/dist/vite-plugins/settings-worker-env.d.ts +1 -1
- package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
- package/dist/vite-plugins/settings-worker-env.js +2 -2
- package/dist/vite-plugins/settings-worker-env.js.map +1 -1
- package/dist/vitest-plugin.d.ts +2 -1
- package/dist/vitest-plugin.d.ts.map +1 -1
- package/dist/vitest-plugin.js +6 -6
- package/dist/vitest-plugin.js.map +1 -1
- package/package.json +10 -9
- package/src/builder.ts +18 -5
- package/src/cli.ts +9 -1
- package/src/codegen/reference.ts +1 -0
- package/src/codegen/run-codegen.ts +17 -19
- package/src/codegen/store-resolver.ts +5 -3
- package/src/commands/build.ts +13 -2
- package/src/commands/codegen.ts +6 -5
- package/src/commands/deploy.ts +4 -2
- package/src/commands/dev.ts +38 -4
- package/src/commands/docs.ts +1 -0
- package/src/deploy/freedesktop/metainfo.ts +73 -21
- package/src/deploy/freedesktop/validate.ts +12 -4
- package/src/deploy/nfpm/config.ts +13 -4
- package/src/deploy/nfpm/contents.ts +8 -0
- package/src/deploy/node-runtime/elf.ts +43 -5
- package/src/deploy/node-runtime/index.ts +107 -6
- package/src/deploy/notices/node-runtime.ts +2 -2
- package/src/deploy/payload/launcher.ts +20 -1
- package/src/deploy/payload/stage.ts +1 -1
- package/src/deploy/run-deploy.ts +56 -16
- package/src/deploy/settings/build-manifest.ts +28 -3
- package/src/deploy/settings/extra-files.ts +26 -2
- package/src/deploy/settings/index.ts +3 -1
- package/src/deploy/settings/paths.ts +70 -5
- package/src/deploy/targets/flatpak-source.ts +13 -2
- package/src/deploy/targets/nfpm-target.ts +13 -6
- package/src/deploy/types.ts +1 -0
- package/src/dev/entry-env.ts +2 -1
- package/src/dev/headless.ts +40 -0
- package/src/dev/runner-deps.ts +3 -3
- package/src/dev/runner-main.ts +10 -3
- package/src/dev/supervisor.ts +6 -3
- package/src/i18n/catalogs.ts +1 -0
- package/src/i18n/source-messages.ts +1429 -28
- package/src/internal/build-manifest.ts +25 -1
- package/src/internal/build-output.ts +113 -0
- package/src/internal/docs-elements.ts +10 -4
- package/src/internal/entry-arg.ts +8 -1
- package/src/internal/errors.ts +1 -1
- package/src/internal/output-directory.ts +699 -0
- package/src/internal/parent-process.ts +110 -0
- package/src/internal/prepare-project.ts +5 -5
- package/src/internal/source-imports.ts +1 -1
- package/src/mcp/client.ts +2 -2
- package/src/vite-plugins/build-manifest.ts +7 -1
- package/src/vite-plugins/ensure-store.ts +2 -2
- package/src/vite-plugins/i18n.ts +83 -5
- package/src/vite-plugins/index.ts +14 -7
- package/src/vite-plugins/native.ts +0 -5
- package/src/vite-plugins/self-contained.ts +184 -185
- package/src/vite-plugins/settings-worker-env.ts +2 -2
- package/src/vitest-plugin.ts +6 -6
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
constants,
|
|
4
|
+
fstatSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
openSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
rmdirSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
17
|
+
|
|
18
|
+
type DirectoryIdentity = { device: bigint; inode: bigint };
|
|
19
|
+
type DirectoryLease = { identity: DirectoryIdentity; path: string; release: () => void };
|
|
20
|
+
type DirectoryValidator = (path: string) => boolean;
|
|
21
|
+
type QuarantineDirectory = { displayPath: string; path: string; remove: () => void; release: () => void };
|
|
22
|
+
type CommitOutputRequest = {
|
|
23
|
+
freshIdentity: DirectoryIdentity;
|
|
24
|
+
preservedNames: ReadonlySet<string>;
|
|
25
|
+
previous: string | null;
|
|
26
|
+
quarantine: QuarantineDirectory;
|
|
27
|
+
target: string;
|
|
28
|
+
};
|
|
29
|
+
type InstallFreshOutputRequest = Omit<CommitOutputRequest, "freshIdentity" | "previous"> & {
|
|
30
|
+
fresh: string;
|
|
31
|
+
previous: string;
|
|
32
|
+
};
|
|
33
|
+
type RenameOperation = { destination: string; source: string };
|
|
34
|
+
type RecoverCommitRequest = InstallFreshOutputRequest & {
|
|
35
|
+
backup: string;
|
|
36
|
+
error: unknown;
|
|
37
|
+
operations: RenameOperation[];
|
|
38
|
+
};
|
|
39
|
+
type OverlayEntryRequest = RenameOperation & { backup: string; operations: RenameOperation[] };
|
|
40
|
+
type OutputDirectoryTransaction = Disposable & {
|
|
41
|
+
commit: (preservedNames?: ReadonlySet<string>, isFresh?: DirectoryValidator) => void;
|
|
42
|
+
path: string;
|
|
43
|
+
};
|
|
44
|
+
type PreparedOutputDirectory = { status: "prepared"; transaction: OutputDirectoryTransaction } | { status: "unsafe" };
|
|
45
|
+
type PreparationResources = {
|
|
46
|
+
parent: DirectoryLease;
|
|
47
|
+
previous: string | null;
|
|
48
|
+
publicParent: string;
|
|
49
|
+
publicPath: string;
|
|
50
|
+
publicRoot: string;
|
|
51
|
+
quarantine: QuarantineDirectory;
|
|
52
|
+
root: DirectoryLease;
|
|
53
|
+
target: string;
|
|
54
|
+
};
|
|
55
|
+
type TransactionRequest = PreparationResources & { fresh: DirectoryLease };
|
|
56
|
+
type TransactionState = "committed" | "prepared" | "retained" | "rolled-back";
|
|
57
|
+
|
|
58
|
+
const hasErrorCode = (value: unknown, code: string): boolean =>
|
|
59
|
+
typeof value === "object" && value !== null && "code" in value && value.code === code;
|
|
60
|
+
|
|
61
|
+
const readRegularFile = (path: string): string | null => {
|
|
62
|
+
let descriptor: number | null = null;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
66
|
+
|
|
67
|
+
if (!fstatSync(descriptor).isFile()) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return readFileSync(descriptor, "utf8");
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
} finally {
|
|
75
|
+
if (descriptor !== null) {
|
|
76
|
+
closeSync(descriptor);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const openDirectoryLease = (path: string): DirectoryLease | null => {
|
|
82
|
+
let descriptor: number | null = null;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
descriptor = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
86
|
+
const stats = fstatSync(descriptor, { bigint: true });
|
|
87
|
+
let retainedDescriptor: number | null = descriptor;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
identity: { device: stats.dev, inode: stats.ino },
|
|
91
|
+
path: join("/proc/self/fd", String(descriptor)),
|
|
92
|
+
release: () => {
|
|
93
|
+
if (retainedDescriptor === null) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const releasedDescriptor = retainedDescriptor;
|
|
98
|
+
retainedDescriptor = null;
|
|
99
|
+
closeSync(releasedDescriptor);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
if (descriptor !== null) {
|
|
104
|
+
closeSync(descriptor);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const readDirectoryIdentity = (path: string): DirectoryIdentity | null => {
|
|
112
|
+
const lease = openDirectoryLease(path);
|
|
113
|
+
|
|
114
|
+
if (lease === null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
lease.release();
|
|
119
|
+
|
|
120
|
+
return lease.identity;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const hasDirectoryIdentity = (path: string, expected: DirectoryIdentity): boolean => {
|
|
124
|
+
const actual = readDirectoryIdentity(path);
|
|
125
|
+
|
|
126
|
+
return actual !== null && actual.device === expected.device && actual.inode === expected.inode;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const hasResolvedDirectoryIdentity = (path: string, expected: DirectoryIdentity): boolean => {
|
|
130
|
+
try {
|
|
131
|
+
return hasDirectoryIdentity(realpathSync(path), expected);
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const capturePath = (source: string, destination: string): "captured" | "missing" => {
|
|
138
|
+
try {
|
|
139
|
+
renameSync(source, destination);
|
|
140
|
+
|
|
141
|
+
return "captured";
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
144
|
+
return "missing";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const createQuarantineDirectory = (parent: DirectoryLease, displayParent: string): QuarantineDirectory => {
|
|
152
|
+
const created = mkdtempSync(join(parent.path, ".gtkx-output-"));
|
|
153
|
+
const name = basename(created);
|
|
154
|
+
const entry = join(parent.path, name);
|
|
155
|
+
const lease = openDirectoryLease(entry);
|
|
156
|
+
|
|
157
|
+
if (lease === null) {
|
|
158
|
+
rmdirSync(entry);
|
|
159
|
+
throw new Error("Could not secure the output quarantine directory");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
displayPath: join(displayParent, name),
|
|
164
|
+
path: lease.path,
|
|
165
|
+
release: lease.release,
|
|
166
|
+
remove: () => {
|
|
167
|
+
if (!hasDirectoryIdentity(entry, lease.identity)) {
|
|
168
|
+
throw new Error(`The output quarantine changed; retained files are in ${join(displayParent, name)}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
rmdirSync(entry);
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const releaseTransactionLeases = (
|
|
177
|
+
fresh: DirectoryLease,
|
|
178
|
+
quarantine: QuarantineDirectory,
|
|
179
|
+
parent: DirectoryLease,
|
|
180
|
+
root: DirectoryLease,
|
|
181
|
+
): void => {
|
|
182
|
+
try {
|
|
183
|
+
fresh.release();
|
|
184
|
+
} finally {
|
|
185
|
+
try {
|
|
186
|
+
quarantine.release();
|
|
187
|
+
} finally {
|
|
188
|
+
try {
|
|
189
|
+
parent.release();
|
|
190
|
+
} finally {
|
|
191
|
+
root.release();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const releasePreparationLeases = (
|
|
198
|
+
quarantine: QuarantineDirectory,
|
|
199
|
+
parent: DirectoryLease,
|
|
200
|
+
root: DirectoryLease,
|
|
201
|
+
): void => {
|
|
202
|
+
try {
|
|
203
|
+
quarantine.release();
|
|
204
|
+
} finally {
|
|
205
|
+
try {
|
|
206
|
+
parent.release();
|
|
207
|
+
} finally {
|
|
208
|
+
root.release();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const movePath = (source: string, destination: string, operations: RenameOperation[]): void => {
|
|
214
|
+
renameSync(source, destination);
|
|
215
|
+
operations.push({ source, destination });
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const moveNames = (source: string, destination: string, names: string[], operations: RenameOperation[]): void => {
|
|
219
|
+
for (const name of names) {
|
|
220
|
+
movePath(join(source, name), join(destination, name), operations);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const reverseOperations = (operations: RenameOperation[]): void => {
|
|
225
|
+
for (const operation of operations.toReversed()) {
|
|
226
|
+
renameSync(operation.destination, operation.source);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const overlayDirectory = (
|
|
231
|
+
source: string,
|
|
232
|
+
destination: string,
|
|
233
|
+
backup: string,
|
|
234
|
+
operations: RenameOperation[],
|
|
235
|
+
): void => {
|
|
236
|
+
for (const name of readdirSync(source)) {
|
|
237
|
+
overlayEntry({
|
|
238
|
+
source: join(source, name),
|
|
239
|
+
destination: join(destination, name),
|
|
240
|
+
backup: join(backup, name),
|
|
241
|
+
operations,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const overlayEntry = (request: OverlayEntryRequest): void => {
|
|
247
|
+
const destinationStats = lstatSync(request.destination, { throwIfNoEntry: false });
|
|
248
|
+
|
|
249
|
+
if (destinationStats === undefined) {
|
|
250
|
+
movePath(request.source, request.destination, request.operations);
|
|
251
|
+
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const sourceStats = lstatSync(request.source);
|
|
256
|
+
|
|
257
|
+
if (sourceStats.isDirectory() && destinationStats.isDirectory()) {
|
|
258
|
+
overlayDirectory(request.source, request.destination, request.backup, request.operations);
|
|
259
|
+
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!sourceStats.isFile() || !destinationStats.isFile()) {
|
|
264
|
+
throw new Error("Cannot merge conflicting preserved output entries");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
mkdirSync(dirname(request.backup), { recursive: true });
|
|
268
|
+
movePath(request.destination, request.backup, request.operations);
|
|
269
|
+
movePath(request.source, request.destination, request.operations);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const restoreUnexpectedOutput = (
|
|
273
|
+
unexpected: string,
|
|
274
|
+
target: string,
|
|
275
|
+
quarantine: QuarantineDirectory,
|
|
276
|
+
previous: string | null,
|
|
277
|
+
): never => {
|
|
278
|
+
try {
|
|
279
|
+
renameSync(unexpected, target);
|
|
280
|
+
|
|
281
|
+
if (previous === null) {
|
|
282
|
+
quarantine.remove();
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
throw new Error(`Output recovery failed; retained files are in ${quarantine.displayPath}`, { cause: error });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
throw new Error(
|
|
289
|
+
`Output changed during the operation; the previous output is retained in ${quarantine.displayPath}`,
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const recoverUnsecuredPublicPath = (
|
|
294
|
+
target: string,
|
|
295
|
+
quarantine: QuarantineDirectory,
|
|
296
|
+
previous: string | null,
|
|
297
|
+
error: unknown,
|
|
298
|
+
): never => {
|
|
299
|
+
const unsecured = join(quarantine.path, "unsecured");
|
|
300
|
+
const publicCapture = capturePath(target, unsecured);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
if (previous !== null) {
|
|
304
|
+
renameSync(previous, target);
|
|
305
|
+
} else if (publicCapture === "captured") {
|
|
306
|
+
renameSync(unsecured, target);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (previous === null || publicCapture === "missing") {
|
|
310
|
+
quarantine.remove();
|
|
311
|
+
}
|
|
312
|
+
} catch (recoveryError) {
|
|
313
|
+
throw new AggregateError(
|
|
314
|
+
[error, recoveryError],
|
|
315
|
+
`Could not recover the output directory; retained files are in ${quarantine.displayPath}`,
|
|
316
|
+
{ cause: recoveryError },
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
throw new Error(`Could not secure the output directory; retained files are in ${quarantine.displayPath}`, {
|
|
321
|
+
cause: error,
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const recoverFailedCapture = (quarantine: QuarantineDirectory, error: unknown): never => {
|
|
326
|
+
try {
|
|
327
|
+
quarantine.remove();
|
|
328
|
+
} catch (recoveryError) {
|
|
329
|
+
throw new AggregateError(
|
|
330
|
+
[error, recoveryError],
|
|
331
|
+
`Could not clean the output quarantine; retained files are in ${quarantine.displayPath}`,
|
|
332
|
+
{ cause: recoveryError },
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
throw error;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const rollbackOutputDirectory = (
|
|
340
|
+
target: string,
|
|
341
|
+
quarantine: QuarantineDirectory,
|
|
342
|
+
previous: string | null,
|
|
343
|
+
freshIdentity: DirectoryIdentity,
|
|
344
|
+
): void => {
|
|
345
|
+
const failed = join(quarantine.path, "failed");
|
|
346
|
+
const freshCapture = capturePath(target, failed);
|
|
347
|
+
|
|
348
|
+
if (freshCapture === "captured" && !hasDirectoryIdentity(failed, freshIdentity)) {
|
|
349
|
+
restoreUnexpectedOutput(failed, target, quarantine, previous);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (previous !== null) {
|
|
353
|
+
renameSync(previous, target);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (freshCapture === "captured") {
|
|
357
|
+
rmSync(failed, { recursive: true, force: true, maxRetries: 5 });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
quarantine.remove();
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const restoreMissingFreshOutput = (
|
|
364
|
+
target: string,
|
|
365
|
+
quarantine: QuarantineDirectory,
|
|
366
|
+
previous: string | null,
|
|
367
|
+
): never => {
|
|
368
|
+
try {
|
|
369
|
+
if (previous !== null) {
|
|
370
|
+
renameSync(previous, target);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
quarantine.remove();
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw new Error(`The fresh output disappeared; retained files are in ${quarantine.displayPath}`, {
|
|
376
|
+
cause: error,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
throw new Error("The fresh output directory disappeared before it could be committed");
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const captureVerifiedFreshOutput = (
|
|
384
|
+
target: string,
|
|
385
|
+
quarantine: QuarantineDirectory,
|
|
386
|
+
previous: string | null,
|
|
387
|
+
freshIdentity: DirectoryIdentity,
|
|
388
|
+
): string => {
|
|
389
|
+
const fresh = join(quarantine.path, "fresh");
|
|
390
|
+
|
|
391
|
+
if (capturePath(target, fresh) === "missing") {
|
|
392
|
+
restoreMissingFreshOutput(target, quarantine, previous);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!hasDirectoryIdentity(fresh, freshIdentity)) {
|
|
396
|
+
restoreUnexpectedOutput(fresh, target, quarantine, previous);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return fresh;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const recoverCommit = (request: RecoverCommitRequest): never => {
|
|
403
|
+
const { backup, error, fresh, operations, previous, quarantine, target } = request;
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
reverseOperations(operations);
|
|
407
|
+
renameSync(previous, target);
|
|
408
|
+
rmSync(fresh, { recursive: true, force: true, maxRetries: 5 });
|
|
409
|
+
rmSync(backup, { recursive: true, force: true, maxRetries: 5 });
|
|
410
|
+
quarantine.remove();
|
|
411
|
+
} catch (recoveryError) {
|
|
412
|
+
throw new AggregateError(
|
|
413
|
+
[error, recoveryError],
|
|
414
|
+
`Could not restore the previous output; retained files are in ${quarantine.displayPath}`,
|
|
415
|
+
{ cause: recoveryError },
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
throw error;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const installFreshOutput = (request: InstallFreshOutputRequest): void => {
|
|
423
|
+
const { fresh, preservedNames, previous, quarantine, target } = request;
|
|
424
|
+
const backup = join(quarantine.path, "backup");
|
|
425
|
+
const operations: RenameOperation[] = [];
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
const oldNames = readdirSync(previous).filter((name) => !preservedNames.has(name));
|
|
429
|
+
const freshNames = readdirSync(fresh);
|
|
430
|
+
const ordinaryFreshNames = freshNames.filter((name) => !preservedNames.has(name));
|
|
431
|
+
const preservedFreshNames = freshNames.filter((name) => preservedNames.has(name));
|
|
432
|
+
mkdirSync(backup);
|
|
433
|
+
moveNames(previous, backup, oldNames, operations);
|
|
434
|
+
moveNames(fresh, previous, ordinaryFreshNames, operations);
|
|
435
|
+
|
|
436
|
+
for (const name of preservedFreshNames) {
|
|
437
|
+
overlayEntry({
|
|
438
|
+
source: join(fresh, name),
|
|
439
|
+
destination: join(previous, name),
|
|
440
|
+
backup: join(backup, name),
|
|
441
|
+
operations,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
renameSync(previous, target);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
recoverCommit({ ...request, backup, operations, error });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
rmSync(fresh, { recursive: true, force: true, maxRetries: 5 });
|
|
451
|
+
rmSync(backup, { recursive: true, force: true, maxRetries: 5 });
|
|
452
|
+
quarantine.remove();
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const commitOutputDirectory = (request: CommitOutputRequest): void => {
|
|
456
|
+
const { freshIdentity, preservedNames, previous, quarantine, target } = request;
|
|
457
|
+
const fresh = captureVerifiedFreshOutput(target, quarantine, previous, freshIdentity);
|
|
458
|
+
|
|
459
|
+
if (previous === null) {
|
|
460
|
+
renameSync(fresh, target);
|
|
461
|
+
quarantine.remove();
|
|
462
|
+
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
installFreshOutput({ target, quarantine, previous, fresh, preservedNames });
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const assertFreshOutput = (path: string, isFresh: DirectoryValidator | undefined): void => {
|
|
470
|
+
if (isFresh !== undefined && !isFresh(path)) {
|
|
471
|
+
throw new Error("The build did not write to the secured output directory");
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const createOutputDirectoryTransaction = (request: TransactionRequest): OutputDirectoryTransaction => {
|
|
476
|
+
const { fresh, parent, previous, publicParent, publicPath, publicRoot, quarantine, root, target } = request;
|
|
477
|
+
let state: TransactionState = "prepared";
|
|
478
|
+
|
|
479
|
+
const commit = (
|
|
480
|
+
preservedNames: ReadonlySet<string> = new Set(),
|
|
481
|
+
isFresh?: DirectoryValidator,
|
|
482
|
+
): void => {
|
|
483
|
+
if (state !== "prepared") {
|
|
484
|
+
throw new Error("The output directory transaction has already finished");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (
|
|
488
|
+
!hasResolvedDirectoryIdentity(publicRoot, root.identity) ||
|
|
489
|
+
!hasResolvedDirectoryIdentity(publicParent, parent.identity)
|
|
490
|
+
) {
|
|
491
|
+
throw new Error(`The output parent changed; previous output is retained in ${quarantine.displayPath}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
assertFreshOutput(target, isFresh);
|
|
495
|
+
state = "retained";
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
commitOutputDirectory({
|
|
499
|
+
target,
|
|
500
|
+
quarantine,
|
|
501
|
+
previous,
|
|
502
|
+
freshIdentity: fresh.identity,
|
|
503
|
+
preservedNames,
|
|
504
|
+
});
|
|
505
|
+
state = "committed";
|
|
506
|
+
} finally {
|
|
507
|
+
releaseTransactionLeases(fresh, quarantine, parent, root);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const rollback = (): void => {
|
|
512
|
+
if (state !== "prepared") {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
state = "retained";
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
rollbackOutputDirectory(target, quarantine, previous, fresh.identity);
|
|
520
|
+
state = "rolled-back";
|
|
521
|
+
} finally {
|
|
522
|
+
releaseTransactionLeases(fresh, quarantine, parent, root);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
return { path: publicPath, commit, [Symbol.dispose]: rollback };
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const rollbackAfterError = (transaction: OutputDirectoryTransaction, error: unknown): never => {
|
|
530
|
+
try {
|
|
531
|
+
transaction[Symbol.dispose]();
|
|
532
|
+
} catch (rollbackError) {
|
|
533
|
+
throw new AggregateError(
|
|
534
|
+
[error, rollbackError],
|
|
535
|
+
"Could not roll back the output directory transaction",
|
|
536
|
+
{ cause: rollbackError },
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
throw error;
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const requireFreshDirectoryLease = (resources: PreparationResources): DirectoryLease => {
|
|
544
|
+
const { parent, previous, quarantine, root, target } = resources;
|
|
545
|
+
const lease = openDirectoryLease(target);
|
|
546
|
+
|
|
547
|
+
if (lease !== null) {
|
|
548
|
+
return lease;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
return recoverUnsecuredPublicPath(
|
|
553
|
+
target,
|
|
554
|
+
quarantine,
|
|
555
|
+
previous,
|
|
556
|
+
new Error("Could not record the fresh output directory identity"),
|
|
557
|
+
);
|
|
558
|
+
} finally {
|
|
559
|
+
releasePreparationLeases(quarantine, parent, root);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const requireDirectoryLease = (path: string, message: string): DirectoryLease => {
|
|
564
|
+
const lease = openDirectoryLease(path);
|
|
565
|
+
|
|
566
|
+
if (lease === null) {
|
|
567
|
+
throw new Error(message);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return lease;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const createChildDirectoryLease = (parent: DirectoryLease, name: string): DirectoryLease => {
|
|
574
|
+
const path = join(parent.path, name);
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
mkdirSync(path);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (!hasErrorCode(error, "EEXIST")) {
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return requireDirectoryLease(path, "Could not secure the output parent directory");
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const openOutputParent = (root: DirectoryLease, rootPath: string, outputPath: string): DirectoryLease => {
|
|
588
|
+
const relativeParent = relative(rootPath, dirname(outputPath));
|
|
589
|
+
|
|
590
|
+
if (relativeParent === ".." || isAbsolute(relativeParent) || relativeParent.startsWith(`..${sep}`)) {
|
|
591
|
+
throw new Error("The output directory must be below the project root");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
let parent = requireDirectoryLease(`${root.path}/.`, "Could not secure the project root directory");
|
|
595
|
+
const segments = relativeParent.split(sep);
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
for (const segment of segments) {
|
|
599
|
+
if (segment.length === 0) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const child = createChildDirectoryLease(parent, segment);
|
|
604
|
+
parent.release();
|
|
605
|
+
parent = child;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return parent;
|
|
609
|
+
} catch (error) {
|
|
610
|
+
parent.release();
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const createPreparationResources = (rootPath: string, path: string): PreparationResources => {
|
|
616
|
+
const canonicalRoot = realpathSync(rootPath);
|
|
617
|
+
const root = requireDirectoryLease(canonicalRoot, "Could not secure the project root directory");
|
|
618
|
+
let parent: DirectoryLease;
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
parent = openOutputParent(root, rootPath, path);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
root.release();
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let quarantine: QuarantineDirectory;
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
quarantine = createQuarantineDirectory(root, canonicalRoot);
|
|
631
|
+
} catch (error) {
|
|
632
|
+
parent.release();
|
|
633
|
+
root.release();
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const target = join(parent.path, basename(path));
|
|
638
|
+
const captured = join(quarantine.path, "previous");
|
|
639
|
+
let previous: string | null = null;
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
previous = capturePath(target, captured) === "captured" ? captured : null;
|
|
643
|
+
} catch (error) {
|
|
644
|
+
try {
|
|
645
|
+
recoverFailedCapture(quarantine, error);
|
|
646
|
+
} finally {
|
|
647
|
+
releasePreparationLeases(quarantine, parent, root);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
mkdirSync(target);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
try {
|
|
655
|
+
recoverUnsecuredPublicPath(target, quarantine, previous, error);
|
|
656
|
+
} finally {
|
|
657
|
+
releasePreparationLeases(quarantine, parent, root);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
target,
|
|
663
|
+
publicPath: path,
|
|
664
|
+
publicRoot: rootPath,
|
|
665
|
+
publicParent: dirname(path),
|
|
666
|
+
parent,
|
|
667
|
+
quarantine,
|
|
668
|
+
previous,
|
|
669
|
+
root,
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const prepareOutputDirectory = (
|
|
674
|
+
rootPath: string,
|
|
675
|
+
path: string,
|
|
676
|
+
isReusable: DirectoryValidator,
|
|
677
|
+
): PreparedOutputDirectory => {
|
|
678
|
+
const resources = createPreparationResources(rootPath, path);
|
|
679
|
+
const fresh = requireFreshDirectoryLease(resources);
|
|
680
|
+
const transaction = createOutputDirectoryTransaction({ ...resources, fresh });
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
if (resources.previous === null || isReusable(resources.previous)) {
|
|
684
|
+
return { status: "prepared", transaction };
|
|
685
|
+
}
|
|
686
|
+
} catch (error) {
|
|
687
|
+
rollbackAfterError(transaction, error);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
transaction[Symbol.dispose]();
|
|
691
|
+
|
|
692
|
+
return { status: "unsafe" };
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
export {
|
|
696
|
+
type OutputDirectoryTransaction,
|
|
697
|
+
prepareOutputDirectory,
|
|
698
|
+
readRegularFile,
|
|
699
|
+
};
|