mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-07 07:58:36 +00:00
refactor: move canvas asset build into plugin
This commit is contained in:
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -547,11 +547,13 @@ jobs:
|
||||
path: dist-runtime-build.tar.zst
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload A2UI bundle artifact
|
||||
- name: Upload bundled plugin asset artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: canvas-a2ui-bundle
|
||||
path: extensions/canvas/src/host/a2ui/
|
||||
name: bundled-plugin-assets
|
||||
path: |
|
||||
extensions/*/src/host/**/.bundle.hash
|
||||
extensions/*/src/host/**/*.bundle.js
|
||||
include-hidden-files: true
|
||||
retention-days: 1
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ Done:
|
||||
- Added plugin-owned hosted media resolvers so Canvas document URLs resolve through the Canvas plugin instead of core importing Canvas document internals.
|
||||
- Added `api.registerNodeCliFeature(...)` so Canvas can declare `openclaw nodes canvas` as a plugin-owned node feature without manually spelling the parent command path.
|
||||
- Removed production `src/**` imports of `extensions/canvas/runtime-api.js`.
|
||||
- Moved the A2UI bundle source from `apps/shared/OpenClawKit/Tools/CanvasA2UI` to `extensions/canvas/src/host/a2ui-app`.
|
||||
- Moved A2UI build/copy implementation under `extensions/canvas/scripts` and replaced root build wiring with generic bundled-plugin asset hooks.
|
||||
- Kept top-level `canvasHost` as a legacy read compatibility alias while doctor repairs old configs.
|
||||
- Updated generated plugin inventory to include Canvas.
|
||||
- Added plugin reference docs at `docs/plugins/reference/canvas.md`.
|
||||
@@ -59,7 +61,7 @@ Known remaining core-owned Canvas surfaces:
|
||||
- `src/config/types.gateway.ts` and related schema labels/help retain legacy `canvasHost` read/repair compatibility
|
||||
- Gateway node hello and `nodes.canvasCapability.refresh` still carry `canvasHostUrl`/capability fields because native clients already speak that protocol shape
|
||||
- native app Canvas protocol/client handlers under `apps/`
|
||||
- build/package output still copies A2UI to `dist/canvas-host/a2ui` for published artifact compatibility
|
||||
- published artifact output still uses `dist/canvas-host/a2ui` for backwards-compatible runtime lookup, but the copy step is now plugin-owned
|
||||
|
||||
## Target shape
|
||||
|
||||
@@ -69,6 +71,7 @@ Known remaining core-owned Canvas surfaces:
|
||||
- agent tool registration
|
||||
- node invoke command policy
|
||||
- Canvas host and A2UI runtime
|
||||
- Canvas A2UI bundle source and asset build/copy scripts
|
||||
- Canvas document creation and asset resolution
|
||||
- Canvas CLI implementation
|
||||
- Canvas docs page and plugin inventory entry
|
||||
@@ -83,6 +86,7 @@ Core should own only generic seams:
|
||||
- generic hosted media resolver registration
|
||||
- generic node capability transport plus the existing Canvas protocol fields until native clients have a plugin-generic replacement
|
||||
- generic config plumbing plus the legacy `canvasHost` alias for existing Canvas config
|
||||
- generic bundled-plugin asset hook discovery
|
||||
|
||||
Native apps may keep Canvas command handlers as clients of the protocol. They are not the plugin runtime owner.
|
||||
|
||||
@@ -104,6 +108,7 @@ Before calling the refactor complete:
|
||||
- `rg "canvas-documents" src` is empty.
|
||||
- `rg "registerNodesCanvasCommands|nodes-canvas" src` is empty; the Canvas plugin registers `openclaw nodes canvas` through nested plugin CLI metadata.
|
||||
- `rg "createCanvasHostHandler|handleA2uiHttpRequest" src/gateway` returns no gateway runtime ownership.
|
||||
- `rg "apps/shared/OpenClawKit/Tools/CanvasA2UI|canvas-a2ui-copy|extensions/canvas/src/host/a2ui" scripts .github package.json` finds only compatibility wrappers or plugin-owned paths.
|
||||
- `pnpm plugins:inventory:check` passes.
|
||||
- `pnpm plugin-sdk:api:check` passes, or generated API baselines are intentionally updated and reviewed.
|
||||
- Targeted Canvas tests pass.
|
||||
@@ -118,7 +123,7 @@ Use targeted local checks while iterating:
|
||||
pnpm test extensions/canvas/src/host/server.test.ts extensions/canvas/src/host/server.state-dir.test.ts extensions/canvas/src/host/file-resolver.test.ts
|
||||
pnpm test src/gateway/server.plugin-node-capability-auth.test.ts src/gateway/server-import-boundary.test.ts
|
||||
pnpm test extensions/canvas/src/config-migration.test.ts src/commands/doctor-legacy-config.migrations.test.ts
|
||||
pnpm test test/scripts/changed-lanes.test.ts test/scripts/bundle-a2ui.test.ts
|
||||
pnpm test test/scripts/changed-lanes.test.ts test/scripts/build-all.test.ts test/scripts/bundle-a2ui.test.ts test/scripts/bundled-plugin-assets.test.ts src/scripts/canvas-a2ui-copy.test.ts src/infra/run-node.test.ts
|
||||
pnpm tsgo:extensions
|
||||
pnpm plugins:inventory:check
|
||||
pnpm plugin-sdk:api:check
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
],
|
||||
"assetScripts": {
|
||||
"build": "node scripts/bundle-a2ui.mjs",
|
||||
"copy": "node scripts/copy-a2ui.mjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
228
extensions/canvas/scripts/bundle-a2ui.mjs
Normal file
228
extensions/canvas/scripts/bundle-a2ui.mjs
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { resolvePnpmRunner } from "../../../scripts/pnpm-runner.mjs";
|
||||
|
||||
const pluginDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const rootDir = path.resolve(pluginDir, "../..");
|
||||
const require = createRequire(import.meta.url);
|
||||
const hashFile = path.join(pluginDir, "src", "host", "a2ui", ".bundle.hash");
|
||||
const outputFile = path.join(pluginDir, "src", "host", "a2ui", "a2ui.bundle.js");
|
||||
const a2uiAppDir = path.join(pluginDir, "src", "host", "a2ui-app");
|
||||
const rootPackageFile = path.join(rootDir, "package.json");
|
||||
const lockFile = path.join(rootDir, "pnpm-lock.yaml");
|
||||
const repoInputPaths = [rootPackageFile, lockFile, a2uiAppDir];
|
||||
const relativeRepoInputPaths = repoInputPaths.map((inputPath) =>
|
||||
normalizePath(path.relative(rootDir, inputPath)),
|
||||
);
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
console.error("A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle");
|
||||
console.error("If this persists, verify pnpm deps and try again.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function pathExists(targetPath) {
|
||||
try {
|
||||
await fs.stat(targetPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePath(filePath) {
|
||||
return filePath.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
export function isBundleHashInputPath(filePath, repoRoot = rootDir) {
|
||||
return Boolean(filePath && repoRoot);
|
||||
}
|
||||
|
||||
export function getLocalRolldownCliCandidates(repoRoot = rootDir) {
|
||||
return [
|
||||
path.join(repoRoot, "node_modules", "rolldown", "bin", "cli.mjs"),
|
||||
path.join(repoRoot, "node_modules", ".pnpm", "node_modules", "rolldown", "bin", "cli.mjs"),
|
||||
path.join(
|
||||
repoRoot,
|
||||
"node_modules",
|
||||
".pnpm",
|
||||
"rolldown@1.0.0-rc.12",
|
||||
"node_modules",
|
||||
"rolldown",
|
||||
"bin",
|
||||
"cli.mjs",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function getBundleHashRepoInputPaths(repoRoot = rootDir) {
|
||||
return [
|
||||
path.join(repoRoot, "package.json"),
|
||||
path.join(repoRoot, "pnpm-lock.yaml"),
|
||||
path.join(repoRoot, "extensions", "canvas", "src", "host", "a2ui-app"),
|
||||
];
|
||||
}
|
||||
|
||||
export function getBundleHashInputPaths(repoRoot = rootDir) {
|
||||
return getBundleHashRepoInputPaths(repoRoot);
|
||||
}
|
||||
|
||||
export function compareNormalizedPaths(left, right) {
|
||||
const normalizedLeft = normalizePath(left);
|
||||
const normalizedRight = normalizePath(right);
|
||||
if (normalizedLeft < normalizedRight) {
|
||||
return -1;
|
||||
}
|
||||
if (normalizedLeft > normalizedRight) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function walkFiles(entryPath, files) {
|
||||
if (!isBundleHashInputPath(entryPath)) {
|
||||
return;
|
||||
}
|
||||
const stat = await fs.stat(entryPath);
|
||||
if (!stat.isDirectory()) {
|
||||
files.push(entryPath);
|
||||
return;
|
||||
}
|
||||
const entries = await fs.readdir(entryPath);
|
||||
for (const entry of entries) {
|
||||
await walkFiles(path.join(entryPath, entry), files);
|
||||
}
|
||||
}
|
||||
|
||||
function listTrackedInputFiles() {
|
||||
const result = spawnSync("git", ["ls-files", "--", ...relativeRepoInputPaths], {
|
||||
cwd: rootDir,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
const trackedFiles = result.stdout
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((filePath) => path.join(rootDir, filePath))
|
||||
.filter((filePath) => existsSync(filePath))
|
||||
.filter((filePath) => isBundleHashInputPath(filePath));
|
||||
return trackedFiles;
|
||||
}
|
||||
|
||||
async function computeHash() {
|
||||
let files = listTrackedInputFiles();
|
||||
if (!files) {
|
||||
files = [];
|
||||
for (const inputPath of getBundleHashRepoInputPaths(rootDir)) {
|
||||
await walkFiles(inputPath, files);
|
||||
}
|
||||
}
|
||||
files = [...new Set(files)].toSorted(compareNormalizedPaths);
|
||||
|
||||
const hash = createHash("sha256");
|
||||
for (const filePath of files) {
|
||||
hash.update(normalizePath(path.relative(rootDir, filePath)));
|
||||
hash.update("\0");
|
||||
hash.update(await fs.readFile(filePath));
|
||||
hash.update("\0");
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
function runStep(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: rootDir,
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
...options,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
function runPnpm(pnpmArgs) {
|
||||
const runner = resolvePnpmRunner({
|
||||
pnpmArgs,
|
||||
nodeExecPath: process.execPath,
|
||||
npmExecPath: process.env.npm_execpath,
|
||||
comSpec: process.env.ComSpec,
|
||||
platform: process.platform,
|
||||
});
|
||||
runStep(runner.command, runner.args, {
|
||||
shell: runner.shell,
|
||||
windowsVerbatimArguments: runner.windowsVerbatimArguments,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const hasAppDir = await pathExists(a2uiAppDir);
|
||||
const hasOutputFile = await pathExists(outputFile);
|
||||
let hasA2uiPackage = true;
|
||||
try {
|
||||
require.resolve("@a2ui/lit");
|
||||
require.resolve("@a2ui/lit/ui");
|
||||
} catch {
|
||||
hasA2uiPackage = false;
|
||||
}
|
||||
if (!hasA2uiPackage || !hasAppDir) {
|
||||
if (hasOutputFile) {
|
||||
console.log("A2UI package missing; keeping prebuilt bundle.");
|
||||
return;
|
||||
}
|
||||
if (process.env.OPENCLAW_SPARSE_PROFILE || process.env.OPENCLAW_A2UI_SKIP_MISSING === "1") {
|
||||
console.error(
|
||||
"A2UI package missing; skipping bundle because OPENCLAW_A2UI_SKIP_MISSING=1 or OPENCLAW_SPARSE_PROFILE is set.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
fail(`A2UI package missing and no prebuilt bundle found at: ${outputFile}`);
|
||||
}
|
||||
|
||||
const currentHash = await computeHash();
|
||||
if (await pathExists(hashFile)) {
|
||||
const previousHash = (await fs.readFile(hashFile, "utf8")).trim();
|
||||
if (previousHash === currentHash && hasOutputFile) {
|
||||
console.log("A2UI bundle up to date; skipping.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const localRolldownCliCandidates = getLocalRolldownCliCandidates(rootDir);
|
||||
const localRolldownCli = (
|
||||
await Promise.all(
|
||||
localRolldownCliCandidates.map(async (candidate) =>
|
||||
(await pathExists(candidate)) ? candidate : null,
|
||||
),
|
||||
)
|
||||
).find(Boolean);
|
||||
|
||||
if (localRolldownCli) {
|
||||
runStep(process.execPath, [
|
||||
localRolldownCli,
|
||||
"-c",
|
||||
path.join(a2uiAppDir, "rolldown.config.mjs"),
|
||||
]);
|
||||
} else {
|
||||
runPnpm(["-s", "exec", "rolldown", "-c", path.join(a2uiAppDir, "rolldown.config.mjs")]);
|
||||
}
|
||||
|
||||
await fs.writeFile(hashFile, `${currentHash}\n`, "utf8");
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main().catch((error) => {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const pluginDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const rootDir = path.resolve(pluginDir, "../..");
|
||||
|
||||
function getA2uiPaths(env = process.env) {
|
||||
const srcDir =
|
||||
env.OPENCLAW_A2UI_SRC_DIR ?? path.join(repoRoot, "extensions", "canvas", "src", "host", "a2ui");
|
||||
const outDir = env.OPENCLAW_A2UI_OUT_DIR ?? path.join(repoRoot, "dist", "canvas-host", "a2ui");
|
||||
const srcDir = env.OPENCLAW_A2UI_SRC_DIR ?? path.join(pluginDir, "src", "host", "a2ui");
|
||||
const outDir = env.OPENCLAW_A2UI_OUT_DIR ?? path.join(rootDir, "dist", "canvas-host", "a2ui");
|
||||
return { srcDir, outDir };
|
||||
}
|
||||
|
||||
function shouldSkipMissingA2uiAssets(env = process.env): boolean {
|
||||
function shouldSkipMissingA2uiAssets(env = process.env) {
|
||||
return env.OPENCLAW_A2UI_SKIP_MISSING === "1" || Boolean(env.OPENCLAW_SPARSE_PROFILE);
|
||||
}
|
||||
|
||||
export async function copyA2uiAssets({ srcDir, outDir }: { srcDir: string; outDir: string }) {
|
||||
export async function copyA2uiAssets({ srcDir, outDir }) {
|
||||
const skipMissing = shouldSkipMissingA2uiAssets(process.env);
|
||||
try {
|
||||
await fs.stat(path.join(srcDir, "index.html"));
|
||||
@@ -1,10 +1,9 @@
|
||||
import { html, css, LitElement, unsafeCSS } from "lit";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
import { ContextProvider } from "@lit/context";
|
||||
|
||||
import { v0_8 } from "@a2ui/lit";
|
||||
import "@a2ui/lit/ui";
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { themeContext } from "@openclaw/a2ui-theme-context";
|
||||
import { html, css, LitElement, unsafeCSS } from "lit";
|
||||
import "@a2ui/lit/ui";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
|
||||
const modalStyles = css`
|
||||
dialog {
|
||||
@@ -97,8 +96,12 @@ const textHintStyles = () => ({ h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, body: {}
|
||||
|
||||
const isAndroid = /Android/i.test(globalThis.navigator?.userAgent ?? "");
|
||||
const cardShadow = isAndroid ? "0 2px 10px rgba(0,0,0,.18)" : "0 10px 30px rgba(0,0,0,.35)";
|
||||
const buttonShadow = isAndroid ? "0 2px 10px rgba(6, 182, 212, 0.14)" : "0 10px 25px rgba(6, 182, 212, 0.18)";
|
||||
const statusShadow = isAndroid ? "0 2px 10px rgba(0, 0, 0, 0.18)" : "0 10px 24px rgba(0, 0, 0, 0.25)";
|
||||
const buttonShadow = isAndroid
|
||||
? "0 2px 10px rgba(6, 182, 212, 0.14)"
|
||||
: "0 10px 25px rgba(6, 182, 212, 0.18)";
|
||||
const statusShadow = isAndroid
|
||||
? "0 2px 10px rgba(0, 0, 0, 0.18)"
|
||||
: "0 10px 24px rgba(0, 0, 0, 0.25)";
|
||||
const statusBlur = isAndroid ? "10px" : "14px";
|
||||
|
||||
const openclawTheme = {
|
||||
@@ -125,7 +128,11 @@ const openclawTheme = {
|
||||
MultipleChoice: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
|
||||
Row: emptyClasses(),
|
||||
Slider: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
|
||||
Tabs: { container: emptyClasses(), element: emptyClasses(), controls: { all: emptyClasses(), selected: emptyClasses() } },
|
||||
Tabs: {
|
||||
container: emptyClasses(),
|
||||
element: emptyClasses(),
|
||||
controls: { all: emptyClasses(), selected: emptyClasses() },
|
||||
},
|
||||
Text: {
|
||||
all: emptyClasses(),
|
||||
h1: emptyClasses(),
|
||||
@@ -235,11 +242,8 @@ class OpenClawA2UIHost extends LitElement {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding:
|
||||
var(--openclaw-a2ui-inset-top, 0px)
|
||||
var(--openclaw-a2ui-inset-right, 0px)
|
||||
var(--openclaw-a2ui-inset-bottom, 0px)
|
||||
var(--openclaw-a2ui-inset-left, 0px);
|
||||
padding: var(--openclaw-a2ui-inset-top, 0px) var(--openclaw-a2ui-inset-right, 0px)
|
||||
var(--openclaw-a2ui-inset-bottom, 0px) var(--openclaw-a2ui-inset-left, 0px);
|
||||
}
|
||||
|
||||
#surfaces {
|
||||
@@ -264,7 +268,12 @@ class OpenClawA2UIHost extends LitElement {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font: 13px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Roboto", sans-serif;
|
||||
font:
|
||||
13px/1.2 system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Roboto",
|
||||
sans-serif;
|
||||
pointer-events: none;
|
||||
backdrop-filter: blur(${unsafeCSS(statusBlur)});
|
||||
-webkit-backdrop-filter: blur(${unsafeCSS(statusBlur)});
|
||||
@@ -285,7 +294,12 @@ class OpenClawA2UIHost extends LitElement {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font: 13px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Roboto", sans-serif;
|
||||
font:
|
||||
13px/1.2 system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Roboto",
|
||||
sans-serif;
|
||||
pointer-events: none;
|
||||
backdrop-filter: blur(${unsafeCSS(statusBlur)});
|
||||
-webkit-backdrop-filter: blur(${unsafeCSS(statusBlur)});
|
||||
@@ -360,7 +374,10 @@ class OpenClawA2UIHost extends LitElement {
|
||||
}
|
||||
|
||||
#makeActionId() {
|
||||
return globalThis.crypto?.randomUUID?.() ?? `a2ui_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
return (
|
||||
globalThis.crypto?.randomUUID?.() ??
|
||||
`a2ui_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||||
);
|
||||
}
|
||||
|
||||
#setToast(text, kind = "ok", timeoutMs = 1400) {
|
||||
@@ -377,8 +394,12 @@ class OpenClawA2UIHost extends LitElement {
|
||||
|
||||
#handleActionStatus(evt) {
|
||||
const detail = evt?.detail ?? null;
|
||||
if (!detail || typeof detail.id !== "string") {return;}
|
||||
if (!this.pendingAction || this.pendingAction.id !== detail.id) {return;}
|
||||
if (!detail || typeof detail.id !== "string") {
|
||||
return;
|
||||
}
|
||||
if (!this.pendingAction || this.pendingAction.id !== detail.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detail.ok) {
|
||||
this.pendingAction = { ...this.pendingAction, phase: "sent", sentAt: Date.now() };
|
||||
@@ -421,7 +442,9 @@ class OpenClawA2UIHost extends LitElement {
|
||||
for (const item of ctxItems) {
|
||||
const key = item?.key;
|
||||
const value = item?.value ?? null;
|
||||
if (!key || !value) {continue;}
|
||||
if (!key || !value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value.path === "string") {
|
||||
const resolved = sourceNode
|
||||
@@ -474,11 +497,23 @@ class OpenClawA2UIHost extends LitElement {
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = String(e?.message ?? e);
|
||||
this.pendingAction = { id: actionId, name, phase: "error", startedAt: Date.now(), error: msg };
|
||||
this.pendingAction = {
|
||||
id: actionId,
|
||||
name,
|
||||
phase: "error",
|
||||
startedAt: Date.now(),
|
||||
error: msg,
|
||||
};
|
||||
this.#setToast(`Failed: ${msg}`, "error", 4500);
|
||||
}
|
||||
} else {
|
||||
this.pendingAction = { id: actionId, name, phase: "error", startedAt: Date.now(), error: "missing native bridge" };
|
||||
this.pendingAction = {
|
||||
id: actionId,
|
||||
name,
|
||||
phase: "error",
|
||||
startedAt: Date.now(),
|
||||
error: "missing native bridge",
|
||||
};
|
||||
this.#setToast("Failed: missing native bridge", "error", 4500);
|
||||
}
|
||||
}
|
||||
@@ -525,24 +560,28 @@ class OpenClawA2UIHost extends LitElement {
|
||||
? `Failed: ${this.pendingAction.name}`
|
||||
: "";
|
||||
|
||||
return html`
|
||||
${this.pendingAction && this.pendingAction.phase !== "error"
|
||||
? html`<div class="status"><div class="spinner"></div><div>${statusText}</div></div>`
|
||||
return html` ${this.pendingAction && this.pendingAction.phase !== "error"
|
||||
? html`<div class="status">
|
||||
<div class="spinner"></div>
|
||||
<div>${statusText}</div>
|
||||
</div>`
|
||||
: ""}
|
||||
${this.toast
|
||||
? html`<div class="toast ${this.toast.kind === "error" ? "error" : ""}">${this.toast.text}</div>`
|
||||
? html`<div class="toast ${this.toast.kind === "error" ? "error" : ""}">
|
||||
${this.toast.text}
|
||||
</div>`
|
||||
: ""}
|
||||
<section id="surfaces">
|
||||
${repeat(
|
||||
this.surfaces,
|
||||
([surfaceId]) => surfaceId,
|
||||
([surfaceId, surface]) => html`<a2ui-surface
|
||||
.surfaceId=${surfaceId}
|
||||
.surface=${surface}
|
||||
.processor=${this.#processor}
|
||||
></a2ui-surface>`
|
||||
)}
|
||||
</section>`;
|
||||
${repeat(
|
||||
this.surfaces,
|
||||
([surfaceId]) => surfaceId,
|
||||
([surfaceId, surface]) => html`<a2ui-surface
|
||||
.surfaceId=${surfaceId}
|
||||
.surface=${surface}
|
||||
.processor=${this.#processor}
|
||||
></a2ui-surface>`,
|
||||
)}
|
||||
</section>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
@@ -8,16 +8,7 @@ const repoRoot = path.resolve(here, "../../../../..");
|
||||
const require = createRequire(import.meta.url);
|
||||
const uiRoot = path.resolve(repoRoot, "ui");
|
||||
const fromHere = (p) => path.resolve(here, p);
|
||||
const outputFile = path.resolve(
|
||||
here,
|
||||
"../../../../..",
|
||||
"extensions",
|
||||
"canvas",
|
||||
"src",
|
||||
"host",
|
||||
"a2ui",
|
||||
"a2ui.bundle.js",
|
||||
);
|
||||
const outputFile = path.resolve(here, "..", "a2ui", "a2ui.bundle.js");
|
||||
|
||||
const a2uiLitIndex = require.resolve("@a2ui/lit");
|
||||
const a2uiLitUi = require.resolve("@a2ui/lit/ui");
|
||||
@@ -1 +1 @@
|
||||
8e45568ff64c7d2fd95957f076b635b6df99f115d1ee92e75ea63916566adb48
|
||||
992142e47ead0d7fb084464cc70f9752f2a7ffb8921598a94a02adaff0fc683c
|
||||
|
||||
@@ -1302,10 +1302,10 @@
|
||||
"audit:seams": "node scripts/audit-seams.mjs",
|
||||
"build": "node scripts/build-all.mjs",
|
||||
"build:ci-artifacts": "node scripts/build-all.mjs ciArtifacts",
|
||||
"build:docker": "node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --experimental-strip-types scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
|
||||
"build:docker": "node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && pnpm plugins:assets:copy && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --experimental-strip-types scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
|
||||
"build:plugin-sdk:dts": "node scripts/run-tsgo.mjs -p tsconfig.plugin-sdk.dts.json --declaration true",
|
||||
"build:plugin-sdk:strict-smoke": "pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts",
|
||||
"build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs",
|
||||
"build:strict-smoke": "pnpm plugins:assets:build && node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs",
|
||||
"canon:check": "node scripts/canon.mjs check",
|
||||
"canon:check:json": "node scripts/canon.mjs check --json",
|
||||
"canon:enforce": "node scripts/canon.mjs enforce --json",
|
||||
@@ -1458,6 +1458,8 @@
|
||||
"plugins:boundary-report:ci": "node --import tsx scripts/plugin-boundary-report.ts --summary --fail-on-cross-owner --fail-on-unclassified-unused-reserved --fail-on-eligible-compat",
|
||||
"plugins:boundary-report:json": "node --import tsx scripts/plugin-boundary-report.ts --json",
|
||||
"plugins:boundary-report:summary": "node --import tsx scripts/plugin-boundary-report.ts --summary",
|
||||
"plugins:assets:build": "node scripts/bundled-plugin-assets.mjs --phase build",
|
||||
"plugins:assets:copy": "node scripts/bundled-plugin-assets.mjs --phase copy",
|
||||
"plugins:inventory:check": "node scripts/generate-plugin-inventory-doc.mjs --check",
|
||||
"plugins:inventory:gen": "node scripts/generate-plugin-inventory-doc.mjs --write",
|
||||
"plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
|
||||
|
||||
@@ -11,7 +11,7 @@ const nodeBin = process.execPath;
|
||||
const WINDOWS_BUILD_MAX_OLD_SPACE_MB = 4096;
|
||||
const BUILD_CACHE_VERSION = 2;
|
||||
export const BUILD_ALL_STEPS = [
|
||||
{ label: "canvas:a2ui:bundle", kind: "pnpm", pnpmArgs: ["canvas:a2ui:bundle"] },
|
||||
{ label: "plugins:assets:build", kind: "pnpm", pnpmArgs: ["plugins:assets:build"] },
|
||||
{ label: "tsdown", kind: "node", args: ["scripts/tsdown-build.mjs"] },
|
||||
{
|
||||
label: "check-cli-bootstrap-imports",
|
||||
@@ -53,13 +53,9 @@ export const BUILD_ALL_STEPS = [
|
||||
args: ["scripts/check-plugin-sdk-exports.mjs"],
|
||||
},
|
||||
{
|
||||
label: "canvas-a2ui-copy",
|
||||
kind: "node",
|
||||
args: ["--import", "tsx", "scripts/canvas-a2ui-copy.ts"],
|
||||
cache: {
|
||||
inputs: ["scripts/canvas-a2ui-copy.ts", "extensions/canvas/src/host/a2ui"],
|
||||
outputs: ["dist/canvas-host/a2ui/index.html", "dist/canvas-host/a2ui/a2ui.bundle.js"],
|
||||
},
|
||||
label: "plugins:assets:copy",
|
||||
kind: "pnpm",
|
||||
pnpmArgs: ["plugins:assets:copy"],
|
||||
},
|
||||
{
|
||||
label: "copy-hook-metadata",
|
||||
@@ -99,7 +95,7 @@ export const BUILD_ALL_STEPS = [
|
||||
export const BUILD_ALL_PROFILES = {
|
||||
full: BUILD_ALL_STEPS.map((step) => step.label),
|
||||
ciArtifacts: [
|
||||
"canvas:a2ui:bundle",
|
||||
"plugins:assets:build",
|
||||
"tsdown",
|
||||
"check-cli-bootstrap-imports",
|
||||
"runtime-postbuild",
|
||||
@@ -108,7 +104,7 @@ export const BUILD_ALL_PROFILES = {
|
||||
"build:plugin-sdk:dts",
|
||||
"write-plugin-sdk-entry-dts",
|
||||
"check-plugin-sdk-exports",
|
||||
"canvas-a2ui-copy",
|
||||
"plugins:assets:copy",
|
||||
"copy-hook-metadata",
|
||||
"copy-export-html-templates",
|
||||
"write-build-info",
|
||||
|
||||
@@ -1,235 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
|
||||
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const require = createRequire(import.meta.url);
|
||||
const hashFile = path.join(rootDir, "extensions", "canvas", "src", "host", "a2ui", ".bundle.hash");
|
||||
const outputFile = path.join(
|
||||
rootDir,
|
||||
"extensions",
|
||||
"canvas",
|
||||
"src",
|
||||
"host",
|
||||
"a2ui",
|
||||
"a2ui.bundle.js",
|
||||
);
|
||||
const a2uiAppDir = path.join(rootDir, "apps", "shared", "OpenClawKit", "Tools", "CanvasA2UI");
|
||||
const rootPackageFile = path.join(rootDir, "package.json");
|
||||
const lockFile = path.join(rootDir, "pnpm-lock.yaml");
|
||||
const repoInputPaths = [rootPackageFile, lockFile, a2uiAppDir];
|
||||
const relativeRepoInputPaths = repoInputPaths.map((inputPath) =>
|
||||
normalizePath(path.relative(rootDir, inputPath)),
|
||||
);
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
console.error("A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle");
|
||||
console.error("If this persists, verify pnpm deps and try again.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function pathExists(targetPath) {
|
||||
try {
|
||||
await fs.stat(targetPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePath(filePath) {
|
||||
return filePath.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
export function isBundleHashInputPath(filePath, repoRoot = rootDir) {
|
||||
return Boolean(filePath && repoRoot);
|
||||
}
|
||||
|
||||
export function getLocalRolldownCliCandidates(repoRoot = rootDir) {
|
||||
return [
|
||||
path.join(repoRoot, "node_modules", "rolldown", "bin", "cli.mjs"),
|
||||
path.join(repoRoot, "node_modules", ".pnpm", "node_modules", "rolldown", "bin", "cli.mjs"),
|
||||
path.join(
|
||||
repoRoot,
|
||||
"node_modules",
|
||||
".pnpm",
|
||||
"rolldown@1.0.0-rc.12",
|
||||
"node_modules",
|
||||
"rolldown",
|
||||
"bin",
|
||||
"cli.mjs",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function getBundleHashRepoInputPaths(repoRoot = rootDir) {
|
||||
return [
|
||||
path.join(repoRoot, "package.json"),
|
||||
path.join(repoRoot, "pnpm-lock.yaml"),
|
||||
path.join(repoRoot, "apps", "shared", "OpenClawKit", "Tools", "CanvasA2UI"),
|
||||
];
|
||||
}
|
||||
|
||||
export function getBundleHashInputPaths(repoRoot = rootDir) {
|
||||
return getBundleHashRepoInputPaths(repoRoot);
|
||||
}
|
||||
|
||||
export function compareNormalizedPaths(left, right) {
|
||||
const normalizedLeft = normalizePath(left);
|
||||
const normalizedRight = normalizePath(right);
|
||||
if (normalizedLeft < normalizedRight) {
|
||||
return -1;
|
||||
}
|
||||
if (normalizedLeft > normalizedRight) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function walkFiles(entryPath, files) {
|
||||
if (!isBundleHashInputPath(entryPath)) {
|
||||
return;
|
||||
}
|
||||
const stat = await fs.stat(entryPath);
|
||||
if (!stat.isDirectory()) {
|
||||
files.push(entryPath);
|
||||
return;
|
||||
}
|
||||
const entries = await fs.readdir(entryPath);
|
||||
for (const entry of entries) {
|
||||
await walkFiles(path.join(entryPath, entry), files);
|
||||
}
|
||||
}
|
||||
|
||||
function listTrackedInputFiles() {
|
||||
const result = spawnSync("git", ["ls-files", "--", ...relativeRepoInputPaths], {
|
||||
cwd: rootDir,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
const trackedFiles = result.stdout
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((filePath) => path.join(rootDir, filePath))
|
||||
.filter((filePath) => existsSync(filePath))
|
||||
.filter((filePath) => isBundleHashInputPath(filePath));
|
||||
return trackedFiles;
|
||||
}
|
||||
|
||||
async function computeHash() {
|
||||
let files = listTrackedInputFiles();
|
||||
if (!files) {
|
||||
files = [];
|
||||
for (const inputPath of getBundleHashRepoInputPaths(rootDir)) {
|
||||
await walkFiles(inputPath, files);
|
||||
}
|
||||
}
|
||||
files = [...new Set(files)].toSorted(compareNormalizedPaths);
|
||||
|
||||
const hash = createHash("sha256");
|
||||
for (const filePath of files) {
|
||||
hash.update(normalizePath(path.relative(rootDir, filePath)));
|
||||
hash.update("\0");
|
||||
hash.update(await fs.readFile(filePath));
|
||||
hash.update("\0");
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
function runStep(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
...options,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
function runPnpm(pnpmArgs) {
|
||||
const runner = resolvePnpmRunner({
|
||||
pnpmArgs,
|
||||
nodeExecPath: process.execPath,
|
||||
npmExecPath: process.env.npm_execpath,
|
||||
comSpec: process.env.ComSpec,
|
||||
platform: process.platform,
|
||||
});
|
||||
runStep(runner.command, runner.args, {
|
||||
shell: runner.shell,
|
||||
windowsVerbatimArguments: runner.windowsVerbatimArguments,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const hasAppDir = await pathExists(a2uiAppDir);
|
||||
const hasOutputFile = await pathExists(outputFile);
|
||||
let hasA2uiPackage = true;
|
||||
try {
|
||||
require.resolve("@a2ui/lit");
|
||||
require.resolve("@a2ui/lit/ui");
|
||||
} catch {
|
||||
hasA2uiPackage = false;
|
||||
}
|
||||
if (!hasA2uiPackage || !hasAppDir) {
|
||||
if (hasOutputFile) {
|
||||
console.log("A2UI package missing; keeping prebuilt bundle.");
|
||||
return;
|
||||
}
|
||||
if (process.env.OPENCLAW_SPARSE_PROFILE || process.env.OPENCLAW_A2UI_SKIP_MISSING === "1") {
|
||||
console.error(
|
||||
"A2UI package missing; skipping bundle because OPENCLAW_A2UI_SKIP_MISSING=1 or OPENCLAW_SPARSE_PROFILE is set.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
fail(`A2UI package missing and no prebuilt bundle found at: ${outputFile}`);
|
||||
}
|
||||
|
||||
const currentHash = await computeHash();
|
||||
if (await pathExists(hashFile)) {
|
||||
const previousHash = (await fs.readFile(hashFile, "utf8")).trim();
|
||||
if (previousHash === currentHash && hasOutputFile) {
|
||||
console.log("A2UI bundle up to date; skipping.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const localRolldownCliCandidates = getLocalRolldownCliCandidates(rootDir);
|
||||
const localRolldownCli = (
|
||||
await Promise.all(
|
||||
localRolldownCliCandidates.map(async (candidate) =>
|
||||
(await pathExists(candidate)) ? candidate : null,
|
||||
),
|
||||
)
|
||||
).find(Boolean);
|
||||
|
||||
if (localRolldownCli) {
|
||||
runStep(process.execPath, [
|
||||
localRolldownCli,
|
||||
"-c",
|
||||
path.join(a2uiAppDir, "rolldown.config.mjs"),
|
||||
]);
|
||||
} else {
|
||||
runPnpm(["-s", "exec", "rolldown", "-c", path.join(a2uiAppDir, "rolldown.config.mjs")]);
|
||||
}
|
||||
|
||||
await fs.writeFile(hashFile, `${currentHash}\n`, "utf8");
|
||||
}
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { runBundledPluginAssetHooks } from "./bundled-plugin-assets.mjs";
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main().catch((error) => {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
await runBundledPluginAssetHooks({ phase: "build", plugins: ["canvas"] });
|
||||
}
|
||||
|
||||
177
scripts/bundled-plugin-assets.mjs
Normal file
177
scripts/bundled-plugin-assets.mjs
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const VALID_PHASES = new Set(["build", "copy"]);
|
||||
|
||||
async function readJsonFile(filePath) {
|
||||
return JSON.parse(await fs.readFile(filePath, "utf8"));
|
||||
}
|
||||
|
||||
async function pathExists(filePath) {
|
||||
try {
|
||||
await fs.stat(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function packagePluginAliases(packageName) {
|
||||
if (typeof packageName !== "string") {
|
||||
return [];
|
||||
}
|
||||
const aliases = [packageName];
|
||||
const unscopedName = packageName.split("/").at(-1);
|
||||
if (unscopedName) {
|
||||
aliases.push(unscopedName);
|
||||
if (unscopedName.endsWith("-plugin")) {
|
||||
aliases.push(unscopedName.slice(0, -"-plugin".length));
|
||||
}
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
async function resolvePluginAliases(pluginDir, packageJson) {
|
||||
const aliases = new Set([path.basename(pluginDir), ...packagePluginAliases(packageJson.name)]);
|
||||
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
|
||||
if (await pathExists(manifestPath)) {
|
||||
const manifest = await readJsonFile(manifestPath);
|
||||
if (typeof manifest.id === "string" && manifest.id) {
|
||||
aliases.add(manifest.id);
|
||||
}
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function resolveAssetCommand(packageJson, phase) {
|
||||
const assetScripts = packageJson.openclaw?.assetScripts;
|
||||
if (!assetScripts || typeof assetScripts !== "object") {
|
||||
return null;
|
||||
}
|
||||
const command = assetScripts[phase];
|
||||
return typeof command === "string" && command.trim() ? command.trim() : null;
|
||||
}
|
||||
|
||||
export async function readBundledPluginAssetHooks(options = {}) {
|
||||
const repoRoot = options.rootDir ?? rootDir;
|
||||
const phase = options.phase;
|
||||
if (!VALID_PHASES.has(phase)) {
|
||||
throw new Error(`Unsupported bundled plugin asset phase: ${String(phase)}`);
|
||||
}
|
||||
|
||||
const pluginFilters = new Set((options.plugins ?? []).filter(Boolean));
|
||||
const extensionsDir = path.join(repoRoot, "extensions");
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(extensionsDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hooks = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const pluginDir = path.join(extensionsDir, entry.name);
|
||||
const packagePath = path.join(pluginDir, "package.json");
|
||||
if (!(await pathExists(packagePath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageJson = await readJsonFile(packagePath);
|
||||
const aliases = await resolvePluginAliases(pluginDir, packageJson);
|
||||
if (pluginFilters.size > 0 && ![...pluginFilters].some((plugin) => aliases.has(plugin))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const command = resolveAssetCommand(packageJson, phase);
|
||||
if (!command) {
|
||||
continue;
|
||||
}
|
||||
|
||||
hooks.push({
|
||||
aliases: [...aliases].toSorted(),
|
||||
command,
|
||||
packageName: packageJson.name,
|
||||
phase,
|
||||
pluginDir,
|
||||
pluginId: aliases.has(entry.name) ? entry.name : [...aliases][0],
|
||||
});
|
||||
}
|
||||
|
||||
return hooks.toSorted((left, right) => left.pluginDir.localeCompare(right.pluginDir));
|
||||
}
|
||||
|
||||
export async function runBundledPluginAssetHooks(options = {}) {
|
||||
const phase = options.phase;
|
||||
const hooks = await readBundledPluginAssetHooks(options);
|
||||
if (hooks.length === 0) {
|
||||
const scope = options.plugins?.length ? ` for ${options.plugins.join(", ")}` : "";
|
||||
console.log(`No bundled plugin asset ${phase} hooks${scope}; skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const hook of hooks) {
|
||||
console.log(`[${hook.pluginId}] ${phase}: ${hook.command}`);
|
||||
const result = spawnSync(hook.command, {
|
||||
cwd: hook.pluginDir,
|
||||
env: process.env,
|
||||
shell: true,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseBundledPluginAssetArgs(argv) {
|
||||
const args = [...argv];
|
||||
const plugins = [];
|
||||
let phase = null;
|
||||
|
||||
while (args.length > 0) {
|
||||
const arg = args.shift();
|
||||
if (arg === "--phase") {
|
||||
phase = args.shift() ?? null;
|
||||
continue;
|
||||
}
|
||||
if (arg?.startsWith("--phase=")) {
|
||||
phase = arg.slice("--phase=".length);
|
||||
continue;
|
||||
}
|
||||
if (arg === "--plugin") {
|
||||
const plugin = args.shift();
|
||||
if (plugin) {
|
||||
plugins.push(plugin);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg?.startsWith("--plugin=")) {
|
||||
plugins.push(arg.slice("--plugin=".length));
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown bundled plugin asset argument: ${String(arg)}`);
|
||||
}
|
||||
|
||||
if (!VALID_PHASES.has(phase)) {
|
||||
throw new Error(`Expected --phase ${[...VALID_PHASES].join("|")}`);
|
||||
}
|
||||
|
||||
return { phase, plugins };
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
try {
|
||||
await runBundledPluginAssetHooks(parseBundledPluginAssetArgs(process.argv.slice(2)));
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ const TOOLING_PATH_RE =
|
||||
const ROOT_GLOBAL_PATH_RE =
|
||||
/^(?:package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsdown\.config\.ts$|vitest\.config\.ts$)/u;
|
||||
const LEGACY_ROOT_ASSET_PATH_RE = /^assets\//u;
|
||||
const CANVAS_A2UI_BUNDLE_INPUT_RE = /^apps\/shared\/OpenClawKit\/Tools\/CanvasA2UI\//u;
|
||||
const LIVE_DOCKER_TOOLING_PATH_RE =
|
||||
/^(?:scripts\/test-docker-all\.mjs|scripts\/test-docker-all\.sh|scripts\/lib\/live-docker-auth\.sh|scripts\/test-live-(?:acp-bind|cli-backend|codex-harness|gateway-models|models)-docker\.sh|src\/gateway\/gateway-acp-bind\.live\.test\.ts|src\/gateway\/live-agent-probes\.test\.ts)$/u;
|
||||
const LIVE_DOCKER_PACKAGE_SCRIPT_RE = /^test:docker:live-[\w:-]+$/u;
|
||||
@@ -172,14 +171,6 @@ export function detectChangedLanes(changedPaths, options = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CANVAS_A2UI_BUNDLE_INPUT_RE.test(changedPath)) {
|
||||
lanes.core = true;
|
||||
lanes.coreTests = true;
|
||||
lanes.tooling = true;
|
||||
reasons.push(`${changedPath}: CanvasA2UI bundle input`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (APP_PATH_RE.test(changedPath)) {
|
||||
lanes.apps = true;
|
||||
reasons.push(`${changedPath}: app surface`);
|
||||
|
||||
@@ -48,7 +48,7 @@ parallels_package_write_dist_inventory() {
|
||||
|
||||
parallels_package_assert_no_generated_drift() {
|
||||
local drift
|
||||
drift="$(git status --porcelain -- extensions/canvas/src/host/a2ui/.bundle.hash 2>/dev/null || true)"
|
||||
drift="$(git status --porcelain -- ':(glob)extensions/*/src/host/**/.bundle.hash' 2>/dev/null || true)"
|
||||
if [[ -z "$drift" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -107,7 +107,7 @@ async function ensureCurrentBuildUnlocked(input: {
|
||||
}
|
||||
const drift = run(
|
||||
"git",
|
||||
["status", "--porcelain", "--", "extensions/canvas/src/host/a2ui/.bundle.hash"],
|
||||
["status", "--porcelain", "--", ":(glob)extensions/*/src/host/**/.bundle.hash"],
|
||||
{
|
||||
quiet: true,
|
||||
},
|
||||
|
||||
@@ -22,14 +22,14 @@ if (mode !== "lint" && mode !== "format") {
|
||||
|
||||
const lintExts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
||||
const formatExts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".md", ".mdx"]);
|
||||
const formatIgnoredPaths = new Set(["extensions/canvas/src/host/a2ui/a2ui.bundle.js"]);
|
||||
const formatIgnoredPathPatterns = [/^extensions\/[^/]+\/src\/host\/.+\/[^/]+\.bundle\.js$/u];
|
||||
|
||||
const shouldSelect = (filePath) => {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (mode === "lint") {
|
||||
return lintExts.has(ext);
|
||||
}
|
||||
if (formatIgnoredPaths.has(filePath)) {
|
||||
if (formatIgnoredPathPatterns.some((pattern) => pattern.test(filePath))) {
|
||||
return false;
|
||||
}
|
||||
return formatExts.has(ext);
|
||||
|
||||
@@ -54,7 +54,7 @@ run_linux_ci_mirror() {
|
||||
run_step pnpm build:strict-smoke
|
||||
run_step pnpm lint:ui:no-raw-window-open
|
||||
run_protocol_ci_mirror
|
||||
run_step pnpm canvas:a2ui:bundle
|
||||
run_step pnpm plugins:assets:build
|
||||
run_step node scripts/run-vitest.mjs run --config test/vitest/vitest.extensions.config.ts --maxWorkers=1
|
||||
run_step env CI=true node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --maxWorkers=1
|
||||
|
||||
|
||||
@@ -153,8 +153,8 @@ log "==> Killing existing OpenClaw instances"
|
||||
kill_all_openclaw
|
||||
stop_launch_agent
|
||||
|
||||
# Bundle Gateway-hosted Canvas A2UI assets.
|
||||
run_step "bundle canvas a2ui" bash -lc "cd '${ROOT_DIR}' && pnpm canvas:a2ui:bundle"
|
||||
# Bundle Gateway-hosted plugin assets.
|
||||
run_step "bundle plugin assets" bash -lc "cd '${ROOT_DIR}' && pnpm plugins:assets:build"
|
||||
|
||||
# 2) Rebuild into the same path the packager consumes (.build).
|
||||
run_step "clean build cache" bash -lc "cd '${ROOT_DIR}/apps/macos' && rm -rf .build .build-swift .swiftpm 2>/dev/null || true"
|
||||
|
||||
@@ -9,10 +9,10 @@ export const runNodeConfigFiles = ["tsconfig.json", "package.json", "tsdown.conf
|
||||
export const runNodeWatchedPaths = [...runNodeSourceRoots, ...runNodeConfigFiles];
|
||||
export const extensionRestartMetadataFiles = new Set(["openclaw.plugin.json", "package.json"]);
|
||||
|
||||
const ignoredRunNodeRepoPaths = new Set([
|
||||
"extensions/canvas/src/host/a2ui/.bundle.hash",
|
||||
"extensions/canvas/src/host/a2ui/a2ui.bundle.js",
|
||||
]);
|
||||
const ignoredRunNodeRepoPathPatterns = [
|
||||
/^extensions\/[^/]+\/src\/host\/.+\/\.bundle\.hash$/u,
|
||||
/^extensions\/[^/]+\/src\/host\/.+\/[^/]+\.bundle\.js$/u,
|
||||
];
|
||||
const extensionSourceFilePattern = /\.(?:[cm]?[jt]sx?)$/;
|
||||
|
||||
export const normalizeRunNodePath = (filePath) => String(filePath ?? "").replaceAll("\\", "/");
|
||||
@@ -41,7 +41,7 @@ const isRestartRelevantExtensionPath = (relativePath) => {
|
||||
|
||||
const isRelevantRunNodePath = (repoPath, isRelevantBundledPluginPath) => {
|
||||
const normalizedPath = normalizeRunNodePath(repoPath).replace(/^\.\/+/, "");
|
||||
if (ignoredRunNodeRepoPaths.has(normalizedPath)) {
|
||||
if (ignoredRunNodeRepoPathPatterns.some((pattern) => pattern.test(normalizedPath))) {
|
||||
return false;
|
||||
}
|
||||
if (runNodeConfigFiles.includes(normalizedPath)) {
|
||||
|
||||
@@ -374,6 +374,10 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
||||
["scripts/blacksmith-testbox-state.mjs", ["test/scripts/blacksmith-testbox-state.test.ts"]],
|
||||
["scripts/blacksmith-testbox-runner.mjs", ["test/scripts/blacksmith-testbox-runner.test.ts"]],
|
||||
["scripts/testbox-sync-sanity.mjs", ["test/scripts/testbox-sync-sanity.test.ts"]],
|
||||
["scripts/bundled-plugin-assets.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]],
|
||||
["scripts/bundle-a2ui.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]],
|
||||
["extensions/canvas/scripts/bundle-a2ui.mjs", ["test/scripts/bundle-a2ui.test.ts"]],
|
||||
["extensions/canvas/scripts/copy-a2ui.mjs", ["src/scripts/canvas-a2ui-copy.test.ts"]],
|
||||
]);
|
||||
const TOOLING_TEST_TARGETS = new Map([
|
||||
["test/scripts/barnacle-auto-response.test.ts", ["test/scripts/barnacle-auto-response.test.ts"]],
|
||||
@@ -494,10 +498,10 @@ const SOURCE_TEST_TARGETS = new Map([
|
||||
["src/auto-reply/reply/dispatch-acp-command-bypass.test.ts"],
|
||||
],
|
||||
]);
|
||||
const GENERATED_CHANGED_TEST_TARGETS = new Set([
|
||||
"extensions/canvas/src/host/a2ui/.bundle.hash",
|
||||
"extensions/canvas/src/host/a2ui/a2ui.bundle.js",
|
||||
]);
|
||||
const GENERATED_CHANGED_TEST_TARGET_PATTERNS = [
|
||||
/^extensions\/[^/]+\/src\/host\/.+\/\.bundle\.hash$/u,
|
||||
/^extensions\/[^/]+\/src\/host\/.+\/[^/]+\.bundle\.js$/u,
|
||||
];
|
||||
const SOURCE_ROOTS_FOR_IMPORT_GRAPH = ["src", "extensions", "packages", "ui/src", "test"];
|
||||
const IMPORTABLE_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
|
||||
const IMPORT_SPECIFIER_PATTERN =
|
||||
@@ -939,7 +943,7 @@ function shouldUseBroadChangedTargets(env = process.env) {
|
||||
}
|
||||
|
||||
function isRoutableChangedTarget(changedPath) {
|
||||
if (GENERATED_CHANGED_TEST_TARGETS.has(changedPath)) {
|
||||
if (GENERATED_CHANGED_TEST_TARGET_PATTERNS.some((pattern) => pattern.test(changedPath))) {
|
||||
return false;
|
||||
}
|
||||
if (changedPath.endsWith(".live.test.ts")) {
|
||||
|
||||
@@ -24,8 +24,8 @@ const ROOT_SRC = "src/index.ts";
|
||||
const ROOT_TSCONFIG = "tsconfig.json";
|
||||
const ROOT_PACKAGE = "package.json";
|
||||
const ROOT_TSDOWN = "tsdown.config.ts";
|
||||
const GENERATED_A2UI_BUNDLE = "extensions/canvas/src/host/a2ui/a2ui.bundle.js";
|
||||
const GENERATED_A2UI_BUNDLE_HASH = "extensions/canvas/src/host/a2ui/.bundle.hash";
|
||||
const GENERATED_PLUGIN_ASSET_BUNDLE = "extensions/demo/src/host/assets/view.bundle.js";
|
||||
const GENERATED_PLUGIN_ASSET_BUNDLE_HASH = "extensions/demo/src/host/assets/.bundle.hash";
|
||||
const DIST_ENTRY = "dist/entry.js";
|
||||
const BUILD_STAMP = `dist/${BUILD_STAMP_FILE}`;
|
||||
const RUNTIME_POSTBUILD_STAMP = `dist/${RUNTIME_POSTBUILD_STAMP_FILE}`;
|
||||
@@ -1478,7 +1478,7 @@ describe("run-node script", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores dirty generated A2UI bundle artifacts when dist is current", async () => {
|
||||
it("ignores dirty generated plugin bundle artifacts when dist is current", async () => {
|
||||
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
|
||||
await setupTrackedProject(tmp, {
|
||||
files: {
|
||||
@@ -1491,7 +1491,7 @@ describe("run-node script", () => {
|
||||
const requirement = resolveBuildRequirement(
|
||||
createBuildRequirementDeps(tmp, {
|
||||
gitHead: "abc123\n",
|
||||
gitStatus: ` M ${GENERATED_A2UI_BUNDLE_HASH}\n M ${GENERATED_A2UI_BUNDLE}\n`,
|
||||
gitStatus: ` M ${GENERATED_PLUGIN_ASSET_BUNDLE_HASH}\n M ${GENERATED_PLUGIN_ASSET_BUNDLE}\n`,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { copyA2uiAssets } from "../../scripts/canvas-a2ui-copy.js";
|
||||
import { copyA2uiAssets } from "../../extensions/canvas/scripts/copy-a2ui.mjs";
|
||||
import { withTempDir } from "../test-utils/temp-dir.js";
|
||||
|
||||
const ORIGINAL_SKIP_MISSING = process.env.OPENCLAW_A2UI_SKIP_MISSING;
|
||||
|
||||
@@ -53,7 +53,7 @@ function withBuildCacheFixture(
|
||||
|
||||
describe("resolveBuildAllStep", () => {
|
||||
it("routes pnpm steps through the npm_execpath pnpm runner on Windows", () => {
|
||||
const step = BUILD_ALL_STEPS.find((entry) => entry.label === "canvas:a2ui:bundle");
|
||||
const step = BUILD_ALL_STEPS.find((entry) => entry.label === "plugins:assets:build");
|
||||
expect(step).toBeTruthy();
|
||||
|
||||
const result = resolveBuildAllStep(step, {
|
||||
@@ -65,7 +65,7 @@ describe("resolveBuildAllStep", () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
command: "C:\\Program Files\\nodejs\\node.exe",
|
||||
args: ["C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs", "canvas:a2ui:bundle"],
|
||||
args: ["C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs", "plugins:assets:build"],
|
||||
options: {
|
||||
stdio: "inherit",
|
||||
env: {},
|
||||
@@ -129,7 +129,7 @@ describe("resolveBuildAllSteps", () => {
|
||||
|
||||
it("uses a runtime artifact plus plugin SDK export profile for ci artifacts", () => {
|
||||
expect(resolveBuildAllSteps("ciArtifacts").map((step) => step.label)).toEqual([
|
||||
"canvas:a2ui:bundle",
|
||||
"plugins:assets:build",
|
||||
"tsdown",
|
||||
"check-cli-bootstrap-imports",
|
||||
"runtime-postbuild",
|
||||
@@ -138,7 +138,7 @@ describe("resolveBuildAllSteps", () => {
|
||||
"build:plugin-sdk:dts",
|
||||
"write-plugin-sdk-entry-dts",
|
||||
"check-plugin-sdk-exports",
|
||||
"canvas-a2ui-copy",
|
||||
"plugins:assets:copy",
|
||||
"copy-hook-metadata",
|
||||
"copy-export-html-templates",
|
||||
"write-build-info",
|
||||
|
||||
@@ -6,17 +6,17 @@ import {
|
||||
getBundleHashRepoInputPaths,
|
||||
getLocalRolldownCliCandidates,
|
||||
isBundleHashInputPath,
|
||||
} from "../../scripts/bundle-a2ui.mjs";
|
||||
} from "../../extensions/canvas/scripts/bundle-a2ui.mjs";
|
||||
|
||||
describe("scripts/bundle-a2ui.mjs", () => {
|
||||
it("uses package metadata and CanvasA2UI sources as bundle hash inputs", () => {
|
||||
it("uses package metadata and plugin-owned A2UI sources as bundle hash inputs", () => {
|
||||
const repoRoot = path.resolve("repo-root");
|
||||
const inputPaths = getBundleHashRepoInputPaths(repoRoot);
|
||||
|
||||
expect(inputPaths).toContain(path.join(repoRoot, "package.json"));
|
||||
expect(inputPaths).toContain(path.join(repoRoot, "pnpm-lock.yaml"));
|
||||
expect(inputPaths).toContain(
|
||||
path.join(repoRoot, "apps", "shared", "OpenClawKit", "Tools", "CanvasA2UI"),
|
||||
path.join(repoRoot, "extensions", "canvas", "src", "host", "a2ui-app"),
|
||||
);
|
||||
expect(inputPaths).not.toContain(path.join(repoRoot, "vendor", "a2ui", "renderers", "lit"));
|
||||
expect(isBundleHashInputPath(path.join(repoRoot, "package.json"), repoRoot)).toBe(true);
|
||||
|
||||
73
test/scripts/bundled-plugin-assets.test.ts
Normal file
73
test/scripts/bundled-plugin-assets.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
parseBundledPluginAssetArgs,
|
||||
readBundledPluginAssetHooks,
|
||||
} from "../../scripts/bundled-plugin-assets.mjs";
|
||||
|
||||
async function withPluginAssetFixture(run: (rootDir: string) => Promise<void>) {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-assets-"));
|
||||
try {
|
||||
fs.mkdirSync(path.join(rootDir, "extensions", "canvas"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "extensions", "canvas", "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/canvas-plugin",
|
||||
openclaw: {
|
||||
assetScripts: {
|
||||
build: "node scripts/bundle-a2ui.mjs",
|
||||
copy: "node scripts/copy-a2ui.mjs",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "extensions", "canvas", "openclaw.plugin.json"),
|
||||
JSON.stringify({ id: "canvas" }, null, 2),
|
||||
);
|
||||
await run(rootDir);
|
||||
} finally {
|
||||
fs.rmSync(rootDir, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("bundled plugin assets", () => {
|
||||
it("discovers plugin-owned asset scripts by manifest id", async () => {
|
||||
await withPluginAssetFixture(async (rootDir) => {
|
||||
const hooks = await readBundledPluginAssetHooks({
|
||||
phase: "build",
|
||||
plugins: ["canvas"],
|
||||
rootDir,
|
||||
});
|
||||
|
||||
expect(hooks).toMatchObject([
|
||||
{
|
||||
command: "node scripts/bundle-a2ui.mjs",
|
||||
phase: "build",
|
||||
pluginId: "canvas",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("skips cleanly when a requested plugin is absent", async () => {
|
||||
await withPluginAssetFixture(async (rootDir) => {
|
||||
await expect(
|
||||
readBundledPluginAssetHooks({ phase: "copy", plugins: ["missing"], rootDir }),
|
||||
).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("parses phase and plugin filters", () => {
|
||||
expect(parseBundledPluginAssetArgs(["--phase", "build", "--plugin=canvas"])).toEqual({
|
||||
phase: "build",
|
||||
plugins: ["canvas"],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -853,21 +853,19 @@ describe("scripts/changed-lanes", () => {
|
||||
expect(plan.commands.map((command) => command.args[0])).not.toContain("tsgo:all");
|
||||
});
|
||||
|
||||
it("routes CanvasA2UI bundle changes to core and tooling instead of all lanes", () => {
|
||||
it("routes A2UI bundle source changes as extension changes", () => {
|
||||
const result = detectChangedLanes([
|
||||
"apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js",
|
||||
"apps/shared/OpenClawKit/Tools/CanvasA2UI/rolldown.config.mjs",
|
||||
"extensions/canvas/src/host/a2ui-app/bootstrap.js",
|
||||
"extensions/canvas/src/host/a2ui-app/rolldown.config.mjs",
|
||||
]);
|
||||
const plan = createChangedCheckPlan(result);
|
||||
|
||||
expect(result.lanes).toMatchObject({
|
||||
core: true,
|
||||
coreTests: true,
|
||||
tooling: true,
|
||||
extensions: true,
|
||||
extensionTests: true,
|
||||
all: false,
|
||||
});
|
||||
expect(plan.commands.map((command) => command.args[0])).toContain("lint:scripts");
|
||||
expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:core");
|
||||
expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:extensions");
|
||||
expect(plan.commands.map((command) => command.args[0])).not.toContain("tsgo:all");
|
||||
});
|
||||
|
||||
@@ -887,9 +885,9 @@ describe("scripts/changed-lanes", () => {
|
||||
expect(plan.commands.map((command) => command.args[0])).not.toContain("test");
|
||||
});
|
||||
|
||||
it("does not route generated A2UI artifacts as direct Vitest targets", () => {
|
||||
it("does not route generated plugin bundle artifacts as direct Vitest targets", () => {
|
||||
const result = detectChangedLanes([
|
||||
"extensions/canvas/src/host/a2ui/.bundle.hash",
|
||||
"extensions/demo/src/host/assets/.bundle.hash",
|
||||
"test/scripts/bundle-a2ui.test.ts",
|
||||
]);
|
||||
const plan = createChangedCheckPlan(result);
|
||||
|
||||
Reference in New Issue
Block a user