Skip to content

Commit 178a36c

Browse files
authored
fix: prevent publishing unresolved workspace imports (#1577)
1 parent 761317d commit 178a36c

5 files changed

Lines changed: 67 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ jobs:
191191
npm install --global --prefix "$prefix" --ignore-scripts "$tarball"
192192
"$prefix/bin/agent-device" --version
193193
"$prefix/bin/agent-device" help
194+
"$prefix/bin/agent-device" devices --json
194195
"$prefix/bin/agent-device" doctor --remote --json
195196
196197
fallow:

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
"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",
104104
"build:macos-helper": "swift build -c release --package-path apple/macos-helper",
105105
"build:macos-helper:clean": "swift package --package-path apple/macos-helper clean && pnpm build:macos-helper",
106-
"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",
106+
"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",
107107
"ad": "node bin/agent-device.mjs",
108108
"bench:help-conformance": "node scripts/help-conformance-bench.mjs",
109109
"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,14 +134,15 @@
134134
"depgraph:test": "node --experimental-strip-types --test scripts/depgraph/model.test.ts scripts/depgraph/affected.test.ts",
135135
"check:production-exports": "fallow dead-code --config fallow-production-exports.json --production --unused-exports --fail-on-issues",
136136
"check:bundle-owner-files": "node --experimental-strip-types scripts/check-bundle-owner-files.ts",
137+
"check:bundle-dependencies": "node --experimental-strip-types scripts/check-bundle-dependencies.ts",
137138
"check:command-docs": "vitest run --project unit-core src/__tests__/command-doc-coverage.test.ts",
138139
"check:replay-compat": "node --experimental-strip-types scripts/check-replay-compat-provenance.ts",
139140
"check:freerange": "fr",
140141
"check:quick": "pnpm lint && pnpm typecheck",
141142
"sync:mcp-metadata": "node scripts/sync-mcp-metadata.mjs",
142143
"check:mcp-metadata": "node scripts/sync-mcp-metadata.mjs --check",
143144
"version": "pnpm sync:mcp-metadata && git add server.json",
144-
"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",
145+
"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",
145146
"check:unit": "pnpm check:contention-retry && pnpm test:unit && pnpm test:smoke",
146147
"check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit",
147148
"prepack": "pnpm check:mcp-metadata && pnpm package:npm",
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { parseSync } from 'oxc-parser';
4+
import { walkFiles } from './lib/walk-files.ts';
5+
6+
const repoRoot = path.resolve(import.meta.dirname, '..');
7+
const distRoot = path.join(repoRoot, 'dist', 'src');
8+
9+
function moduleSpecifiers(file: string, source: string): string[] {
10+
const record = parseSync(file, source).module;
11+
return [
12+
...record.staticImports.map((entry) => entry.moduleRequest.value),
13+
...record.staticExports.flatMap((entry) =>
14+
entry.entries.flatMap((exported) => moduleRequestValue(exported.moduleRequest)),
15+
),
16+
...record.dynamicImports.flatMap((entry) =>
17+
dynamicModuleRequestValue(source, entry.moduleRequest),
18+
),
19+
];
20+
}
21+
22+
function moduleRequestValue(request: { value?: string } | undefined): string[] {
23+
return request?.value ? [request.value] : [];
24+
}
25+
26+
function dynamicModuleRequestValue(
27+
source: string,
28+
request: { start: number; end: number },
29+
): string[] {
30+
const raw = source.slice(request.start, request.end);
31+
const literal = /^(['"])([^'"]*)\1$/.exec(raw);
32+
return literal?.[2] ? [literal[2]] : [];
33+
}
34+
35+
const bundleFiles = walkFiles(distRoot).filter(
36+
(file) => file.endsWith('.js') || file.endsWith('.d.ts'),
37+
);
38+
if (bundleFiles.length === 0) {
39+
throw new Error('No dist/src JavaScript files found. Run `pnpm build` first.');
40+
}
41+
42+
const leaks = bundleFiles.flatMap((file) => {
43+
const source = fs.readFileSync(file, 'utf8');
44+
return moduleSpecifiers(file, source)
45+
.filter((specifier) => specifier.startsWith('@agent-device/'))
46+
.map((specifier) => ({ file: path.relative(repoRoot, file), specifier }));
47+
});
48+
49+
if (leaks.length > 0) {
50+
const details = leaks.map(({ file, specifier }) => `- ${specifier} in ${file}`).join('\n');
51+
throw new Error(
52+
`Private workspace dependencies escaped the production bundle:\n${details}\n` +
53+
'Published installs cannot resolve private @agent-device packages.',
54+
);
55+
}
56+
57+
process.stdout.write(
58+
`Verified ${bundleFiles.length} production module files contain no private workspace imports.\n`,
59+
);

src/__tests__/npm-package-scripts.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ test('Fallow exposes one changed-code gate and an explicit full-tree audit', ()
5858
test('the npm package build covers every package-owned build output', () => {
5959
assert.deepEqual(script('package:npm').split(' && '), [
6060
'pnpm build',
61+
'pnpm check:bundle-dependencies',
6162
'pnpm build:xcuitest:ios',
6263
'pnpm build:xcuitest:macos',
6364
'pnpm build:xcuitest:tvos',

tsdown.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ export default defineConfig({
7373
'internal/png-worker': 'src/utils/png-worker.ts',
7474
'internal/update-check-entry': 'src/utils/update-check-entry.ts',
7575
},
76-
noExternal: [/^@agent-device\//, 'pngjs'],
76+
deps: {
77+
alwaysBundle: [/^@agent-device\//, 'pngjs'],
78+
},
7779
format: 'esm',
7880
platform: 'node',
7981
target: 'es2022',

0 commit comments

Comments
 (0)