Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/fix-transitive-local-dep-bundling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/builders': patch
---

Fix transitive local TS dependencies being externalized in step bundles instead of bundled
52 changes: 52 additions & 0 deletions packages/builders/src/discover-entries-esbuild-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,58 @@ describe('createDiscoverEntriesPlugin projectRoot', () => {
);
});

it('tracks extensionless relative imports in the import graph', async () => {
const workflowFile = join(
testRoot,
'server',
'workflows',
'my-workflow.ts'
);
const constantsFile = join(testRoot, 'shared', 'constants.ts');
const helpersFile = join(testRoot, 'shared', 'helpers.ts');

writeFile(helpersFile, `export const HELLO = "world";`);
writeFile(
constantsFile,
`import { HELLO } from './helpers';\nexport const MSG = HELLO;`
);
writeFile(
workflowFile,
`import { MSG } from '../../shared/constants';\nexport function myWorkflow() {\n "use workflow";\n return MSG;\n}`
);

const state = {
discoveredSteps: new Set<string>(),
discoveredWorkflows: new Set<string>(),
discoveredSerdeFiles: new Set<string>(),
};

await esbuild.build({
entryPoints: [workflowFile],
absWorkingDir: testRoot,
bundle: true,
format: 'esm',
platform: 'node',
write: false,
resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
plugins: [createDiscoverEntriesPlugin(state, testRoot)],
});

const normalizedWorkflow = normalizeSlashes(workflowFile);
const normalizedConstants = normalizeSlashes(constantsFile);
const normalizedHelpers = normalizeSlashes(helpersFile);

// my-workflow.ts -> shared/constants.ts (extensionless import)
const workflowChildren = importParents.get(normalizedWorkflow);
expect(workflowChildren).toBeDefined();
expect(workflowChildren!.has(normalizedConstants)).toBe(true);

// shared/constants.ts -> shared/helpers.ts (extensionless import)
const constantsChildren = importParents.get(normalizedConstants);
expect(constantsChildren).toBeDefined();
expect(constantsChildren!.has(normalizedHelpers)).toBe(true);
});

it('defaults discovery transforms to absWorkingDir when projectRoot is omitted', async () => {
const fixture = setupFixture();
const normalizedWorkflowFile = normalizeSlashes(fixture.workflowFile);
Expand Down
23 changes: 22 additions & 1 deletion packages/builders/src/discover-entries-esbuild-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,28 @@ import {
isGeneratedWorkflowFile,
} from './transform-utils.js';

const enhancedResolve = promisify(enhancedResolveOriginal);
const enhancedResolve = promisify(
enhancedResolveOriginal.create({
extensions: [
'.ts',
'.tsx',
'.mts',
'.cts',
'.cjs',
'.mjs',
'.js',
'.jsx',
'.json',
'.node',
],
mainFields: ['main'],
mainFiles: ['index'],
conditionNames: ['node', 'import'],
// Match swc-esbuild-plugin's resolver so both plugins resolve the same
// paths — important for parentHasChild() graph lookups.
symlinks: true,
})
);

export const jsTsRegex = /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;

Expand Down
67 changes: 67 additions & 0 deletions packages/builders/src/swc-esbuild-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ vi.mock('./apply-swc-transform.js', () => ({
}));

import { createSwcPlugin } from './swc-esbuild-plugin.js';
import {
importParents,
createDiscoverEntriesPlugin,
} from './discover-entries-esbuild-plugin.js';

const realTmpdir = realpathSync(tmpdir());

Expand Down Expand Up @@ -226,6 +230,69 @@ describe('createSwcPlugin externalizeNonSteps', () => {
const output = result.outputFiles[0].text;
expect(output).toContain(`/dep${inputExt}`);
});

it('bundles transitive local dependencies of entries instead of externalizing them', async () => {
const outdir = join(testRoot, 'out');
const stepFile = join(testRoot, 'server', 'workflows', 'my-step.ts');
const constantsFile = join(testRoot, 'shared', 'constants.ts');
const helpersFile = join(testRoot, 'shared', 'helpers.ts');

writeFile(helpersFile, `export const HELLO = "world";`);
writeFile(
constantsFile,
`import { HELLO } from './helpers';\nexport const MSG = HELLO;`
);
writeFile(
stepFile,
`import { MSG } from '../../shared/constants';\nconsole.log(MSG);`
);

// Run discovery first to populate importParents
importParents.clear();
const state = {
discoveredSteps: new Set<string>(),
discoveredWorkflows: new Set<string>(),
discoveredSerdeFiles: new Set<string>(),
};
await esbuild.build({
entryPoints: [stepFile],
absWorkingDir: testRoot,
bundle: true,
format: 'esm',
platform: 'node',
write: false,
resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
plugins: [createDiscoverEntriesPlugin(state, testRoot)],
});

// Now build with swc plugin — transitive deps should be bundled
const result = await esbuild.build({
entryPoints: [stepFile],
absWorkingDir: testRoot,
outdir,
bundle: true,
format: 'esm',
platform: 'node',
write: false,
resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
plugins: [
createSwcPlugin({
mode: 'step',
entriesToBundle: [stepFile],
outdir,
}),
],
});

expect(result.errors).toHaveLength(0);
const output = result.outputFiles[0].text;
// Both constants.ts and helpers.ts should be inlined, not externalized
expect(output).toContain('world');
expect(output).not.toContain('../shared/constants');
expect(output).not.toContain('./helpers');

importParents.clear();
});
});

describe('createSwcPlugin sideEffectEntries', () => {
Expand Down
9 changes: 8 additions & 1 deletion packages/builders/src/swc-esbuild-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,20 @@ export function createSwcPlugin(options: SwcPluginOptions): Plugin {
break;
}

// if the current entry imports a child that needs
// if the current file imports a child that needs
// to be bundled then it needs to also be bundled so
// that the child can have our transform applied
if (parentHasChild(normalizedResolvedPath, normalizedEntry)) {
shouldBundle = true;
break;
}

// if an entry transitively imports this file, bundle it
// so the step bundle is self-contained for local deps
if (parentHasChild(normalizedEntry, normalizedResolvedPath)) {
shouldBundle = true;
break;
}
}

if (shouldBundle) {
Expand Down
Loading