diff --git a/.changeset/fix-vitest-path-aliases.md b/.changeset/fix-vitest-path-aliases.md new file mode 100644 index 0000000000..295b0031c2 --- /dev/null +++ b/.changeset/fix-vitest-path-aliases.md @@ -0,0 +1,5 @@ +--- +"@workflow/builders": patch +--- + +Resolve path aliases when externalizing non-step imports diff --git a/packages/builders/src/swc-esbuild-plugin.test.ts b/packages/builders/src/swc-esbuild-plugin.test.ts index 66435071d4..9725e3f113 100644 --- a/packages/builders/src/swc-esbuild-plugin.test.ts +++ b/packages/builders/src/swc-esbuild-plugin.test.ts @@ -85,6 +85,77 @@ describe('createSwcPlugin externalizeNonSteps', () => { expect(output).not.toContain(`/dep${inputExt}`); }); + it('rewrites path-aliased imports to relative paths', async () => { + const outdir = join(testRoot, 'out'); + const srcDir = join(testRoot, 'src'); + const libDir = join(srcDir, 'lib'); + const stepFile = join(srcDir, 'step.ts'); + + writeFile(join(libDir, 'config.ts'), 'export const config = {};'); + writeFile( + stepFile, + `import { config } from '@/lib/config';\nconsole.log(config);` + ); + + const result = await esbuild.build({ + entryPoints: [stepFile], + absWorkingDir: testRoot, + outdir, + bundle: true, + format: 'esm', + platform: 'node', + write: false, + alias: { '@': srcDir }, + plugins: [ + createSwcPlugin({ + mode: 'step', + entriesToBundle: [stepFile], + outdir, + rewriteTsExtensions: true, + }), + ], + }); + + expect(result.errors).toHaveLength(0); + const output = result.outputFiles[0].text; + expect(output).toContain('/lib/config.js'); + expect(output).not.toContain('@/lib/config'); + }); + + it('does not externalize aliased imports that resolve into node_modules', async () => { + const outdir = join(testRoot, 'out'); + const srcDir = join(testRoot, 'src'); + const stepFile = join(srcDir, 'step.ts'); + const nodeModulesDir = join(testRoot, 'node_modules', 'some-pkg'); + + writeFile(join(nodeModulesDir, 'index.js'), 'export const pkg = "hello";'); + writeFile(stepFile, `import { pkg } from '@pkg';\nconsole.log(pkg);`); + + const result = await esbuild.build({ + entryPoints: [stepFile], + absWorkingDir: testRoot, + outdir, + bundle: true, + format: 'esm', + platform: 'node', + write: false, + alias: { '@pkg': join(nodeModulesDir, 'index.js') }, + plugins: [ + createSwcPlugin({ + mode: 'step', + entriesToBundle: [stepFile], + outdir, + }), + ], + }); + + expect(result.errors).toHaveLength(0); + const output = result.outputFiles[0].text; + // Should be bundled (inlined), not externalized as a relative node_modules path + expect(output).toContain('hello'); + expect(output).not.toMatch(/from\s+["'].*node_modules/); + }); + it.each([ '.ts', '.tsx', diff --git a/packages/builders/src/swc-esbuild-plugin.ts b/packages/builders/src/swc-esbuild-plugin.ts index 49347b5057..cc493f8f08 100644 --- a/packages/builders/src/swc-esbuild-plugin.ts +++ b/packages/builders/src/swc-esbuild-plugin.ts @@ -106,6 +106,8 @@ export function createSwcPlugin(options: SwcPluginOptions): Plugin { ); build.onResolve({ filter: /.*/ }, async (args) => { + if (args.pluginData?.skipSwcPlugin) return null; + if ( !options.entriesToBundle && normalizedSideEffectEntries.size === 0 @@ -122,21 +124,44 @@ export function createSwcPlugin(options: SwcPluginOptions): Plugin { } try { - let resolvedPath: string | false | undefined = args.path; + const specifier = args.path; + const specifierIsPath = + specifier.startsWith('.') || specifier.startsWith('/'); + + let resolvedPath: string | false | undefined; + // Determines whether the external path should be relativized + // (project-local file) or kept as a bare specifier (npm package). + let shouldMakeRelative = specifierIsPath; - // handle local imports e.g. ./hello or ../another - if (args.path.startsWith('.')) { - resolvedPath = await enhancedResolve(args.resolveDir, args.path); + if (specifierIsPath) { + resolvedPath = await enhancedResolve(args.resolveDir, specifier); } else { + // Resolve from project root so nested deps aren't externalized resolvedPath = await enhancedResolve( - // `args.resolveDir` is not used here to ensure we only - // externalize packages that can be resolved in the - // project's working directory e.g. a nested dep can't - // be externalized as we won't be able to resolve it once - // it's parent has been bundled build.initialOptions.absWorkingDir || process.cwd(), - args.path - ); + specifier + ).catch(() => undefined); // swallow so esbuild fallback below can try + + // Fall back to esbuild for aliases/tsconfig paths, + // but only accept project-local results + if (!resolvedPath) { + const esbuildResult = await build.resolve(specifier, { + resolveDir: args.resolveDir, + kind: args.kind, + pluginData: { skipSwcPlugin: true }, + }); + const didResolve = + !!esbuildResult.path && !esbuildResult.errors.length; + const isProjectLocalFile = + didResolve && + !esbuildResult.path + .replace(/\\/g, '/') + .includes('/node_modules/'); + if (isProjectLocalFile) { + resolvedPath = esbuildResult.path; + shouldMakeRelative = true; + } + } } if (!resolvedPath) return null; @@ -182,11 +207,8 @@ export function createSwcPlugin(options: SwcPluginOptions): Plugin { : null; } - const isFilePath = - args.path.startsWith('.') || args.path.startsWith('/'); - let externalPath: string; - if (isFilePath) { + if (shouldMakeRelative) { externalPath = relative( options.outdir || process.cwd(), resolvedPath @@ -201,7 +223,7 @@ export function createSwcPlugin(options: SwcPluginOptions): Plugin { .replace(/\.cts$/, '.cjs'); } } else { - externalPath = args.path; + externalPath = specifier; } return {