From e3a381c92340a417bd540ac73ed659b5eeaf26c3 Mon Sep 17 00:00:00 2001 From: YukiWorks432 Date: Sun, 21 Jun 2026 08:09:04 +0900 Subject: [PATCH 1/4] =?UTF-8?q?ScriptUI=20=E3=81=AE=20this=20=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E3=81=A8=E4=BE=8B=E5=A4=96=E5=87=A6=E7=90=86=E3=82=92?= =?UTF-8?q?=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rollup.config.mjs | 1 + src/lib/lib.ts | 59 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/rollup.config.mjs b/rollup.config.mjs index 046f488..b5c5fe9 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -294,6 +294,7 @@ export default (commandLineArgs) => { output: { file: `${outDir}/${script.name}.jsx`, format: "cjs", + strict: false, }, context: "this", onwarn, diff --git a/src/lib/lib.ts b/src/lib/lib.ts index 460a5d8..aa3a7b7 100644 --- a/src/lib/lib.ts +++ b/src/lib/lib.ts @@ -18,20 +18,52 @@ export const getLineFromString = ( }; /*** - * jp: エラー内容をアラート表示する.\ - * en: Display an alert with the error details. + * jp: エラー内容をダイアログ表示する.\ + * en: Display a dialog with the error details. */ -export const alertError = (error: Error) => { +const getErrorMessage = (error: Error): string => { try { const line = getLineFromString(error.source, error.line)?.trim() || ""; let lineSuffix = ""; - if (line.length < 200) lineSuffix = `">":\ ${line}`; - alert(`Error: ${error.message}\nLine: ${error.line}\n${lineSuffix}`); + if (line.length < 200) lineSuffix = `">": ${line}`; + return `Error: ${error.message}\nLine: ${error.line}\n${lineSuffix}`; } catch { - alert(`Error: ${error.message}`); + return `Error: ${error.message}`; } }; +type ScriptUIWindowLike = { + alignChildren: string | string[]; + margins: number; + add: (...args: unknown[]) => unknown; + show: () => number; +}; + +type ScriptUIWindowConstructor = new ( + kind: string, + title?: string +) => ScriptUIWindowLike; + +const createScriptUIWindow = ( + kind: string, + title: string +): ScriptUIWindowLike => + new (Window as unknown as ScriptUIWindowConstructor)(kind, title); + +const showErrorDialog = (message: string): void => { + const dialog = createScriptUIWindow("dialog", "Error"); + dialog.alignChildren = ["fill", "top"]; + dialog.margins = 12; + dialog.add("statictext", undefined, message, { multiline: true }); + dialog.add("button", undefined, "OK", { name: "ok" }); + dialog.show(); +}; + +export const alertError = (error: Error): never => { + showErrorDialog(getErrorMessage(error)); + throw error; +}; + /*** * jp: 指定された処理をUndoグループ化して実行する.\ * en: Execute the specified process in an undo group. @@ -43,9 +75,9 @@ export const entry = (name: string, func: () => any) => { func(); } catch (e) { alertError(e as Error); + } finally { + if (app.endUndoGroup) app.endUndoGroup(); } - - if (app.endUndoGroup) app.endUndoGroup(); }; /*** @@ -64,19 +96,20 @@ export const entryUI = ( func: (win: Window | Panel) => void ): void => { var win: Window | Panel; + var paletteWindow: ScriptUIWindowLike | null = null; if (thisObj instanceof Panel) { win = thisObj; } else { - win = new Window("palette", name); + paletteWindow = createScriptUIWindow("palette", name); + win = paletteWindow as unknown as Window; } try { func(win); + if (paletteWindow) { + paletteWindow.show(); + } } catch (e) { alertError(e as Error); } - - if (win instanceof Window) { - win.show(); - } }; From 6efa9b484488ca2378a3a689b30d284f8a3d24fa Mon Sep 17 00:00:00 2001 From: YukiWorks432 Date: Sun, 21 Jun 2026 08:09:18 +0900 Subject: [PATCH 2/4] =?UTF-8?q?add-app=20=E3=81=AE=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=82=92=E5=8E=B3=E6=A0=BC=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/guides/getting-started.md | 8 +- docs/project-overview.md | 22 +++- package.json | 2 +- scripts/addApp.mjs | 220 +++++++++++++++++++++++---------- scripts/newScript.mjs | 2 +- 6 files changed, 180 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 9cbbb38..d38cfb3 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ shimの限界として、正しく動作しないものはeslintによってエ | Photoshop | `phxs` | `src/phxs/` | `pnpm add-app -- --app=` で新しいアプリを追加できます。 +追加できるのは `aeft`, `ilst`, `phxs`, `idsn`, `ppro`, `anmt`, `audt` の正式対応アプリのみです。 ## 環境 / Environment diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index e92e51a..58acd08 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -80,6 +80,8 @@ pnpm build --app=aeft pnpm add-app -- --app=idsn ``` +追加できるのは `aeft`, `ilst`, `phxs`, `idsn`, `ppro`, `anmt`, `audt` の正式対応アプリのみです。 + ## ディレクトリ構造 ``` @@ -112,7 +114,8 @@ entry("MyScript", () => { #### `entryUI` — ScriptUI パネル対応スクリプト用 ドッキングパネルとして使う場合は `entryUI` と `__ES_THIS__` を使います。 -`__ES_THIS__` はビルド時にバンドル先頭へ自動注入されるグローバルな `this` です。 +`__ES_THIS__` はビルド時にバンドル先頭へ `var __ES_THIS__=this;` として自動注入されるグローバルな `this` です。 +Rollup 出力は `"use strict";` を出さない設定にしているため、ScriptUI パネル起動時の `this` を保持できます。 ```ts import { entryUI } from "../../lib/lib"; @@ -128,6 +131,9 @@ entryUI("MyScript", __ES_THIS__, (win) => { | スクリプトとして実行 | グローバルオブジェクト | `new Window("palette")` を生成して表示 | | ドッキングパネルとして起動 | `Panel` | 渡された `Panel` をそのまま使用 | +`entryUI` は UI 構築中や palette 表示時の例外をエラーダイアログで表示し、同じ例外を再送出します。 +`onClick` などのイベントハンドラ内で処理を行う場合は、処理本体を `entry()` で囲んでください。 + ```ts import { entry, alertError } from "../../lib/lib"; ``` diff --git a/docs/project-overview.md b/docs/project-overview.md index 622bd29..657e2f9 100644 --- a/docs/project-overview.md +++ b/docs/project-overview.md @@ -132,9 +132,12 @@ entryUI("MyScript", __ES_THIS__, (win) => { }); ``` -`__ES_THIS__` はビルド時にバンドル先頭へ自動注入されるグローバルな `this` で、 -Extension Manager / Dockable パネルとして起動された場合は `Panel`、 -スクリプトとして実行された場合はグローバルオブジェクトを返す。 +`__ES_THIS__` はビルド時にバンドル先頭へ `var __ES_THIS__=this;` として自動注入される。 +Rollup 出力は `"use strict";` を出さない設定にしているため、After Effects の ScriptUI パネルとして起動された場合は `Panel`、 +スクリプトとして直接実行された場合はグローバルオブジェクトを保持する。 + +通常の手書き JSX であれば `createUI(this)` のように直接 `this` を渡せる。 +このテンプレートでは TypeScript / Rollup / Terser の変換後も同じ値を保つため、出力先頭で退避した `__ES_THIS__` を `entryUI` に渡す。 ### 予約名 @@ -159,7 +162,8 @@ entry("MyScript", () => { ``` - 実行時に Undo グループを作成し、完了後に閉じる -- 例外が発生した場合は `alertError()` でエラーダイアログを表示する +- 例外が発生した場合は `alertError()` でエラーダイアログを表示し、同じ例外を再送出する +- 例外が発生しても Undo グループは必ず閉じる ### `entryUI` — ScriptUI ウィンドウ用 @@ -179,6 +183,7 @@ entryUI("MyScript", __ES_THIS__, (win) => { ``` - `entryUI` はウィンドウ・パネルの UI を組み立てるための関数で、Undo グループは作らない +- UI 構築中や palette 表示時の例外はエラーダイアログを表示し、同じ例外を再送出する - **`onClick` などのイベントハンドラ内で処理を行う場合は、必ず `entry` で囲むこと** - こうすることで処理ごとに Undo グループが作られ、エラーハンドリングも機能する @@ -188,13 +193,18 @@ entryUI("MyScript", __ES_THIS__, (win) => { pnpm add-app -- --app=idsn ``` +追加できるのは `aeft`, `ilst`, `phxs`, `idsn`, `ppro`, `anmt`, `audt` の正式対応アプリのみ。 +任意のアプリID追加は未対応。 + 実行すると以下を自動生成: -- `src/{app}/tsconfig.json`(types-for-adobe のマッピング付き) +- `src/{app}/tsconfig.json`(types-for-adobe と ScriptUI 共通型のマッピング付き) - `src/{app}/types/index.d.ts` - `src/{app}/lib/.gitkeep` - `src/{app}/example/index.ts` -- `es.config.mjs` に `scripts.{app}` キーを追加 +- `es.config.mjs` に `scripts.{app}` キーを追加し、`example` を `build: true`, `license: true` で登録 + +既存の `src/{appId}` または `es.config.mjs` の `scripts.{appId}` と衝突する場合は、ファイルを生成せずに停止する。 ## 開発コマンド diff --git a/package.json b/package.json index a569d2b..f8f4a43 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "watch": "rollup -c -w", "lint": "eslint .", "format": "prettier --write .", - "new": "node ./scripts/newScript.mjs&&prettier --write ./es.config.mjs", + "new": "node ./scripts/newScript.mjs", "add-app": "node ./scripts/addApp.mjs" }, "keywords": [ diff --git a/scripts/addApp.mjs b/scripts/addApp.mjs index 45aa871..b97f3c3 100644 --- a/scripts/addApp.mjs +++ b/scripts/addApp.mjs @@ -7,7 +7,8 @@ import { createInterface } from "node:readline"; import { stdin as input, stdout as output } from "node:process"; import { parseArgs } from "node:util"; -import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { constants } from "node:fs"; +import { access, readFile, writeFile, mkdir } from "node:fs/promises"; import { execSync } from "node:child_process"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -18,36 +19,42 @@ const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, ".."); const ES_CONFIG_PATH = path.resolve(projectRoot, "es.config.mjs"); const SRC_DIR = path.resolve(projectRoot, "src"); +const EXAMPLE_SCRIPT = { + name: "example", + version: "0.0.1", + build: true, + license: true, +}; // types-for-adobe のアプリID → ディレクトリ名マッピング const APP_TYPES_MAP = { aeft: { dir: "AfterEffects/22.0", - shared: ["shared/XMPScript"], + shared: ["shared/ScriptUI", "shared/XMPScript"], }, ilst: { dir: "Illustrator/2022", - shared: [], + shared: ["shared/ScriptUI"], }, phxs: { dir: "Photoshop/2015.5", - shared: [], + shared: ["shared/ScriptUI"], }, idsn: { dir: "InDesign/2022", - shared: [], + shared: ["shared/ScriptUI"], }, ppro: { dir: "Premiere/24.0", - shared: [], + shared: ["shared/ScriptUI"], }, anmt: { dir: "Animate/22.0", - shared: [], + shared: ["shared/ScriptUI"], }, audt: { dir: "Audition/2018", - shared: [], + shared: ["shared/ScriptUI"], }, }; @@ -69,14 +76,24 @@ const LOCALE = detectLocale(); const I18N = { ja: { prompt: { - enterApp: (known) => - `追加するアプリのIDを入力してください (例: ${known.join(", ")}): `, + selectApp: (known) => + `追加するアプリを番号で選択してください (${known.join(", ")}): `, }, error: { emptyApp: "エラー: アプリIDを入力してください。", - exists: (app) => `エラー: ${app} は既に存在します。`, - unknownType: (app) => - `警告: ${app} の types-for-adobe マッピングが不明です。tsconfig.json を手動で設定してください。`, + invalidArgs: + "エラー: 引数が不正です。pnpm add-app -- --app= を使用してください。", + invalidSelection: (value, count) => + `エラー: ${value} は無効な選択です。1 から ${count} の番号を入力してください。`, + unsupportedApp: (app, known) => + `エラー: 未対応のアプリIDです: ${app} (対応: ${known.join(", ")})`, + srcExists: (app) => `エラー: src/${app} は既に存在します。`, + configExists: (app) => + `エラー: es.config.mjs に scripts.${app} が既に存在します。`, + scriptsNotFound: + "エラー: es.config.mjs の scripts を特定できませんでした。", + configReadFailed: + "エラー: es.config.mjs の読み込みに失敗しました。ファイルの構文を確認してください。", unexpected: "予期せぬエラーが発生しました:", }, done: (app) => `完了: src/${app}/ のスキャフォールディングが完了しました。`, @@ -84,14 +101,23 @@ const I18N = { }, en: { prompt: { - enterApp: (known) => - `Enter the app ID to add (e.g. ${known.join(", ")}): `, + selectApp: (known) => + `Select the app to add by number (${known.join(", ")}): `, }, error: { emptyApp: "Error: Please enter an app ID.", - exists: (app) => `Error: ${app} already exists.`, - unknownType: (app) => - `Warning: Unknown types-for-adobe mapping for ${app}. Please configure tsconfig.json manually.`, + invalidArgs: + "Error: Invalid arguments. Use pnpm add-app -- --app=.", + invalidSelection: (value, count) => + `Error: ${value} is not a valid selection. Enter a number from 1 to ${count}.`, + unsupportedApp: (app, known) => + `Error: Unsupported app ID: ${app} (supported: ${known.join(", ")})`, + srcExists: (app) => `Error: src/${app} already exists.`, + configExists: (app) => + `Error: scripts.${app} already exists in es.config.mjs.`, + scriptsNotFound: "Error: Could not locate scripts in es.config.mjs.", + configReadFailed: + "Error: Failed to load es.config.mjs. Please verify the file syntax.", unexpected: "An unexpected error occurred:", }, done: (app) => `Done: Scaffolding for src/${app}/ complete.`, @@ -113,44 +139,46 @@ function ask(rl, question) { } function parseCliArgs() { + const args = process.argv.slice(2).filter((a) => a !== "--"); + if (args.length === 0) return null; + try { const { values } = parseArgs({ - args: process.argv.slice(2).filter((a) => a !== "--"), + args, options: { app: { type: "string" }, }, strict: true, }); - return values.app?.trim() || null; - } catch { - return null; + const appId = values.app?.trim() || ""; + if (!appId) throw new CliError(L.error.emptyApp); + return appId; + } catch (error) { + if (error instanceof CliError) throw error; + throw new CliError(L.error.invalidArgs); } } async function loadConfig() { - const fileUrl = pathToFileURL(ES_CONFIG_PATH).href; - const cacheBuster = `?update=${Date.now()}`; - const module = await import(fileUrl + cacheBuster); - return module.default; + try { + const fileUrl = pathToFileURL(ES_CONFIG_PATH).href; + const cacheBuster = `?update=${Date.now()}`; + const module = await import(fileUrl + cacheBuster); + const config = module.default; + if (!config || !config.scripts || typeof config.scripts !== "object") { + throw new CliError(L.error.scriptsNotFound); + } + return config; + } catch (error) { + if (error instanceof CliError) throw error; + throw new CliError( + L.error.configReadFailed + (error?.message ? `\n${error.message}` : "") + ); + } } function generateTsconfig(appId) { const mapping = APP_TYPES_MAP[appId]; - if (!mapping) { - console.warn(L.error.unknownType(appId)); - return JSON.stringify( - { - extends: "../../tsconfig.json", - compilerOptions: { - types: [], - }, - include: ["./**/*", "../lib/**/*", "../types/**/*", "../init.ts"], - }, - null, - 2 - ); - } - const types = [ `../../node_modules/types-for-adobe/${mapping.dir}`, ...mapping.shared.map((s) => `../../node_modules/types-for-adobe/${s}`), @@ -167,15 +195,11 @@ function generateTsconfig(appId) { ); } -const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - -async function updateEsConfig(appId) { - const content = await readFile(ES_CONFIG_PATH, { encoding: "utf8" }); - +function findScriptsBlockEnd(content) { // scripts: { ... } ブロックの閉じ } を正確に特定するため、ブレースの深度を追跡 const scriptsStart = content.search(/scripts\s*:\s*\{/); if (scriptsStart === -1) { - throw new Error("Could not locate scripts block in es.config.mjs."); + throw new CliError(L.error.scriptsNotFound); } // scripts の開き { の位置を特定 @@ -194,23 +218,68 @@ async function updateEsConfig(appId) { } if (braceEnd === -1) { - throw new Error("Could not find closing brace of scripts block."); + throw new CliError(L.error.scriptsNotFound); + } + + return braceEnd; +} + +async function pathExists(targetPath) { + try { + await access(targetPath, constants.F_OK); + return true; + } catch { + return false; + } +} + +function assertKnownApp(appId) { + const knownApps = Object.keys(APP_TYPES_MAP); + if (!appId) throw new CliError(L.error.emptyApp); + if (!Object.prototype.hasOwnProperty.call(APP_TYPES_MAP, appId)) { + throw new CliError(L.error.unsupportedApp(appId, knownApps)); } +} + +async function validateScaffold(appId) { + assertKnownApp(appId); + + const appDir = path.resolve(SRC_DIR, appId); + if (await pathExists(appDir)) { + throw new CliError(L.error.srcExists(appId)); + } + + const config = await loadConfig(); + if (Object.prototype.hasOwnProperty.call(config.scripts, appId)) { + throw new CliError(L.error.configExists(appId)); + } + + const esConfigContent = await readFile(ES_CONFIG_PATH, { encoding: "utf8" }); + findScriptsBlockEnd(esConfigContent); + + return { appDir, esConfigContent }; +} + +async function updateEsConfig(appId, content) { + const braceEnd = findScriptsBlockEnd(content); // 閉じ } の直前に新しいアプリキーを挿入 const beforeClose = content.slice(0, braceEnd); const afterClose = content.slice(braceEnd); - const newContent = `${beforeClose} ${appId}: [],\n ${afterClose}`; + const newContent = `${beforeClose} ${appId}: [ + { + name: "${EXAMPLE_SCRIPT.name}", + version: "${EXAMPLE_SCRIPT.version}", + build: ${EXAMPLE_SCRIPT.build}, + license: ${EXAMPLE_SCRIPT.license}, + }, + ], + ${afterClose}`; await writeFile(ES_CONFIG_PATH, newContent, { encoding: "utf8" }); } async function scaffold(appId) { - const config = await loadConfig(); - if (config.scripts && config.scripts[appId]) { - throw new CliError(L.error.exists(appId)); - } - - const appDir = path.resolve(SRC_DIR, appId); + const { appDir, esConfigContent } = await validateScaffold(appId); // tsconfig.json const tsconfigContent = generateTsconfig(appId); @@ -218,7 +287,7 @@ async function scaffold(appId) { await writeFile( path.resolve(appDir, "tsconfig.json"), tsconfigContent + "\n", - { encoding: "utf8" } + { encoding: "utf8", flag: "wx" } ); // types/index.d.ts @@ -227,13 +296,16 @@ async function scaffold(appId) { await writeFile( path.resolve(typesDir, "index.d.ts"), `// Type definitions specific to ${appId}.\n`, - { encoding: "utf8" } + { encoding: "utf8", flag: "wx" } ); // lib/.gitkeep const libDir = path.resolve(appDir, "lib"); await mkdir(libDir, { recursive: true }); - await writeFile(path.resolve(libDir, ".gitkeep"), "", { encoding: "utf8" }); + await writeFile(path.resolve(libDir, ".gitkeep"), "", { + encoding: "utf8", + flag: "wx", + }); // example/index.ts const exampleDir = path.resolve(appDir, "example"); @@ -241,18 +313,25 @@ async function scaffold(appId) { await writeFile( path.resolve(exampleDir, "index.ts"), `import "../../init";\nimport { entry } from "../../lib/lib";\n\nentry("example", () => {\n // TODO: Implement example\n});\n`, - { encoding: "utf8" } + { encoding: "utf8", flag: "wx" } ); // es.config.mjs 更新 - await updateEsConfig(appId); + await updateEsConfig(appId, esConfigContent); execSync(`prettier --write "${ES_CONFIG_PATH}"`, { stdio: "inherit" }); console.log(L.configUpdated(appId)); console.log(L.done(appId)); } async function main() { - const cliAppId = parseCliArgs(); + let cliAppId; + try { + cliAppId = parseCliArgs(); + } catch (err) { + process.exitCode = 1; + console.error(err instanceof CliError ? err.message : L.error.unexpected); + return; + } if (cliAppId) { // CLI モード @@ -273,12 +352,21 @@ async function main() { const rl = createInterface({ input, output }); try { const knownApps = Object.keys(APP_TYPES_MAP); - const appId = String(await ask(rl, L.prompt.enterApp(knownApps))).trim(); - if (!appId) { - console.error(L.error.emptyApp); - process.exitCode = 1; - return; + knownApps.forEach((app, index) => { + console.log(`${index + 1}. ${app}`); + }); + const selected = String( + await ask(rl, L.prompt.selectApp(knownApps)) + ).trim(); + const selectedIndex = Number(selected); + if ( + !Number.isInteger(selectedIndex) || + selectedIndex < 1 || + selectedIndex > knownApps.length + ) { + throw new CliError(L.error.invalidSelection(selected, knownApps.length)); } + const appId = knownApps[selectedIndex - 1]; await scaffold(appId); } catch (err) { process.exitCode = 1; diff --git a/scripts/newScript.mjs b/scripts/newScript.mjs index 88ff0dc..c4d2185 100644 --- a/scripts/newScript.mjs +++ b/scripts/newScript.mjs @@ -199,7 +199,7 @@ const createIndexTsTemplate = (name) => `/** @description Explain script */ import "../../init"; -import { entry } from "../lib/lib"; +import { entry } from "../../lib/lib"; entry("${name}", () => { // TODO: Implement ${name} From 950ce06fdc2cb8417016010e72e1cad858854e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8A=B1=E9=9B=AA?= <47615501+YukiWorks432@users.noreply.github.com> Date: Sun, 21 Jun 2026 09:48:48 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E3=82=A2=E3=83=97=E3=83=AA=E5=88=A5?= =?UTF-8?q?=E3=83=93=E3=83=AB=E3=83=89=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #18 対応として、アプリ別の build:* コマンドと add-app 時の自動追加を整備する。 --- README.md | 6 ++++ docs/guides/getting-started.md | 5 +++ docs/project-overview.md | 26 ++++++++------ package.json | 3 ++ scripts/addApp.mjs | 66 ++++++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d38cfb3..2107462 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ entry("example", () => { pnpm build # 変更のあるスクリプトをビルド pnpm build --all # 全スクリプトを強制ビルド pnpm build --app=aeft # 特定アプリのみビルド +pnpm build:aeft # After Effects のスクリプトのみビルド ``` 出力先は `dist/{appId}/{ScriptName}/` です。 @@ -130,6 +131,9 @@ pnpm watch | `pnpm build` | 変更のあるスクリプトをビルド | | `pnpm build --all` | 全スクリプトを強制ビルド | | `pnpm build --app=aeft` | 特定アプリのみビルド | +| `pnpm build:aeft` | After Effects のみビルド | +| `pnpm build:ilst` | Illustrator のみビルド | +| `pnpm build:phxs` | Photoshop のみビルド | | `pnpm watch` | ファイル変更を監視して自動ビルド | | `pnpm lint` | ESLint でコード検査 | | `pnpm format` | Prettier でコード整形 | @@ -137,6 +141,8 @@ pnpm watch | `pnpm add-app` | 新規アプリ追加 | | `pnpm clean` | ビルドハッシュをクリーンアップ | +`pnpm add-app -- --app=` で正式対応アプリを追加すると、`pnpm build:` も自動で追加されます。 + ## テスト / Test `src/tests/index.ts`にテストを記述しています。 diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 58acd08..ed21a6a 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -72,8 +72,12 @@ pnpm build ```bash pnpm build --app=aeft +pnpm build:aeft ``` +`pnpm build:aeft` は `pnpm build --app=aeft` と同じく、After Effects 向けスクリプトだけをビルドする短い別名です。 +Illustrator は `pnpm build:ilst`、Photoshop は `pnpm build:phxs` を使えます。 + ### 新しいアプリを追加 ```bash @@ -81,6 +85,7 @@ pnpm add-app -- --app=idsn ``` 追加できるのは `aeft`, `ilst`, `phxs`, `idsn`, `ppro`, `anmt`, `audt` の正式対応アプリのみです。 +追加したアプリには `pnpm build:` も自動で追加されます。 ## ディレクトリ構造 diff --git a/docs/project-overview.md b/docs/project-overview.md index 657e2f9..1424059 100644 --- a/docs/project-overview.md +++ b/docs/project-overview.md @@ -203,22 +203,26 @@ pnpm add-app -- --app=idsn - `src/{app}/lib/.gitkeep` - `src/{app}/example/index.ts` - `es.config.mjs` に `scripts.{app}` キーを追加し、`example` を `build: true`, `license: true` で登録 +- `package.json` に `build:` コマンドを追加 既存の `src/{appId}` または `es.config.mjs` の `scripts.{appId}` と衝突する場合は、ファイルを生成せずに停止する。 ## 開発コマンド -| コマンド | 説明 | -| ----------------------- | ---------------------------------- | -| `pnpm build` | 変更のあるスクリプトをビルド | -| `pnpm build --all` | 全スクリプトを強制ビルド | -| `pnpm build --app=aeft` | 特定アプリのスクリプトのみビルド | -| `pnpm watch` | ファイル変更を監視して自動ビルド | -| `pnpm lint` | ESLint でコード検査 | -| `pnpm format` | Prettier でコード整形 | -| `pnpm new` | 新規スクリプト追加(対話式 / CLI) | -| `pnpm add-app` | 新規アプリスキャフォールディング | -| `pnpm clean` | ビルドハッシュをクリーンアップ | +| コマンド | 説明 | +| ----------------------- | ------------------------------------ | +| `pnpm build` | 変更のあるスクリプトをビルド | +| `pnpm build --all` | 全スクリプトを強制ビルド | +| `pnpm build --app=aeft` | 特定アプリのスクリプトのみビルド | +| `pnpm build:aeft` | After Effects のスクリプトのみビルド | +| `pnpm build:ilst` | Illustrator のスクリプトのみビルド | +| `pnpm build:phxs` | Photoshop のスクリプトのみビルド | +| `pnpm watch` | ファイル変更を監視して自動ビルド | +| `pnpm lint` | ESLint でコード検査 | +| `pnpm format` | Prettier でコード整形 | +| `pnpm new` | 新規スクリプト追加(対話式 / CLI) | +| `pnpm add-app` | 新規アプリスキャフォールディング | +| `pnpm clean` | ビルドハッシュをクリーンアップ | ## 型情報について diff --git a/package.json b/package.json index f8f4a43..0f5f75b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "description": "Multi-app ExtendScript TypeScript template for Adobe After Effects, Illustrator, Photoshop, and more", "scripts": { "build": "rollup -c", + "build:aeft": "rollup -c --app=aeft", + "build:ilst": "rollup -c --app=ilst", + "build:phxs": "rollup -c --app=phxs", "clean": "node ./scripts/cleanBuildHashes.mjs", "watch": "rollup -c -w", "lint": "eslint .", diff --git a/scripts/addApp.mjs b/scripts/addApp.mjs index b97f3c3..09463b9 100644 --- a/scripts/addApp.mjs +++ b/scripts/addApp.mjs @@ -18,6 +18,7 @@ const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, ".."); const ES_CONFIG_PATH = path.resolve(projectRoot, "es.config.mjs"); +const PACKAGE_JSON_PATH = path.resolve(projectRoot, "package.json"); const SRC_DIR = path.resolve(projectRoot, "src"); const EXAMPLE_SCRIPT = { name: "example", @@ -98,6 +99,8 @@ const I18N = { }, done: (app) => `完了: src/${app}/ のスキャフォールディングが完了しました。`, configUpdated: (app) => `es.config.mjs に scripts.${app} を追加しました。`, + packageScriptUpdated: (app) => + `package.json に build:${app} を追加しました。`, }, en: { prompt: { @@ -122,6 +125,7 @@ const I18N = { }, done: (app) => `Done: Scaffolding for src/${app}/ complete.`, configUpdated: (app) => `Added scripts.${app} to es.config.mjs.`, + packageScriptUpdated: (app) => `Added build:${app} to package.json.`, }, }; @@ -278,6 +282,64 @@ async function updateEsConfig(appId, content) { await writeFile(ES_CONFIG_PATH, newContent, { encoding: "utf8" }); } +function shouldInsertAfterBuildScript(currentKey, nextKey) { + const isBuildKey = currentKey === "build" || currentKey.startsWith("build:"); + const nextIsBuildKey = nextKey && nextKey.startsWith("build:"); + return isBuildKey && !nextIsBuildKey; +} + +function addBuildScriptAlias(scripts, appId) { + const scriptName = `build:${appId}`; + if (Object.prototype.hasOwnProperty.call(scripts, scriptName)) { + return { scripts, added: false }; + } + + const entries = Object.entries(scripts); + const nextScripts = {}; + let inserted = false; + + for (let i = 0; i < entries.length; i++) { + const [key, value] = entries[i]; + nextScripts[key] = value; + + const nextKey = entries[i + 1]?.[0] || null; + if (!inserted && shouldInsertAfterBuildScript(key, nextKey)) { + nextScripts[scriptName] = `rollup -c --app=${appId}`; + inserted = true; + } + } + + if (!inserted) { + nextScripts[scriptName] = `rollup -c --app=${appId}`; + } + + return { scripts: nextScripts, added: true }; +} + +async function updatePackageScripts(appId) { + const content = await readFile(PACKAGE_JSON_PATH, { encoding: "utf8" }); + const packageJson = JSON.parse(content); + const scripts = + packageJson.scripts && typeof packageJson.scripts === "object" + ? packageJson.scripts + : {}; + const result = addBuildScriptAlias(scripts, appId); + + if (!result.added) { + return false; + } + + packageJson.scripts = result.scripts; + await writeFile( + PACKAGE_JSON_PATH, + JSON.stringify(packageJson, null, 2) + "\n", + { + encoding: "utf8", + } + ); + return true; +} + async function scaffold(appId) { const { appDir, esConfigContent } = await validateScaffold(appId); @@ -318,7 +380,11 @@ async function scaffold(appId) { // es.config.mjs 更新 await updateEsConfig(appId, esConfigContent); + const packageScriptAdded = await updatePackageScripts(appId); execSync(`prettier --write "${ES_CONFIG_PATH}"`, { stdio: "inherit" }); + if (packageScriptAdded) { + console.log(L.packageScriptUpdated(appId)); + } console.log(L.configUpdated(appId)); console.log(L.done(appId)); } From fea7342fff34f203db5ef3f13c28955b4f115adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8A=B1=E9=9B=AA?= <47615501+YukiWorks432@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:38:36 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E5=85=B1=E9=80=9A=E4=BE=9D=E5=AD=98?= =?UTF-8?q?=E3=82=92=E3=83=93=E3=83=AB=E3=83=89=E3=83=8F=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=81=AB=E5=90=AB=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 共通依存やビルド設定の変更が通常ビルドのスキップ判定に反映されるようにする。 - 共有入力と app 直下の共有ファイルをハッシュ対象に追加 - license ファイルも対象に含める - app 指定ビルド時は非対象スクリプトの履歴を保持 Closes #30 --- rollup.config.mjs | 91 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 72 insertions(+), 19 deletions(-) diff --git a/rollup.config.mjs b/rollup.config.mjs index b5c5fe9..6c9373e 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -40,32 +40,57 @@ const hashText = (text) => { return hashSum.digest("hex"); }; -const collectTypeScriptFiles = (targetDir) => { - if (!fs.existsSync(targetDir)) { +const normalizePath = (filePath) => filePath.replace(/\\/g, "/"); + +const collectFiles = (targetPath) => { + if (!fs.existsSync(targetPath)) { + return []; + } + + const stats = fs.statSync(targetPath); + if (stats.isFile()) { + return [path.resolve(targetPath)]; + } + + if (!stats.isDirectory()) { return []; } - const entries = fs.readdirSync(targetDir, { withFileTypes: true }); + const entries = fs.readdirSync(targetPath, { withFileTypes: true }); const files = []; entries.forEach((entry) => { - const fullPath = path.join(targetDir, entry.name); + const fullPath = path.join(targetPath, entry.name); if (entry.isDirectory()) { - files.push(...collectTypeScriptFiles(fullPath)); + files.push(...collectFiles(fullPath)); return; } - if (/\.ts$/i.test(entry.name)) { - files.push(fullPath); + if (entry.isFile()) { + files.push(path.resolve(fullPath)); } }); - return files.sort((left, right) => left.localeCompare(right)); + return files; +}; + +const getUniqueSortedFiles = (inputPaths) => { + const fileMap = new Map(); + + inputPaths.forEach((inputPath) => { + collectFiles(inputPath).forEach((filePath) => { + fileMap.set(filePath, filePath); + }); + }); + + return Array.from(fileMap.values()).sort((left, right) => + normalizePath(left).localeCompare(normalizePath(right)) + ); }; -const calculateScriptHash = (scriptDir) => { - const files = collectTypeScriptFiles(scriptDir); +const calculateInputHash = (inputPaths) => { + const files = getUniqueSortedFiles(inputPaths); if (files.length === 0) { return hashText("empty"); @@ -73,9 +98,7 @@ const calculateScriptHash = (scriptDir) => { const merged = files .map((filePath) => { - const relativePath = path - .relative(scriptDir, filePath) - .replace(/\\/g, "/"); + const relativePath = normalizePath(path.relative(".", filePath)); return `${relativePath}:${calculateFileHash(filePath)}`; }) .join("|"); @@ -83,6 +106,37 @@ const calculateScriptHash = (scriptDir) => { return hashText(merged); }; +const SHARED_BUILD_INPUTS = [ + "rollup.config.mjs", + "es.config.mjs", + "package.json", + "pnpm-lock.yaml", + "tsconfig.json", + "src/init.ts", + "src/lib", + "src/types", +]; + +const getLicenseFile = (srcDir) => + fs.existsSync(`${srcDir}/LICENSE`) ? `${srcDir}/LICENSE` : "LICENSE"; + +const getScriptHashInputs = ({ appId, script, srcDir, tsconfig }) => { + const inputs = [...SHARED_BUILD_INPUTS, srcDir, tsconfig]; + + if (appId) { + inputs.push(`src/${appId}`); + } + + if (script.license) { + inputs.push(getLicenseFile(srcDir)); + } + + return inputs; +}; + +const calculateScriptHash = (scriptContext) => + calculateInputHash(getScriptHashInputs(scriptContext)); + const loadBuildHashes = () => { try { if (!fs.existsSync(BUILD_HASH_FILE)) { @@ -173,9 +227,7 @@ const extractCommentsToTop = () => ({ }); const licenser = (srcDir) => { - const licenseDir = fs.existsSync(`${srcDir}/LICENSE`) - ? `${srcDir}/LICENSE` - : "LICENSE"; + const licenseDir = getLicenseFile(srcDir); return license({ banner: { content: { @@ -268,10 +320,11 @@ export default (commandLineArgs) => { } const previousBuildHashes = loadBuildHashes(); - const currentBuildHashes = {}; + const currentBuildHashes = appFilter ? { ...previousBuildHashes } : {}; - const targetScripts = allScripts.filter(({ script, srcDir, hashKey }) => { - const scriptHash = calculateScriptHash(srcDir); + const targetScripts = allScripts.filter((scriptContext) => { + const { hashKey } = scriptContext; + const scriptHash = calculateScriptHash(scriptContext); currentBuildHashes[hashKey] = scriptHash; if (forceBuildAll) {