diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 416d2e37a..ec468b1d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,6 +191,7 @@ jobs: npm install --global --prefix "$prefix" --ignore-scripts "$tarball" "$prefix/bin/agent-device" --version "$prefix/bin/agent-device" help + "$prefix/bin/agent-device" devices --json "$prefix/bin/agent-device" doctor --remote --json fallow: diff --git a/package.json b/package.json index fc7210de4..756b67588 100644 --- a/package.json +++ b/package.json @@ -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-dependencies && 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", @@ -134,6 +134,7 @@ "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-owner-files": "node --experimental-strip-types scripts/check-bundle-owner-files.ts", + "check:bundle-dependencies": "node --experimental-strip-types scripts/check-bundle-dependencies.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", @@ -141,7 +142,7 @@ "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-owner-files && pnpm check:bundle-dependencies", "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", diff --git a/scripts/check-bundle-dependencies.ts b/scripts/check-bundle-dependencies.ts new file mode 100644 index 000000000..691e974ce --- /dev/null +++ b/scripts/check-bundle-dependencies.ts @@ -0,0 +1,59 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { parseSync } from 'oxc-parser'; +import { walkFiles } from './lib/walk-files.ts'; + +const repoRoot = path.resolve(import.meta.dirname, '..'); +const distRoot = path.join(repoRoot, 'dist', 'src'); + +function moduleSpecifiers(file: string, source: string): string[] { + const record = parseSync(file, source).module; + return [ + ...record.staticImports.map((entry) => entry.moduleRequest.value), + ...record.staticExports.flatMap((entry) => + entry.entries.flatMap((exported) => moduleRequestValue(exported.moduleRequest)), + ), + ...record.dynamicImports.flatMap((entry) => + dynamicModuleRequestValue(source, entry.moduleRequest), + ), + ]; +} + +function moduleRequestValue(request: { value?: string } | undefined): string[] { + return request?.value ? [request.value] : []; +} + +function dynamicModuleRequestValue( + source: string, + request: { start: number; end: number }, +): string[] { + const raw = source.slice(request.start, request.end); + const literal = /^(['"])([^'"]*)\1$/.exec(raw); + return literal?.[2] ? [literal[2]] : []; +} + +const bundleFiles = walkFiles(distRoot).filter( + (file) => file.endsWith('.js') || file.endsWith('.d.ts'), +); +if (bundleFiles.length === 0) { + throw new Error('No dist/src JavaScript files found. Run `pnpm build` first.'); +} + +const leaks = bundleFiles.flatMap((file) => { + const source = fs.readFileSync(file, 'utf8'); + return moduleSpecifiers(file, source) + .filter((specifier) => specifier.startsWith('@agent-device/')) + .map((specifier) => ({ file: path.relative(repoRoot, file), specifier })); +}); + +if (leaks.length > 0) { + const details = leaks.map(({ file, specifier }) => `- ${specifier} in ${file}`).join('\n'); + throw new Error( + `Private workspace dependencies escaped the production bundle:\n${details}\n` + + 'Published installs cannot resolve private @agent-device packages.', + ); +} + +process.stdout.write( + `Verified ${bundleFiles.length} production module files contain no private workspace imports.\n`, +); diff --git a/src/__tests__/npm-package-scripts.test.ts b/src/__tests__/npm-package-scripts.test.ts index 3ec791ca3..4179ad2ed 100644 --- a/src/__tests__/npm-package-scripts.test.ts +++ b/src/__tests__/npm-package-scripts.test.ts @@ -58,6 +58,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-dependencies', 'pnpm build:xcuitest:ios', 'pnpm build:xcuitest:macos', 'pnpm build:xcuitest:tvos', diff --git a/tsdown.config.ts b/tsdown.config.ts index 245d0b6e9..0f27d7a21 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -73,7 +73,9 @@ export default defineConfig({ 'internal/png-worker': 'src/utils/png-worker.ts', 'internal/update-check-entry': 'src/utils/update-check-entry.ts', }, - noExternal: [/^@agent-device\//, 'pngjs'], + deps: { + alwaysBundle: [/^@agent-device\//, 'pngjs'], + }, format: 'esm', platform: 'node', target: 'es2022',