multiplane 0.3.0 → 0.3.1

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/README.en.md CHANGED
@@ -48,9 +48,11 @@ Requires Node.js 24 or later and FFmpeg with libx264 support.
48
48
  npm install multiplane
49
49
  npx playwright install chromium
50
50
  npx multiplane doctor
51
- npx multiplane render node_modules/multiplane/examples/animation.html
51
+ npx multiplane render examples/animation.html --root node_modules/multiplane
52
52
  ```
53
53
 
54
+ The examples live inside the installed package, so `--root` points the resource root at the package; for safety, renders never serve files from under `node_modules`.
55
+
54
56
  Recent npm versions do not run install scripts of dependencies, so the second command downloads the Chromium build that Playwright uses. To work from a clone of this repository instead, run `npm install` in it and render `examples/animation.html` directly.
55
57
 
56
58
  Give your agent the usage skill with `npx multiplane skill install --host all` (Claude Code: `~/.claude/skills/multiplane`, Codex: `~/.agents/skills/multiplane`), or let the desktop app install it. Start a new agent session afterwards.
package/README.md CHANGED
@@ -48,9 +48,11 @@
48
48
  npm install multiplane
49
49
  npx playwright install chromium
50
50
  npx multiplane doctor
51
- npx multiplane render node_modules/multiplane/examples/animation.html
51
+ npx multiplane render examples/animation.html --root node_modules/multiplane
52
52
  ```
53
53
 
54
+ 示例在安装好的包里,所以用 `--root` 把资源根指向包目录;出于安全,渲染时不会提供 `node_modules` 下的文件。
55
+
54
56
  新版 npm 默认不运行依赖包的安装脚本,所以第二条命令用来下载 Playwright 使用的 Chromium。如果是克隆了本仓库,在仓库里运行 `npm install`,直接渲染 `examples/animation.html` 即可。
55
57
 
56
58
  把使用说明 skill 交给你的 agent:运行 `npx multiplane skill install --host all`(Claude Code 装到 `~/.claude/skills/multiplane`,Codex 装到 `~/.agents/skills/multiplane`),或者让桌面软件来安装。装好后新开一个 agent 会话。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multiplane",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
package/src/capture.mjs CHANGED
@@ -35,6 +35,10 @@ async function openShotOnce(shot, scale = 1, options = {}) {
35
35
  relative = path.relative(root, await normalizeResourcePath(entry));
36
36
  }
37
37
  if (path.extname(relative).toLowerCase() !== '.html') throw invalidOption('shot', 'Shot must be an HTML file.');
38
+ if (relative.split(path.sep).includes('node_modules')) throw new MultiplaneError('shot_outside_root', undefined, {
39
+ details: { path: relative },
40
+ hint: 'Files under node_modules are not served. Use --root to point to the package directory (for example, --root node_modules/multiplane).',
41
+ });
38
42
  try { await access(entry); } catch { throw new MultiplaneError('shot_not_found'); }
39
43
  const server = await startServer(0, root);
40
44
  let browser, page, crashed = false;
@@ -42,7 +46,7 @@ async function openShotOnce(shot, scale = 1, options = {}) {
42
46
  browser = await launchBrowser(options);
43
47
  await options.onBrowser?.(browser);
44
48
  page = await browser.newPage({ viewport: { width: 1920, height: 1080 }, deviceScaleFactor: scale });
45
- if (reuseEnabled(options)) await page.addInitScript(installStaticObserver);
49
+ if (reuseEnabled(options)) await page.addInitScript(installStaticObserver, { platform: process.platform });
46
50
  page.on('crash', () => { crashed = true; });
47
51
  const errors = [];
48
52
  const observerProbe = request => {
@@ -57,8 +61,12 @@ async function openShotOnce(shot, scale = 1, options = {}) {
57
61
  page.on('response', response => { if (response.status() >= 400 && !observerProbe(response.request())) errors.push(new MultiplaneError(response.headers()['x-multiplane-error'] === 'three_not_installed' ? 'three_not_installed' : 'resource_failed', undefined, { details: { status: response.status(), url: response.url() } })); });
58
62
  page.on('requestfailed', request => { if (!observerProbe(request)) errors.push(new MultiplaneError('resource_failed', undefined, { details: { url: request.url() } })); });
59
63
  const url = `${server.url}/${relative.split(path.sep).map(encodeURIComponent).join('/')}?mode=render`;
60
- const response = await page.goto(url).catch(cause => { throw new MultiplaneError('shot_load_failed', undefined, { cause }); });
61
- if (!response.ok()) throw new MultiplaneError('shot_load_failed');
64
+ const response = await page.goto(url).catch(cause => {
65
+ const failedResponse = errors.find(error => error.details?.url === url && error.details.status);
66
+ throw new MultiplaneError('shot_load_failed', undefined, { cause,
67
+ details: { path: relative, url, status: failedResponse?.details.status ?? null, reason: cause.message } });
68
+ });
69
+ if (!response?.ok()) throw new MultiplaneError('shot_load_failed', undefined, { details: { path: relative, url, status: response?.status() ?? null } });
62
70
  if (errors.length) throw errors[0];
63
71
  if (!await page.evaluate(() => typeof window.SHOT?.render === 'function')) throw new MultiplaneError('shot_contract_invalid');
64
72
  const metadata = await page.evaluate(async () => {
@@ -47,7 +47,7 @@ export async function* verifiedFrames(source, options, count, { owned = false }
47
47
  }
48
48
 
49
49
  // Runs before any shot script, so saved drawing functions are instrumented too.
50
- export function installStaticObserver() {
50
+ export function installStaticObserver({ platform } = {}) {
51
51
  let dirty = true, suspended = false, unsafe = false, styleSignature, stylesDirty = true, stylesUnsafe = false, previousSheets = [], readingStyles = false;
52
52
  const exposedDeclarations = new Map();
53
53
  let reason = null;
@@ -248,6 +248,8 @@ export function installStaticObserver() {
248
248
  finally { readingStyles = false; }
249
249
  };
250
250
  const uncertain = () => unsafe ? 'unobservable_api' : !hooks.every(check => check()) ? 'hook_replaced'
251
+ // Native macOS textarea scrollbars can animate outside observable DOM/CSS state.
252
+ : platform === 'darwin' && document.querySelector('textarea') ? 'unobservable_api'
251
253
  : document.querySelector('video,iframe,object,embed,marquee,animate,animateTransform,animateMotion,set') ? 'dynamic_element'
252
254
  : document.activeElement?.matches('input,textarea,[contenteditable]:not([contenteditable="false"])') ? 'editable_focus'
253
255
  : document.getAnimations().some(animation => animation.playState === 'running' || (animation.pending && animation.playState !== 'paused')) || document.fonts.status !== 'loaded' ? 'animation_or_font'