Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ jobs:
- name: Build and pack CLI
run: |
pnpm build
pnpm check:bundle-owner-files
pnpm check:bundle
mkdir -p .tmp/node-compat
npm pack --ignore-scripts --pack-destination .tmp/node-compat

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/package-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
- 'scripts/build-xcuitest-apple.sh'
- 'scripts/package-android-helper.sh'
- 'scripts/package-apple-runner-source.mjs'
- 'scripts/check-bundle-*.ts'
- 'scripts/patch-xcuitest-runner-icon.ts'
- 'scripts/sync-mcp-metadata.mjs'
- 'scripts/write-xcuitest-cache-metadata.mjs'
Expand All @@ -34,6 +35,7 @@ on:
- 'scripts/build-xcuitest-apple.sh'
- 'scripts/package-android-helper.sh'
- 'scripts/package-apple-runner-source.mjs'
- 'scripts/check-bundle-*.ts'
- 'scripts/patch-xcuitest-runner-icon.ts'
- 'scripts/sync-mcp-metadata.mjs'
- 'scripts/write-xcuitest-cache-metadata.mjs'
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
"package:android-ime-helper:npm": "rm -rf android/ime-helper/dist && AGENT_DEVICE_ANDROID_HELPER=ime sh ./scripts/package-android-helper.sh $(node -p \"require('./package.json').version\") android/ime-helper/dist",
"build:macos-helper": "swift build -c release --package-path apple/macos-helper",
"build:macos-helper:clean": "swift package --package-path apple/macos-helper clean && pnpm build:macos-helper",
"package:npm": "pnpm build && pnpm build:xcuitest:ios && pnpm build:xcuitest:macos && pnpm build:xcuitest:tvos && pnpm build:xcuitest:visionos && pnpm build:macos-helper:clean && pnpm package:apple-runner:npm && pnpm build:android",
"package:npm": "pnpm build && pnpm check:bundle && pnpm build:xcuitest:ios && pnpm build:xcuitest:macos && pnpm build:xcuitest:tvos && pnpm build:xcuitest:visionos && pnpm build:macos-helper:clean && pnpm package:apple-runner:npm && pnpm build:android",
"ad": "node bin/agent-device.mjs",
"bench:help-conformance": "node scripts/help-conformance-bench.mjs",
"maestro:conformance": "node --experimental-strip-types --test packages/maestro/test/conformance/verify.test.ts packages/maestro/test/conformance/differential/run.test.ts packages/maestro/test/conformance/differential/invariants.test.ts",
Expand Down Expand Up @@ -133,15 +133,17 @@
"depgraph": "node --experimental-strip-types scripts/depgraph/build.ts",
"depgraph:test": "node --experimental-strip-types --test scripts/depgraph/model.test.ts scripts/depgraph/affected.test.ts",
"check:production-exports": "fallow dead-code --config fallow-production-exports.json --production --unused-exports --fail-on-issues",
"check:bundle": "pnpm check:bundle-owner-files && pnpm check:bundle-private-imports",
"check:bundle-owner-files": "node --experimental-strip-types scripts/check-bundle-owner-files.ts",
"check:bundle-private-imports": "node --experimental-strip-types scripts/check-bundle-private-imports.ts",
"check:command-docs": "vitest run --project unit-core src/__tests__/command-doc-coverage.test.ts",
"check:replay-compat": "node --experimental-strip-types scripts/check-replay-compat-provenance.ts",
"check:freerange": "fr",
"check:quick": "pnpm lint && pnpm typecheck",
"sync:mcp-metadata": "node scripts/sync-mcp-metadata.mjs",
"check:mcp-metadata": "node scripts/sync-mcp-metadata.mjs --check",
"version": "pnpm sync:mcp-metadata && git add server.json",
"check:tooling": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm check:layering && pnpm depgraph:test && pnpm check:production-exports && pnpm check:mcp-metadata && pnpm build && pnpm check:bundle-owner-files",
"check:tooling": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm check:layering && pnpm depgraph:test && pnpm check:production-exports && pnpm check:mcp-metadata && pnpm build && pnpm check:bundle",
"check:unit": "pnpm check:contention-retry && pnpm test:unit && pnpm test:smoke",
"check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit",
"prepack": "pnpm check:mcp-metadata && pnpm package:npm",
Expand Down
65 changes: 65 additions & 0 deletions scripts/check-bundle-private-imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import fs from 'node:fs';
import path from 'node:path';
import { runAsMain } from './lib/run-as-main.ts';
import { walkFiles } from './lib/walk-files.ts';

