@onmark/cli 0.1.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/LICENSE +21 -0
- package/README.md +202 -0
- package/onmark-release.json +388 -0
- package/package.json +52 -0
- package/packages/authoring/dist/src/index.d.ts +3 -0
- package/packages/authoring/dist/src/index.js +4 -0
- package/packages/authoring/dist/src/motion.d.ts +23 -0
- package/packages/authoring/dist/src/motion.js +81 -0
- package/packages/authoring/dist/src/presentation.d.ts +12 -0
- package/packages/authoring/dist/src/presentation.js +226 -0
- package/packages/authoring/dist/src/resource.d.ts +32 -0
- package/packages/authoring/dist/src/resource.js +86 -0
- package/packages/authoring/dist/src/types.d.ts +1 -0
- package/packages/authoring/dist/src/types.js +2 -0
- package/packages/bundler/dist/src/authored_html.d.ts +41 -0
- package/packages/bundler/dist/src/authored_html.js +387 -0
- package/packages/bundler/dist/src/command.d.ts +2 -0
- package/packages/bundler/dist/src/command.js +138 -0
- package/packages/bundler/dist/src/generated/bundle-manifest.d.ts +65 -0
- package/packages/bundler/dist/src/generated/bundle-manifest.js +18 -0
- package/packages/bundler/dist/src/html_image.d.ts +20 -0
- package/packages/bundler/dist/src/html_image.js +337 -0
- package/packages/bundler/dist/src/image_admission.d.ts +2 -0
- package/packages/bundler/dist/src/image_admission.js +217 -0
- package/packages/bundler/dist/src/index.d.ts +2 -0
- package/packages/bundler/dist/src/index.js +3 -0
- package/packages/bundler/dist/src/presentation.d.ts +41 -0
- package/packages/bundler/dist/src/presentation.js +492 -0
- package/packages/launcher/dist/desktop-release.json +21 -0
- package/packages/launcher/dist/src/browser-command.d.ts +2 -0
- package/packages/launcher/dist/src/browser-command.js +43 -0
- package/packages/launcher/dist/src/browser.d.ts +14 -0
- package/packages/launcher/dist/src/browser.js +280 -0
- package/packages/launcher/dist/src/cache.d.ts +8 -0
- package/packages/launcher/dist/src/cache.js +21 -0
- package/packages/launcher/dist/src/capture-environment.d.ts +15 -0
- package/packages/launcher/dist/src/capture-environment.js +145 -0
- package/packages/launcher/dist/src/command.d.ts +2 -0
- package/packages/launcher/dist/src/command.js +58 -0
- package/packages/launcher/dist/src/native.d.ts +20 -0
- package/packages/launcher/dist/src/native.js +31 -0
- package/packages/launcher/dist/src/platform.d.ts +13 -0
- package/packages/launcher/dist/src/platform.js +29 -0
- package/packages/launcher/dist/src/release.d.ts +16 -0
- package/packages/launcher/dist/src/release.js +49 -0
- package/packages/motion-gsap/dist/src/index.d.ts +16 -0
- package/packages/motion-gsap/dist/src/index.js +139 -0
- package/packages/runtime/dist/src/clock.d.ts +10 -0
- package/packages/runtime/dist/src/clock.js +19 -0
- package/packages/runtime/dist/src/generated/browser-request.d.ts +143 -0
- package/packages/runtime/dist/src/generated/browser-request.js +2 -0
- package/packages/runtime/dist/src/generated/browser-response.d.ts +58 -0
- package/packages/runtime/dist/src/generated/browser-response.js +2 -0
- package/packages/runtime/dist/src/generated/bundle-layout.d.ts +7 -0
- package/packages/runtime/dist/src/generated/bundle-layout.js +14 -0
- package/packages/runtime/dist/src/generated/codec.d.ts +8 -0
- package/packages/runtime/dist/src/generated/codec.js +22 -0
- package/packages/runtime/dist/src/generated/runtime-contract.d.ts +7 -0
- package/packages/runtime/dist/src/generated/runtime-contract.js +8 -0
- package/packages/runtime/dist/src/generated/validators.d.ts +13 -0
- package/packages/runtime/dist/src/generated/validators.js +3327 -0
- package/packages/runtime/dist/src/host.d.ts +11 -0
- package/packages/runtime/dist/src/host.js +25 -0
- package/packages/runtime/dist/src/index.d.ts +13 -0
- package/packages/runtime/dist/src/index.js +12 -0
- package/packages/runtime/dist/src/media.d.ts +13 -0
- package/packages/runtime/dist/src/media.js +28 -0
- package/packages/runtime/dist/src/presentation.d.ts +66 -0
- package/packages/runtime/dist/src/presentation.js +379 -0
- package/packages/runtime/dist/src/resource.d.ts +23 -0
- package/packages/runtime/dist/src/resource.js +150 -0
- package/packages/runtime/dist/src/session.d.ts +43 -0
- package/packages/runtime/dist/src/session.js +450 -0
- package/packages/runtime/dist/src/types.d.ts +6 -0
- package/packages/runtime/dist/src/types.js +2 -0
- package/packages/runtime/dist/src/video.d.ts +40 -0
- package/packages/runtime/dist/src/video.js +343 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// Verified browser installation owns the desktop capture environment cache.
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { access, mkdir, mkdtemp, readdir, rename, rm, rmdir, stat, utimes, writeFile, } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { Cache, InstalledBrowser, install, } from "@puppeteer/browsers";
|
|
7
|
+
import { BROWSER_BUILD, desktopTarget, } from "./release.js";
|
|
8
|
+
const INSTALL_LOCK_TIMINGS = {
|
|
9
|
+
heartbeatMilliseconds: 10_000,
|
|
10
|
+
pollMilliseconds: 200,
|
|
11
|
+
staleMilliseconds: 60_000,
|
|
12
|
+
waitMilliseconds: 3 * 60_000,
|
|
13
|
+
};
|
|
14
|
+
const INSTALL_LOCK_OWNER_PREFIX = ".owner-";
|
|
15
|
+
const INSTALL_STAGING_PREFIX = ".browser-install-";
|
|
16
|
+
export class BrowserInstallError extends Error {
|
|
17
|
+
constructor(message, cause) {
|
|
18
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
19
|
+
this.name = "BrowserInstallError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// ── Installation pipeline
|
|
23
|
+
export async function ensureBrowser(options, installBrowser = install) {
|
|
24
|
+
const target = desktopTarget(options.host);
|
|
25
|
+
await mkdir(options.cacheDirectory, { recursive: true });
|
|
26
|
+
return withInstallLock(options.cacheDirectory, async (lease) => {
|
|
27
|
+
const admittedCache = join(options.cacheDirectory, BROWSER_BUILD, target.sha256);
|
|
28
|
+
await mkdir(admittedCache, { recursive: true });
|
|
29
|
+
const installed = installedBrowser(admittedCache, target.browser, target.browserPlatform);
|
|
30
|
+
if (await isExecutable(installed.executablePath, options.host.platform)) {
|
|
31
|
+
return installed.executablePath;
|
|
32
|
+
}
|
|
33
|
+
await verifyInstallLease(lease);
|
|
34
|
+
await removeAbandonedStaging(admittedCache);
|
|
35
|
+
await removeIncompleteInstall(installed);
|
|
36
|
+
return installVerifiedBrowser(lease, {
|
|
37
|
+
cacheDirectory: admittedCache,
|
|
38
|
+
hostPlatform: options.host.platform,
|
|
39
|
+
installBrowser,
|
|
40
|
+
target,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function installVerifiedBrowser(lease, installation) {
|
|
45
|
+
const staging = await mkdtemp(join(installation.cacheDirectory, INSTALL_STAGING_PREFIX));
|
|
46
|
+
return withObservedCleanup(() => publishVerifiedBrowser(lease, installation, staging), () => rm(staging, { force: true, recursive: true }), "browser installation failed and staging cleanup also failed");
|
|
47
|
+
}
|
|
48
|
+
async function publishVerifiedBrowser(lease, installation, staging) {
|
|
49
|
+
const { cacheDirectory, hostPlatform, installBrowser, target } = installation;
|
|
50
|
+
try {
|
|
51
|
+
const staged = await installBrowser({
|
|
52
|
+
browser: target.browser,
|
|
53
|
+
buildId: BROWSER_BUILD,
|
|
54
|
+
cacheDir: staging,
|
|
55
|
+
expectedHash: target.sha256,
|
|
56
|
+
platform: target.browserPlatform,
|
|
57
|
+
});
|
|
58
|
+
if (!(await isExecutable(staged.executablePath, hostPlatform))) {
|
|
59
|
+
throw new BrowserInstallError("the verified browser archive did not produce an executable");
|
|
60
|
+
}
|
|
61
|
+
const destination = installedBrowser(cacheDirectory, target.browser, target.browserPlatform);
|
|
62
|
+
await mkdir(dirname(destination.path), { recursive: true });
|
|
63
|
+
await verifyInstallLease(lease);
|
|
64
|
+
await rename(staged.path, destination.path);
|
|
65
|
+
if (!(await isExecutable(destination.executablePath, hostPlatform))) {
|
|
66
|
+
throw new BrowserInstallError("the published browser installation has no executable");
|
|
67
|
+
}
|
|
68
|
+
return destination.executablePath;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error instanceof BrowserInstallError) {
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
throw new BrowserInstallError(`failed to install ${target.browser} ${BROWSER_BUILD}`, error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function removeAbandonedStaging(cacheDirectory) {
|
|
78
|
+
const entries = await readdir(cacheDirectory, { withFileTypes: true });
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (entry.name.startsWith(INSTALL_STAGING_PREFIX)) {
|
|
81
|
+
await rm(join(cacheDirectory, entry.name), {
|
|
82
|
+
force: true,
|
|
83
|
+
recursive: true,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function installedBrowser(cacheDirectory, browser, platform) {
|
|
89
|
+
return new InstalledBrowser(new Cache(cacheDirectory), browser, BROWSER_BUILD, platform);
|
|
90
|
+
}
|
|
91
|
+
async function removeIncompleteInstall(installed) {
|
|
92
|
+
try {
|
|
93
|
+
await access(installed.path, constants.F_OK);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (isErrno(error, "ENOENT")) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
await rm(installed.path, { force: true, recursive: true });
|
|
102
|
+
}
|
|
103
|
+
async function isExecutable(path, hostPlatform) {
|
|
104
|
+
try {
|
|
105
|
+
const metadata = await stat(path);
|
|
106
|
+
if (!metadata.isFile()) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
await access(path, hostPlatform === "win32" ? constants.F_OK : constants.X_OK);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ── Cross-process installation lease
|
|
117
|
+
async function withInstallLock(cacheDirectory, action, timings = INSTALL_LOCK_TIMINGS) {
|
|
118
|
+
const lock = join(cacheDirectory, ".install.lock");
|
|
119
|
+
const lease = await acquireInstallLock(lock, timings);
|
|
120
|
+
const heartbeat = setInterval(() => {
|
|
121
|
+
void refreshInstallLease(lease);
|
|
122
|
+
}, timings.heartbeatMilliseconds);
|
|
123
|
+
heartbeat.unref();
|
|
124
|
+
return withObservedCleanup(() => action(lease), async () => {
|
|
125
|
+
clearInterval(heartbeat);
|
|
126
|
+
await releaseInstallLock(lease);
|
|
127
|
+
}, "browser installation failed and lease cleanup also failed");
|
|
128
|
+
}
|
|
129
|
+
async function refreshInstallLease(lease) {
|
|
130
|
+
const now = new Date();
|
|
131
|
+
try {
|
|
132
|
+
await utimes(lease.marker, now, now);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// A reclaimer may have moved this exact lease. The owner-specific marker
|
|
136
|
+
// prevents the resumed heartbeat from touching a successor's lock.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function verifyInstallLease(lease) {
|
|
140
|
+
const now = new Date();
|
|
141
|
+
try {
|
|
142
|
+
await utimes(lease.marker, now, now);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
throw new BrowserInstallError("browser installation lease was lost before cache publication", error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function acquireInstallLock(lock, timings) {
|
|
149
|
+
const deadline = Date.now() + timings.waitMilliseconds;
|
|
150
|
+
while (Date.now() < deadline) {
|
|
151
|
+
const lease = await tryAcquireInstallLock(lock);
|
|
152
|
+
if (lease !== undefined) {
|
|
153
|
+
return lease;
|
|
154
|
+
}
|
|
155
|
+
await reclaimAbandonedLock(lock, timings.staleMilliseconds);
|
|
156
|
+
await delay(timings.pollMilliseconds);
|
|
157
|
+
}
|
|
158
|
+
throw new BrowserInstallError("another Onmark process is still installing the browser");
|
|
159
|
+
}
|
|
160
|
+
async function tryAcquireInstallLock(lock) {
|
|
161
|
+
const owner = randomUUID();
|
|
162
|
+
const candidate = `${lock}.candidate-${owner}`;
|
|
163
|
+
const markerName = `${INSTALL_LOCK_OWNER_PREFIX}${owner}`;
|
|
164
|
+
await mkdir(candidate);
|
|
165
|
+
return withObservedCleanup(async () => {
|
|
166
|
+
try {
|
|
167
|
+
await writeFile(join(candidate, markerName), "", { flag: "wx" });
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
throw new BrowserInstallError("failed to prepare the browser installation lock", error);
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
await rename(candidate, lock);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (isLockContention(error) || (await installLockExists(lock))) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
throw new BrowserInstallError("failed to acquire the browser installation lock", error);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
directory: lock,
|
|
183
|
+
marker: join(lock, markerName),
|
|
184
|
+
};
|
|
185
|
+
}, () => rm(candidate, { force: true, recursive: true }), "browser installation lock acquisition and candidate cleanup both failed");
|
|
186
|
+
}
|
|
187
|
+
async function releaseInstallLock(lease) {
|
|
188
|
+
await rm(lease.marker, { force: true });
|
|
189
|
+
try {
|
|
190
|
+
await rmdir(lease.directory);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (isErrno(error, "ENOENT") ||
|
|
194
|
+
isErrno(error, "ENOTEMPTY") ||
|
|
195
|
+
isErrno(error, "EEXIST")) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function reclaimAbandonedLock(lock, staleMilliseconds) {
|
|
202
|
+
let modified;
|
|
203
|
+
try {
|
|
204
|
+
modified = await installLockModifiedAt(lock);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (isErrno(error, "ENOENT")) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
if (Date.now() - modified <= staleMilliseconds) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const abandoned = `${lock}.abandoned-${randomUUID()}`;
|
|
216
|
+
try {
|
|
217
|
+
await rename(lock, abandoned);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (isErrno(error, "ENOENT")) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
await rm(abandoned, { force: true, recursive: true });
|
|
226
|
+
}
|
|
227
|
+
async function installLockModifiedAt(lock) {
|
|
228
|
+
const owners = (await readdir(lock))
|
|
229
|
+
.filter((entry) => entry.startsWith(INSTALL_LOCK_OWNER_PREFIX))
|
|
230
|
+
.sort();
|
|
231
|
+
const owner = owners[0];
|
|
232
|
+
if (owners.length === 1 && owner !== undefined) {
|
|
233
|
+
return (await stat(join(lock, owner))).mtimeMs;
|
|
234
|
+
}
|
|
235
|
+
return (await stat(lock)).mtimeMs;
|
|
236
|
+
}
|
|
237
|
+
function isLockContention(error) {
|
|
238
|
+
return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
|
|
239
|
+
}
|
|
240
|
+
async function installLockExists(lock) {
|
|
241
|
+
try {
|
|
242
|
+
return (await stat(lock)).isDirectory();
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (isErrno(error, "ENOENT")) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
throw new BrowserInstallError("failed to inspect the browser installation lock", error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function isErrno(error, code) {
|
|
252
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
253
|
+
}
|
|
254
|
+
function delay(milliseconds) {
|
|
255
|
+
return new Promise((resolve) => {
|
|
256
|
+
setTimeout(resolve, milliseconds);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async function withObservedCleanup(action, cleanup, combinedMessage) {
|
|
260
|
+
let outcome;
|
|
261
|
+
try {
|
|
262
|
+
outcome = { status: "fulfilled", value: await action() };
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
outcome = { reason: error, status: "rejected" };
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
await cleanup();
|
|
269
|
+
}
|
|
270
|
+
catch (cleanupError) {
|
|
271
|
+
if (outcome.status === "fulfilled") {
|
|
272
|
+
throw cleanupError;
|
|
273
|
+
}
|
|
274
|
+
throw new AggregateError([outcome.reason, cleanupError], combinedMessage);
|
|
275
|
+
}
|
|
276
|
+
if (outcome.status === "rejected") {
|
|
277
|
+
throw outcome.reason;
|
|
278
|
+
}
|
|
279
|
+
return outcome.value;
|
|
280
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReleaseHost } from "./release.js";
|
|
2
|
+
export interface CacheEnvironment {
|
|
3
|
+
readonly localAppData?: string;
|
|
4
|
+
readonly windowsDirectory?: string;
|
|
5
|
+
readonly xdgCacheHome?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function frameCacheDirectory(host: ReleaseHost, homeDirectory: string, environment: CacheEnvironment): string;
|
|
8
|
+
export declare function browserCacheDirectory(host: ReleaseHost, homeDirectory: string, environment: CacheEnvironment): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Host cache selection keeps platform conventions out of browser installation.
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
|
+
export function frameCacheDirectory(host, homeDirectory, environment) {
|
|
4
|
+
return cacheDirectory(host, homeDirectory, environment, "frames");
|
|
5
|
+
}
|
|
6
|
+
export function browserCacheDirectory(host, homeDirectory, environment) {
|
|
7
|
+
return cacheDirectory(host, homeDirectory, environment, "browser");
|
|
8
|
+
}
|
|
9
|
+
function cacheDirectory(host, homeDirectory, environment, leaf) {
|
|
10
|
+
switch (host.platform) {
|
|
11
|
+
case "darwin":
|
|
12
|
+
return posix.join(homeDirectory, "Library", "Caches", "onmark", leaf);
|
|
13
|
+
case "linux":
|
|
14
|
+
return posix.join(environment.xdgCacheHome ?? posix.join(homeDirectory, ".cache"), "onmark", leaf);
|
|
15
|
+
case "win32":
|
|
16
|
+
return win32.join(environment.localAppData ??
|
|
17
|
+
win32.join(homeDirectory, "AppData", "Local"), "onmark", leaf);
|
|
18
|
+
default:
|
|
19
|
+
return posix.join(homeDirectory, ".onmark", leaf);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type CacheEnvironment } from "./cache.js";
|
|
2
|
+
import { type ReleaseHost } from "./release.js";
|
|
3
|
+
export interface CaptureEnvironmentOptions {
|
|
4
|
+
readonly cacheEnvironment: CacheEnvironment;
|
|
5
|
+
readonly fontDirectories?: readonly string[];
|
|
6
|
+
readonly homeDirectory: string;
|
|
7
|
+
readonly host: ReleaseHost;
|
|
8
|
+
readonly osRelease: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LocalCaptureEnvironment {
|
|
11
|
+
readonly cacheDirectory: string;
|
|
12
|
+
readonly seed: string;
|
|
13
|
+
}
|
|
14
|
+
/** Computes the release-owned seed consumed by the native capture boundary. */
|
|
15
|
+
export declare function captureEnvironment(options: CaptureEnvironmentOptions): Promise<LocalCaptureEnvironment>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Desktop capture identity commits local cache reuse to pixel-affecting host facts.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { lstat, readdir, readlink } from "node:fs/promises";
|
|
4
|
+
import { posix, win32 } from "node:path";
|
|
5
|
+
import { frameCacheDirectory } from "./cache.js";
|
|
6
|
+
import { BROWSER_BUILD, desktopTarget } from "./release.js";
|
|
7
|
+
const IDENTITY_DOMAIN = "onmark-desktop-capture-environment-v1";
|
|
8
|
+
const MAX_FONT_ENTRIES = 100_000;
|
|
9
|
+
/** Computes the release-owned seed consumed by the native capture boundary. */
|
|
10
|
+
export async function captureEnvironment(options) {
|
|
11
|
+
const target = desktopTarget(options.host);
|
|
12
|
+
const hash = createHash("sha256");
|
|
13
|
+
record(hash, IDENTITY_DOMAIN);
|
|
14
|
+
record(hash, options.host.platform);
|
|
15
|
+
record(hash, options.host.arch);
|
|
16
|
+
record(hash, options.osRelease);
|
|
17
|
+
record(hash, BROWSER_BUILD);
|
|
18
|
+
record(hash, target.browser);
|
|
19
|
+
record(hash, target.browserPlatform);
|
|
20
|
+
record(hash, target.sha256);
|
|
21
|
+
const directories = options.fontDirectories ??
|
|
22
|
+
systemFontDirectories(options.host, options.homeDirectory, options.cacheEnvironment);
|
|
23
|
+
const state = { entries: 0 };
|
|
24
|
+
for (const [index, directory] of directories.entries()) {
|
|
25
|
+
await scanFontTree(hash, state, directory, index, "");
|
|
26
|
+
}
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
cacheDirectory: frameCacheDirectory(options.host, options.homeDirectory, options.cacheEnvironment),
|
|
29
|
+
seed: `sha256:${hash.digest("hex")}`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// ── Font inventory
|
|
33
|
+
async function scanFontTree(hash, state, root, rootIndex, relative) {
|
|
34
|
+
const paths = optionsPath(root);
|
|
35
|
+
const directory = relative.length === 0 ? root : paths.join(root, relative);
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
record(hash, `unreadable:${rootIndex}:${relative}:${errorCode(error)}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
entries.sort((left, right) => compareNames(left.name, right.name));
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
state.entries += 1;
|
|
47
|
+
if (state.entries > MAX_FONT_ENTRIES) {
|
|
48
|
+
throw new Error(`font inventory exceeds the ${MAX_FONT_ENTRIES}-entry capture limit`);
|
|
49
|
+
}
|
|
50
|
+
const child = relative.length === 0 ? entry.name : paths.join(relative, entry.name);
|
|
51
|
+
const absolute = paths.join(root, child);
|
|
52
|
+
let metadata;
|
|
53
|
+
try {
|
|
54
|
+
metadata = await lstat(absolute, { bigint: true });
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
record(hash, `missing:${rootIndex}:${portable(child)}:${errorCode(error)}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const identity = [
|
|
61
|
+
fontEntryKind(metadata),
|
|
62
|
+
rootIndex,
|
|
63
|
+
portable(child),
|
|
64
|
+
metadata.size,
|
|
65
|
+
metadata.mtimeNs,
|
|
66
|
+
];
|
|
67
|
+
record(hash, identity.join(":"));
|
|
68
|
+
if (metadata.isDirectory()) {
|
|
69
|
+
await scanFontTree(hash, state, root, rootIndex, child);
|
|
70
|
+
}
|
|
71
|
+
else if (metadata.isSymbolicLink()) {
|
|
72
|
+
try {
|
|
73
|
+
record(hash, `target:${await readlink(absolute)}`);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
record(hash, `target-unreadable:${errorCode(error)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function fontEntryKind(metadata) {
|
|
82
|
+
if (metadata.isDirectory()) {
|
|
83
|
+
return "directory";
|
|
84
|
+
}
|
|
85
|
+
if (metadata.isSymbolicLink()) {
|
|
86
|
+
return "symlink";
|
|
87
|
+
}
|
|
88
|
+
return "file";
|
|
89
|
+
}
|
|
90
|
+
function systemFontDirectories(host, homeDirectory, environment) {
|
|
91
|
+
switch (host.platform) {
|
|
92
|
+
case "darwin":
|
|
93
|
+
return [
|
|
94
|
+
"/System/Library/Fonts",
|
|
95
|
+
"/Library/Fonts",
|
|
96
|
+
posix.join(homeDirectory, "Library", "Fonts"),
|
|
97
|
+
];
|
|
98
|
+
case "linux":
|
|
99
|
+
return [
|
|
100
|
+
"/usr/share/fonts",
|
|
101
|
+
"/usr/local/share/fonts",
|
|
102
|
+
posix.join(homeDirectory, ".local", "share", "fonts"),
|
|
103
|
+
posix.join(homeDirectory, ".fonts"),
|
|
104
|
+
];
|
|
105
|
+
case "win32": {
|
|
106
|
+
const windows = environment.windowsDirectory ?? "C:\\Windows";
|
|
107
|
+
return [
|
|
108
|
+
win32.join(windows, "Fonts"),
|
|
109
|
+
win32.join(environment.localAppData ??
|
|
110
|
+
win32.join(homeDirectory, "AppData", "Local"), "Microsoft", "Windows", "Fonts"),
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
default:
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ── Canonical values
|
|
118
|
+
function compareNames(left, right) {
|
|
119
|
+
if (left < right) {
|
|
120
|
+
return -1;
|
|
121
|
+
}
|
|
122
|
+
if (left > right) {
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
function optionsPath(root) {
|
|
128
|
+
return /^[A-Za-z]:\\/u.test(root) ? win32 : posix;
|
|
129
|
+
}
|
|
130
|
+
function portable(path) {
|
|
131
|
+
return path.replaceAll("\\", "/");
|
|
132
|
+
}
|
|
133
|
+
function record(hash, value) {
|
|
134
|
+
const bytes = Buffer.from(value, "utf8");
|
|
135
|
+
const length = Buffer.allocUnsafe(4);
|
|
136
|
+
length.writeUInt32BE(bytes.byteLength);
|
|
137
|
+
hash.update(length);
|
|
138
|
+
hash.update(bytes);
|
|
139
|
+
}
|
|
140
|
+
function errorCode(error) {
|
|
141
|
+
if (error instanceof Error && "code" in error) {
|
|
142
|
+
return String(error.code);
|
|
143
|
+
}
|
|
144
|
+
return "unknown";
|
|
145
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npm release boundary resolves product tools before delegating to native CLI.
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { homedir, release } from "node:os";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { captureEnvironment } from "./capture-environment.js";
|
|
8
|
+
import { platformArtifact } from "./platform.js";
|
|
9
|
+
import { runNative } from "./native.js";
|
|
10
|
+
try {
|
|
11
|
+
const host = { arch: process.arch, platform: process.platform };
|
|
12
|
+
const releasePackage = createRequire(new URL("../../../../package.json", import.meta.url));
|
|
13
|
+
const artifact = platformArtifact(host, createRequire(import.meta.url).resolve);
|
|
14
|
+
const browserProvisioner = fileURLToPath(new URL("./browser-command.js", import.meta.url));
|
|
15
|
+
const bundler = releasePackage.resolve("#onmark-bundler-command");
|
|
16
|
+
const localCapture = await captureEnvironment({
|
|
17
|
+
cacheEnvironment: cacheEnvironment(process.env),
|
|
18
|
+
homeDirectory: homedir(),
|
|
19
|
+
host,
|
|
20
|
+
osRelease: release(),
|
|
21
|
+
});
|
|
22
|
+
const result = await runNative(process.argv.slice(2), {
|
|
23
|
+
browserProvisioner,
|
|
24
|
+
bundler,
|
|
25
|
+
ffmpeg: artifact.ffmpeg,
|
|
26
|
+
ffprobe: artifact.ffprobe,
|
|
27
|
+
nativeCli: artifact.nativeCli,
|
|
28
|
+
node: process.execPath,
|
|
29
|
+
}, {
|
|
30
|
+
...process.env,
|
|
31
|
+
ONMARK_CAPTURE_ENVIRONMENT_SEED: localCapture.seed,
|
|
32
|
+
ONMARK_FRAME_CACHE: localCapture.cacheDirectory,
|
|
33
|
+
});
|
|
34
|
+
if (result.signal !== null) {
|
|
35
|
+
process.kill(process.pid, result.signal);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
process.exitCode = result.code ?? 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : "unknown launcher failure";
|
|
43
|
+
process.stderr.write(`onmark: ${message}\n`);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
}
|
|
46
|
+
function cacheEnvironment(environment) {
|
|
47
|
+
return {
|
|
48
|
+
...(environment["LOCALAPPDATA"] === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: { localAppData: environment["LOCALAPPDATA"] }),
|
|
51
|
+
...(environment["WINDIR"] === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { windowsDirectory: environment["WINDIR"] }),
|
|
54
|
+
...(environment["XDG_CACHE_HOME"] === undefined
|
|
55
|
+
? {}
|
|
56
|
+
: { xdgCacheHome: environment["XDG_CACHE_HOME"] }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type SpawnOptions } from "node:child_process";
|
|
2
|
+
export interface ProductTools {
|
|
3
|
+
readonly browserProvisioner: string;
|
|
4
|
+
readonly bundler: string;
|
|
5
|
+
readonly ffmpeg: string;
|
|
6
|
+
readonly ffprobe: string;
|
|
7
|
+
readonly nativeCli: string;
|
|
8
|
+
readonly node: string;
|
|
9
|
+
}
|
|
10
|
+
export interface NativeResult {
|
|
11
|
+
readonly code: number | null;
|
|
12
|
+
readonly signal: NodeJS.Signals | null;
|
|
13
|
+
}
|
|
14
|
+
export interface NativeInvocation {
|
|
15
|
+
readonly arguments: readonly string[];
|
|
16
|
+
readonly command: string;
|
|
17
|
+
readonly options: SpawnOptions;
|
|
18
|
+
}
|
|
19
|
+
export declare function nativeInvocation(arguments_: readonly string[], tools: ProductTools, environment: NodeJS.ProcessEnv): NativeInvocation;
|
|
20
|
+
export declare function runNative(arguments_: readonly string[], tools: ProductTools, environment: NodeJS.ProcessEnv): Promise<NativeResult>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Native process ownership preserves Rust CLI semantics and terminal behavior.
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
export function nativeInvocation(arguments_, tools, environment) {
|
|
4
|
+
const options = {
|
|
5
|
+
env: {
|
|
6
|
+
...environment,
|
|
7
|
+
ONMARK_BROWSER_PROVISIONER: tools.node,
|
|
8
|
+
ONMARK_BROWSER_PROVISIONER_ENTRY: tools.browserProvisioner,
|
|
9
|
+
ONMARK_BUNDLER: tools.node,
|
|
10
|
+
ONMARK_BUNDLER_ENTRY: tools.bundler,
|
|
11
|
+
ONMARK_FFMPEG: tools.ffmpeg,
|
|
12
|
+
ONMARK_FFPROBE: tools.ffprobe,
|
|
13
|
+
},
|
|
14
|
+
stdio: "inherit",
|
|
15
|
+
};
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
arguments: Object.freeze([...arguments_]),
|
|
18
|
+
command: tools.nativeCli,
|
|
19
|
+
options,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function runNative(arguments_, tools, environment) {
|
|
23
|
+
const invocation = nativeInvocation(arguments_, tools, environment);
|
|
24
|
+
const child = spawn(invocation.command, invocation.arguments, invocation.options);
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
child.once("error", reject);
|
|
27
|
+
child.once("exit", (code, signal) => {
|
|
28
|
+
resolve(Object.freeze({ code, signal }));
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReleaseHost } from "./release.js";
|
|
2
|
+
export { UnsupportedReleaseTargetError } from "./release.js";
|
|
3
|
+
export interface PlatformArtifact {
|
|
4
|
+
readonly ffmpeg: string;
|
|
5
|
+
readonly ffprobe: string;
|
|
6
|
+
readonly nativeCli: string;
|
|
7
|
+
readonly packageName: string;
|
|
8
|
+
}
|
|
9
|
+
type PackageResolver = (specifier: string) => string;
|
|
10
|
+
export declare class MissingPlatformArtifactError extends Error {
|
|
11
|
+
constructor(packageName: string, cause: unknown);
|
|
12
|
+
}
|
|
13
|
+
export declare function platformArtifact(host: ReleaseHost, resolvePackage: PackageResolver): PlatformArtifact;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Platform release artifacts keep native tools out of the portable npm facade.
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
|
+
import { desktopTarget } from "./release.js";
|
|
4
|
+
export { UnsupportedReleaseTargetError } from "./release.js";
|
|
5
|
+
export class MissingPlatformArtifactError extends Error {
|
|
6
|
+
constructor(packageName, cause) {
|
|
7
|
+
super(`Onmark's platform package ${packageName} is missing; reinstall onmark without omitting optional dependencies`, { cause });
|
|
8
|
+
this.name = "MissingPlatformArtifactError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function platformArtifact(host, resolvePackage) {
|
|
12
|
+
const target = desktopTarget(host);
|
|
13
|
+
const paths = host.platform === "win32" ? win32 : posix;
|
|
14
|
+
let packageRoot;
|
|
15
|
+
try {
|
|
16
|
+
packageRoot = paths.dirname(resolvePackage(`${target.packageName}/package.json`));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new MissingPlatformArtifactError(target.packageName, error);
|
|
20
|
+
}
|
|
21
|
+
const extension = host.platform === "win32" ? ".exe" : "";
|
|
22
|
+
const executable = (name) => paths.join(packageRoot, "bin", `${name}${extension}`);
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
ffmpeg: executable("ffmpeg"),
|
|
25
|
+
ffprobe: executable("ffprobe"),
|
|
26
|
+
nativeCli: executable("onmark"),
|
|
27
|
+
packageName: target.packageName,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Browser, BrowserPlatform } from "@puppeteer/browsers";
|
|
2
|
+
export interface ReleaseHost {
|
|
3
|
+
readonly arch: NodeJS.Architecture;
|
|
4
|
+
readonly platform: NodeJS.Platform;
|
|
5
|
+
}
|
|
6
|
+
export interface DesktopTarget {
|
|
7
|
+
readonly browser: Browser;
|
|
8
|
+
readonly browserPlatform: BrowserPlatform;
|
|
9
|
+
readonly packageName: string;
|
|
10
|
+
readonly sha256: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const BROWSER_BUILD: string;
|
|
13
|
+
export declare class UnsupportedReleaseTargetError extends Error {
|
|
14
|
+
constructor(host: ReleaseHost);
|
|
15
|
+
}
|
|
16
|
+
export declare function desktopTarget(host: ReleaseHost): DesktopTarget;
|