multiplane 0.3.6 → 0.3.8
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 +202 -202
- package/README.en.md +3 -0
- package/README.md +3 -0
- package/THIRD_PARTY_NOTICES.md +30 -30
- package/bin/multiplane +24 -14
- package/examples/alpha.html +18 -18
- package/examples/animation.html +17 -17
- package/examples/assets/OFL.txt +93 -93
- package/examples/assets/card.svg +6 -6
- package/examples/three-cards.html +17 -17
- package/examples/typography.html +17 -17
- package/licenses/GPL-3.0.txt +232 -232
- package/licenses/LGPL-3.0.txt +304 -304
- package/licenses/acorn-LICENSE +21 -21
- package/licenses/colour-LICENSE.md +82 -82
- package/licenses/detect-libc-LICENSE +201 -201
- package/licenses/entities-LICENSE +11 -11
- package/licenses/nanoid-LICENSE +20 -20
- package/licenses/parse5-LICENSE +19 -19
- package/licenses/picocolors-LICENSE +15 -15
- package/licenses/playwright-NOTICE +5 -5
- package/licenses/playwright-ThirdPartyNotices.txt +14 -14
- package/licenses/playwright-core-ThirdPartyNotices.txt +13 -13
- package/licenses/postcss-LICENSE +20 -20
- package/licenses/postcss-value-parser-LICENSE +22 -22
- package/licenses/semver-LICENSE +15 -15
- package/licenses/sharp-LICENSE +191 -191
- package/licenses/sharp-win32-x64-NOTICES.md +45 -45
- package/licenses/source-map-js-LICENSE +28 -28
- package/licenses/three-LICENSE +21 -21
- package/package.json +92 -92
- package/skills/multiplane/SKILL.md +3 -0
- package/src/alpha.mjs +69 -69
- package/src/artifacts.mjs +61 -61
- package/src/browser-recovery.mjs +26 -26
- package/src/capture-watchdog.mjs +29 -0
- package/src/capture.mjs +34 -18
- package/src/encoder.mjs +16 -16
- package/src/environment.mjs +56 -56
- package/src/errors.mjs +1 -0
- package/src/events.mjs +12 -10
- package/src/ffmpeg.mjs +32 -32
- package/src/format.mjs +27 -27
- package/src/frame-rate.mjs +28 -28
- package/src/home-migration.mjs +54 -54
- package/src/hosted.mjs +3 -3
- package/src/index.mjs +1 -0
- package/src/input-records.mjs +33 -0
- package/src/jobs.mjs +3 -1
- package/src/lint.mjs +248 -248
- package/src/output-alpha.mjs +36 -36
- package/src/output-check.mjs +110 -110
- package/src/platform.mjs +114 -114
- package/src/png-native.mjs +12 -12
- package/src/png.mjs +74 -74
- package/src/post.mjs +56 -39
- package/src/preview.mjs +23 -23
- package/src/process.mjs +9 -9
- package/src/render-pool.mjs +41 -14
- package/src/render-worker.mjs +14 -3
- package/src/render.mjs +148 -52
- package/src/resource-path.mjs +29 -29
- package/src/resources.mjs +27 -27
- package/src/resume.mjs +23 -0
- package/src/runtime/camera.js +37 -37
- package/src/runtime/shot.css +9 -9
- package/src/segment-encoder.mjs +91 -0
- package/src/segments.mjs +123 -0
- package/src/server.mjs +11 -7
- package/src/setup.mjs +51 -51
- package/src/skills.mjs +128 -128
- package/src/stills.mjs +1 -1
- package/src/three/index.js +39 -31
- package/src/verify.mjs +2 -2
- package/src/video-pipe.mjs +6 -6
- package/tools/bench/shots/canvas-2d.html +1 -1
- package/tools/bench/shots/dom-light.html +1 -1
- package/tools/bench/shots/scene.js +44 -44
- package/tools/bench/shots/webgl.html +1 -1
- package/tools/install-browser.mjs +18 -18
package/src/home-migration.mjs
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { lstatSync, mkdirSync, openSync, closeSync, copyFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
5
|
-
import { userHome } from './platform.mjs';
|
|
6
|
-
|
|
7
|
-
const present = file => {
|
|
8
|
-
try { lstatSync(file); return true; }
|
|
9
|
-
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// Use the writers' locks, including the config lock introduced in 038.
|
|
13
|
-
// Publish only complete copies, and recheck the destination under the lock.
|
|
14
|
-
export function migrateUserHome(env = process.env, platform = process.platform, home = os.homedir()) {
|
|
15
|
-
const result = { migrated: [], warnings: [] };
|
|
16
|
-
if (platform !== 'win32' || env.MULTIPLANE_HOME) return result;
|
|
17
|
-
const directory = userHome(env, platform, home);
|
|
18
|
-
const previous = path.win32.join(env.APPDATA || path.win32.join(env.USERPROFILE || home, 'AppData', 'Roaming'), 'multiplane');
|
|
19
|
-
if (path.win32.resolve(previous).toLowerCase() === path.win32.resolve(directory).toLowerCase()) return result;
|
|
20
|
-
for (const [lockName, names] of [['config.json.lock', ['config.json']], ['jobs.lock', ['jobs.jsonl', 'jobs.1.jsonl']]]) {
|
|
21
|
-
let handle;
|
|
22
|
-
const lock = path.join(directory, lockName);
|
|
23
|
-
try {
|
|
24
|
-
if (!names.some(name => !present(path.join(directory, name)) && present(path.join(previous, name)))) continue;
|
|
25
|
-
mkdirSync(directory, { recursive: true });
|
|
26
|
-
const deadline = performance.now() + 10000;
|
|
27
|
-
for (;;) {
|
|
28
|
-
try { handle = openSync(lock, 'wx'); break; }
|
|
29
|
-
catch (error) {
|
|
30
|
-
if (error.code !== 'EEXIST' || performance.now() >= deadline) throw error;
|
|
31
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
for (const name of names) {
|
|
35
|
-
const target = path.join(directory, name), source = path.join(previous, name);
|
|
36
|
-
if (present(target) || !present(source)) continue;
|
|
37
|
-
const temporary = target + '.' + randomUUID() + '.tmp';
|
|
38
|
-
try {
|
|
39
|
-
copyFileSync(source, temporary);
|
|
40
|
-
renameSync(temporary, target);
|
|
41
|
-
result.migrated.push({ name, source });
|
|
42
|
-
} finally { try { unlinkSync(temporary); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
result.warnings.push({ code: 'home_unavailable', message: 'User directory migration could not be completed.' });
|
|
46
|
-
} finally {
|
|
47
|
-
if (handle !== undefined) {
|
|
48
|
-
closeSync(handle);
|
|
49
|
-
try { unlinkSync(lock); } catch { result.warnings.push({ code: 'home_unavailable', message: 'User directory migration lock could not be removed.' }); }
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { lstatSync, mkdirSync, openSync, closeSync, copyFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
5
|
+
import { userHome } from './platform.mjs';
|
|
6
|
+
|
|
7
|
+
const present = file => {
|
|
8
|
+
try { lstatSync(file); return true; }
|
|
9
|
+
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Use the writers' locks, including the config lock introduced in 038.
|
|
13
|
+
// Publish only complete copies, and recheck the destination under the lock.
|
|
14
|
+
export function migrateUserHome(env = process.env, platform = process.platform, home = os.homedir()) {
|
|
15
|
+
const result = { migrated: [], warnings: [] };
|
|
16
|
+
if (platform !== 'win32' || env.MULTIPLANE_HOME) return result;
|
|
17
|
+
const directory = userHome(env, platform, home);
|
|
18
|
+
const previous = path.win32.join(env.APPDATA || path.win32.join(env.USERPROFILE || home, 'AppData', 'Roaming'), 'multiplane');
|
|
19
|
+
if (path.win32.resolve(previous).toLowerCase() === path.win32.resolve(directory).toLowerCase()) return result;
|
|
20
|
+
for (const [lockName, names] of [['config.json.lock', ['config.json']], ['jobs.lock', ['jobs.jsonl', 'jobs.1.jsonl']]]) {
|
|
21
|
+
let handle;
|
|
22
|
+
const lock = path.join(directory, lockName);
|
|
23
|
+
try {
|
|
24
|
+
if (!names.some(name => !present(path.join(directory, name)) && present(path.join(previous, name)))) continue;
|
|
25
|
+
mkdirSync(directory, { recursive: true });
|
|
26
|
+
const deadline = performance.now() + 10000;
|
|
27
|
+
for (;;) {
|
|
28
|
+
try { handle = openSync(lock, 'wx'); break; }
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== 'EEXIST' || performance.now() >= deadline) throw error;
|
|
31
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const name of names) {
|
|
35
|
+
const target = path.join(directory, name), source = path.join(previous, name);
|
|
36
|
+
if (present(target) || !present(source)) continue;
|
|
37
|
+
const temporary = target + '.' + randomUUID() + '.tmp';
|
|
38
|
+
try {
|
|
39
|
+
copyFileSync(source, temporary);
|
|
40
|
+
renameSync(temporary, target);
|
|
41
|
+
result.migrated.push({ name, source });
|
|
42
|
+
} finally { try { unlinkSync(temporary); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
result.warnings.push({ code: 'home_unavailable', message: 'User directory migration could not be completed.' });
|
|
46
|
+
} finally {
|
|
47
|
+
if (handle !== undefined) {
|
|
48
|
+
closeSync(handle);
|
|
49
|
+
try { unlinkSync(lock); } catch { result.warnings.push({ code: 'home_unavailable', message: 'User directory migration lock could not be removed.' }); }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
package/src/hosted.mjs
CHANGED
|
@@ -18,7 +18,7 @@ async function move(from, to) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export async function hostedAvailable(command, options, env = process.env) {
|
|
21
|
-
if (!['render', 'stills', 'verify'].includes(command) || !options._hostedCLI || options.hosted !== true
|
|
21
|
+
if (!['render', 'stills', 'verify', 'resume'].includes(command) || !options._hostedCLI || options.hosted !== true
|
|
22
22
|
|| options['no-hosted'] || options['hosted-executor'] || env.MULTIPLANE_HOSTED === '0') return false;
|
|
23
23
|
try {
|
|
24
24
|
const app = await json(path.join(userHome(env), 'app.json'));
|
|
@@ -28,7 +28,7 @@ export async function hostedAvailable(command, options, env = process.env) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function hostedEnvironment(env) {
|
|
31
|
-
const allowed = /^(PATH|MULTIPLANE_.*|ELECTRON_RUN_AS_NODE|HTTPS?_PROXY|ALL_PROXY|NO_PROXY|TEMP|TMP|TMPDIR|HOME|USERPROFILE|APPDATA|LOCALAPPDATA|SystemRoot|ComSpec|PATHEXT|LANG|LC_.*)$/i;
|
|
31
|
+
const allowed = /^(PATH|MULTIPLANE_.*|ELECTRON_RUN_AS_NODE|HTTPS?_PROXY|ALL_PROXY|NO_PROXY|TEMP|TMP|TMPDIR|HOME|USERPROFILE|APPDATA|LOCALAPPDATA|SystemRoot|ComSpec|PATHEXT|LANG|LC_.*)$/i;
|
|
32
32
|
const secret = /TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH|BEARER|COOKIE|SESSION|API_?KEY|PRIVATE_?KEY|ACCESS_?KEY/i;
|
|
33
33
|
return Object.fromEntries(Object.entries(env).filter(([key, value]) => {
|
|
34
34
|
if (value === undefined || !allowed.test(key) || secret.test(key)) return false;
|
|
@@ -84,7 +84,7 @@ export async function handoff(request, { home = userHome(), timeout = 5000, star
|
|
|
84
84
|
// Claim before loading config, creating status, linting or opening output files.
|
|
85
85
|
export async function claimExecutor(jobDir, command) {
|
|
86
86
|
try {
|
|
87
|
-
if (!path.isAbsolute(jobDir) || !['render', 'stills', 'verify'].includes(command)) return false;
|
|
87
|
+
if (!path.isAbsolute(jobDir) || !['render', 'stills', 'verify', 'resume'].includes(command)) return false;
|
|
88
88
|
const jobId = path.basename(jobDir), base = hostedBase(jobId);
|
|
89
89
|
const request = await json(base + '.claimed.json');
|
|
90
90
|
if (request?.schema !== schema || request.jobId !== jobId || request.command !== command
|
package/src/index.mjs
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream } from 'node:fs';
|
|
4
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
5
|
+
|
|
6
|
+
// Servers inherit the render's context; unrelated inspect/serve calls do not.
|
|
7
|
+
export const inputRecords = new AsyncLocalStorage();
|
|
8
|
+
const largeFiles = new WeakMap();
|
|
9
|
+
export const digest = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
export async function fileDigest(file) {
|
|
11
|
+
const hash = createHash('sha256');
|
|
12
|
+
for await (const bytes of createReadStream(file)) hash.update(bytes);
|
|
13
|
+
return hash.digest('hex');
|
|
14
|
+
}
|
|
15
|
+
export async function recordInput(file) {
|
|
16
|
+
const record = inputRecords.getStore();
|
|
17
|
+
if (!record) return;
|
|
18
|
+
const info = await stat(file);
|
|
19
|
+
// Large media stays streamed. Cache only its digest, never its body.
|
|
20
|
+
if (info.size > 1024 * 1024) {
|
|
21
|
+
let cache = largeFiles.get(record);
|
|
22
|
+
if (!cache) largeFiles.set(record, cache = new Map());
|
|
23
|
+
const previous = cache.get(file);
|
|
24
|
+
const sha256 = previous?.size === info.size && previous.mtimeMs === info.mtimeMs
|
|
25
|
+
? previous.sha256 : await fileDigest(file);
|
|
26
|
+
cache.set(file, { size: info.size, mtimeMs: info.mtimeMs, sha256 });
|
|
27
|
+
record(file, { bytes: info.size, sha256 });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const bytes = await readFile(file);
|
|
31
|
+
record(file, { bytes: bytes.length, sha256: digest(bytes) });
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
package/src/jobs.mjs
CHANGED
|
@@ -6,8 +6,9 @@ import { MultiplaneError, invalidOption } from './errors.mjs';
|
|
|
6
6
|
import { number } from './capture.mjs';
|
|
7
7
|
import { followJob } from './hosted.mjs';
|
|
8
8
|
import { loadUserConfig } from './user-home.mjs';
|
|
9
|
+
import { resumeStatus } from './segments.mjs';
|
|
9
10
|
|
|
10
|
-
const resolveJob = (target, options) => path.isAbsolute(target) || /[\\/]/.test(target) ? path.resolve(target) : path.join(jobDirectory(options), target);
|
|
11
|
+
export const resolveJob = (target, options) => path.isAbsolute(target) || /[\\/]/.test(target) ? path.resolve(target) : path.join(jobDirectory(options), target);
|
|
11
12
|
export async function readStatus(target, options = {}) {
|
|
12
13
|
let status;
|
|
13
14
|
try { status = JSON.parse(await readFile(path.join(resolveJob(target, options), 'status.json'), 'utf8')); }
|
|
@@ -25,6 +26,7 @@ export async function readStatus(target, options = {}) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
if (status.command === 'render') status.resume = resumeStatus(status, resolveJob(target, options));
|
|
28
30
|
return status;
|
|
29
31
|
}
|
|
30
32
|
export const statusJob = (target, options = {}) => runCommand('status', null, options, events => readStatus(target, events.options), false);
|