const PRIVATE_WORKSPACE_IMPORT_RE =
/(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s*|\brequire\s*\(\s*|\bimport\.meta\.resolve\s*\(\s*)(["'])(@agent-device\/[^"'\\]+)\1/g;

export type BundleSource = {
file: string;
content: string;
};

export type PrivateWorkspaceImportLeak = {
file: string;
specifier: string;
};

export function findPrivateWorkspaceImportLeaks(
sources: readonly BundleSource[],
): PrivateWorkspaceImportLeak[] {
const seen = new Set<string>();
const leaks: PrivateWorkspaceImportLeak[] = [];
for (const source of sources) {
for (const match of source.content.matchAll(PRIVATE_WORKSPACE_IMPORT_RE)) {
const specifier = match[2];
if (!specifier) continue;
const key = `${source.file}\0${specifier}`;
if (seen.has(key)) continue;
seen.add(key);
leaks.push({ file: source.file, specifier });
}
}
return leaks;
}

function run(): number {
const repoRoot = path.resolve(import.meta.dirname, '..');
const distRoot = path.join(repoRoot, 'dist', 'src');
const bundleFiles = walkFiles(distRoot).filter((file) => file.endsWith('.js'));
if (bundleFiles.length === 0) {
throw new Error('No dist/src JavaScript files found. Run `pnpm build` first.');
}

const leaks = findPrivateWorkspaceImportLeaks(
bundleFiles.map((file) => ({
file: path.relative(repoRoot, file),
content: fs.readFileSync(file, 'utf8'),
})),
);
if (leaks.length > 0) {
const details = leaks.map(({ file, specifier }) => `- ${specifier} in ${file}`).join('\n');
throw new Error(
`Private workspace imports leaked into production bundles:\n${details}\n` +
'Run `pnpm install` so workspace packages resolve, then rebuild before publishing.',
);
}

process.stdout.write(
`Verified ${bundleFiles.length} production bundles contain no private workspace imports.\n`,
);
return 0;
}

runAsMain(import.meta.url, 'check:bundle-private-imports', run);
42 changes: 42 additions & 0 deletions src/__tests__/bundle-private-imports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import { findPrivateWorkspaceImportLeaks } from '../../scripts/check-bundle-private-imports.ts';

test('finds private workspace package imports left in emitted chunks', () => {
const leaks = findPrivateWorkspaceImportLeaks([
{
file: 'dist/src/sdk-selectors.js',
content: [
'import{x}from"@agent-device/ad-script";',
'const replay=import("@agent-device/ad-replay");',
'const duplicate=import("@agent-device/ad-script");',
].join(''),
},
{
file: 'dist/src/daemon.js',
content: 'const script=require("@agent-device/ad-script/internal");',
},
]);

assert.deepEqual(leaks, [
{ file: 'dist/src/sdk-selectors.js', specifier: '@agent-device/ad-script' },
{ file: 'dist/src/sdk-selectors.js', specifier: '@agent-device/ad-replay' },
{ file: 'dist/src/daemon.js', specifier: '@agent-device/ad-script/internal' },
]);
});

test('ignores bundled mentions and external runtime dependencies', () => {
assert.deepEqual(
findPrivateWorkspaceImportLeaks([
{
file: 'dist/src/cli.js',
content: [
'const diagnostic="@agent-device/ad-script";',
'import{stringify}from"yaml";',
'const scope="@agent-device-example/public";',
].join(''),
},
]),
[],
);
});
6 changes: 6 additions & 0 deletions src/__tests__/npm-package-scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ test('prepack builds the complete package without stopping the development daemo
for (const input of [
'pnpm-workspace.yaml',
'packages/**',
'scripts/check-bundle-*.ts',
'scripts/patch-xcuitest-runner-icon.ts',
'scripts/sync-mcp-metadata.mjs',
'scripts/write-xcuitest-cache-metadata.mjs',
Expand Down Expand Up @@ -58,6 +59,7 @@ test('Fallow exposes one changed-code gate and an explicit full-tree audit', ()
test('the npm package build covers every package-owned build output', () => {
assert.deepEqual(script('package:npm').split(' && '), [
'pnpm build',
'pnpm check:bundle',
'pnpm build:xcuitest:ios',
'pnpm build:xcuitest:macos',
'pnpm build:xcuitest:tvos',
Expand All @@ -66,6 +68,10 @@ test('the npm package build covers every package-owned build output', () => {
'pnpm package:apple-runner:npm',
'pnpm build:android',
]);
assert.deepEqual(script('check:bundle').split(' && '), [
'pnpm check:bundle-owner-files',
'pnpm check:bundle-private-imports',
]);

assert.deepEqual(script('build:android').split(' && '), [
'pnpm package:android-snapshot-helper:npm',
Expand Down
Loading