Skip to content

Commit

Permalink
feat(test-ssr): add custom esbuild-plugin to properly resolve TS path…
Browse files Browse the repository at this point in the history
… aliases which stopped working starting esbuild 0.19 (#31227)
  • Loading branch information
Hotell authored Apr 30, 2024
1 parent 43ffb44 commit 25027bf
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 22 deletions.
2 changes: 2 additions & 0 deletions scripts/test-ssr/src/commands/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ export async function main(params: MainParams) {
await buildAssets({
chromeVersion: CHROME_VERSION,

distDirectory,

esmEntryPoint,
cjsEntryPoint,

Expand Down
164 changes: 143 additions & 21 deletions scripts/test-ssr/src/utils/buildAssets.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';

import { stripIndents } from '@nx/devkit';
import * as tmp from 'tmp';

import { buildAssets } from './buildAssets';
Expand All @@ -9,44 +10,165 @@ function stripComments(content: string): string {
return content.replace(/\/\/ .+/g, '');
}

describe('buildAssets', () => {
it('compiles code to CJS & ESM', async () => {
const template = `export const Foo = 'foo'`;
async function setup(code: string) {
const filesDir = tmp.dirSync({ unsafeCleanup: true }).name;

const cjsEntryPoint = path.resolve(filesDir, 'cjs.js');
const esmEntryPoint = path.resolve(filesDir, 'esm.js');

const cjsOutfile = path.resolve(filesDir, 'cjs-out.js');
const esmOutfile = path.resolve(filesDir, 'esm-out.js');

await fs.promises.writeFile(cjsEntryPoint, code);
await fs.promises.writeFile(esmEntryPoint, code);

await fs.promises.mkdir(path.resolve(filesDir, 'packages/hello'), { recursive: true });
await fs.promises.writeFile(path.resolve(filesDir, 'packages/hello/index.ts'), `export const hello = 'hello';`);

const filesDir = tmp.dirSync({ unsafeCleanup: true }).name;
const tsConfigPath = path.resolve(filesDir, 'tsconfig.json');
const tsConfigContent = {
compilerOptions: {
baseUrl: '.',
paths: {
'@proj/hello': ['packages/hello/index.ts'],
},
},
};

const cjsEntryPoint = path.resolve(filesDir, 'cjs.js');
const esmEntryPoint = path.resolve(filesDir, 'esm.js');
await fs.promises.writeFile(tsConfigPath, JSON.stringify(tsConfigContent, null, 2), { encoding: 'utf8' });

const getCjsContent = async () =>
stripIndents`${stripComments(await fs.promises.readFile(cjsOutfile, { encoding: 'utf8' }))}`;
const getEsmContent = async () =>
stripIndents`${stripComments(await fs.promises.readFile(esmOutfile, { encoding: 'utf8' }))}`;

const getCjsContentWithoutHelpers = (content: string) => content.split('\n').slice(-8).join('\n');

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateTsConfig = async (updater: (content: Record<string, any>) => typeof content) => {
const updatedContent = updater(tsConfigContent);
await fs.promises.writeFile(tsConfigPath, JSON.stringify(updatedContent, null, 2), { encoding: 'utf-8' });
};

return {
cjsOutfile,
esmOutfile,
cjsEntryPoint,
esmEntryPoint,
distDirectory: filesDir,
updateTsConfig,
getCjsContent,
getEsmContent,
getCjsContentWithoutHelpers,
};
}

const cjsOutfile = path.resolve(filesDir, 'cjs-out.js');
const esmOutfile = path.resolve(filesDir, 'esm-out.js');
describe('buildAssets', () => {
it('compiles code to CJS & ESM', async () => {
const template = stripIndents`
export const Foo = 'foo';
`;

await fs.promises.writeFile(cjsEntryPoint, template);
await fs.promises.writeFile(esmEntryPoint, template);
const { getCjsContent, getEsmContent, getCjsContentWithoutHelpers, ...apiArgs } = await setup(template);

await buildAssets({ chromeVersion: 100, cjsEntryPoint, cjsOutfile, esmEntryPoint, esmOutfile });
await buildAssets({
chromeVersion: 100,
...apiArgs,
});

const cjsContent = stripComments(await fs.promises.readFile(cjsOutfile, { encoding: 'utf8' }));
const esmContent = stripComments(await fs.promises.readFile(esmOutfile, { encoding: 'utf8' }));
const cjsContent = await getCjsContent();
const esmContent = await getEsmContent();

const cjsContentWithoutHelpers = cjsContent.split('\n').slice(-8).join('\n');
const cjsContentWithoutHelpers = getCjsContentWithoutHelpers(cjsContent);

expect(cjsContentWithoutHelpers).toMatchInlineSnapshot(`
"
var cjs_exports = {};
__export(cjs_exports, {
Foo: () => Foo
Foo: () => Foo
});
module.exports = __toCommonJS(cjs_exports);
var Foo = \\"foo\\";
"
var Foo = \\"foo\\";"
`);
expect(esmContent).toMatchInlineSnapshot(`
"(() => {
var Foo = \\"foo\\";
})();
"
var Foo = \\"foo\\";
})();"
`);
}, /* Sets 15s timeout to allow for the build to complete */ 15000);

it('resolves package imports via TS path aliases', async () => {
const template = stripIndents`
import { hello } from '@proj/hello';
export const Foo = 'foo' + hello
`;

const {
getCjsContent,
getEsmContent,
getCjsContentWithoutHelpers,
updateTsConfig: _,
...apiArgs
} = await setup(template);

await buildAssets({
chromeVersion: 100,
...apiArgs,
});

const cjsContent = await getCjsContent();
const esmContent = await getEsmContent();

const cjsContentWithoutHelpers = getCjsContentWithoutHelpers(cjsContent);

expect(cjsContentWithoutHelpers).toMatchInlineSnapshot(`
"});
module.exports = __toCommonJS(cjs_exports);
var hello = \\"hello\\";
var Foo = \\"foo\\" + hello;"
`);
expect(esmContent).toMatchInlineSnapshot(`
"(() => {
var hello = \\"hello\\";
var Foo = \\"foo\\" + hello;
})();"
`);
});

it('fails if TS path aliases path mapping contains unsupported pattern', async () => {
const template = stripIndents`
import { hello } from '@proj/hello';
export const Foo = 'foo' + hello
`;

const { getCjsContent, getEsmContent, getCjsContentWithoutHelpers, updateTsConfig, ...apiArgs } = await setup(
template,
);

await updateTsConfig(content => {
const currentMapping = content.compilerOptions.paths['@proj/hello'];
content.compilerOptions.paths['@proj/hello'] = [...currentMapping, 'packages/hello/foo.ts'];
return content;
});

await expect(
buildAssets({
chromeVersion: 100,
...apiArgs,
}),
).rejects.toMatchInlineSnapshot(
`[Error: Multiple TS path mappings are not supported. Please adjust your config. "@proj/hello": [ packages/hello/index.ts,packages/hello/foo.ts ]"]`,
);
});
});
10 changes: 9 additions & 1 deletion scripts/test-ssr/src/utils/buildAssets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { build } from 'esbuild';
import type { BuildOptions } from 'esbuild';

import { tsConfigPathsPlugin } from './esbuild-plugin';

const NODE_MAJOR_VERSION = process.versions.node.split('.')[0];

const commonOptions: BuildOptions = {
Expand All @@ -21,10 +23,14 @@ type BuildConfig = {
esmOutfile: string;

chromeVersion: number;

distDirectory: string;
};

export async function buildAssets(config: BuildConfig): Promise<void> {
const { chromeVersion, cjsEntryPoint, cjsOutfile, esmEntryPoint, esmOutfile } = config;
const { chromeVersion, cjsEntryPoint, cjsOutfile, esmEntryPoint, esmOutfile, distDirectory } = config;

const pluginInstance = tsConfigPathsPlugin({ cwd: distDirectory });

try {
// Used for SSR rendering, see renderToHTML.js
Expand All @@ -38,6 +44,7 @@ export async function buildAssets(config: BuildConfig): Promise<void> {
external: ['@griffel/core', '@griffel/react', 'react', 'react-dom', 'scheduler'],
format: 'cjs',
target: `node${NODE_MAJOR_VERSION}`,
plugins: [pluginInstance],
});

// Used in generated bundle that will be server by a browser
Expand All @@ -54,6 +61,7 @@ export async function buildAssets(config: BuildConfig): Promise<void> {
],
format: 'iife',
target: `chrome${chromeVersion}`,
plugins: [pluginInstance],
});
} catch (err) {
throw new Error(
Expand Down
45 changes: 45 additions & 0 deletions scripts/test-ssr/src/utils/esbuild-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as path from 'node:path';

import type { Plugin } from 'esbuild';
import { loadConfig } from 'tsconfig-paths';

function assertPathAliasesSetup(paths: Record<string, string[]>): never | void {
for (const [key, mapping] of Object.entries(paths)) {
if (mapping.length > 1) {
throw new Error(
`Multiple TS path mappings are not supported. Please adjust your config. "${key}": [ ${mapping.join()} ]"`,
);
}
}
}

export function tsConfigPathsPlugin(options: { cwd: string }): Plugin {
const tsConfig = loadConfig(options.cwd);

if (tsConfig.resultType === 'failed') {
throw new Error(tsConfig.message);
}

const pathAliases = tsConfig.paths;

assertPathAliasesSetup(pathAliases);

const pluginConfig: Plugin = {
name: 'tsconfig-paths',
setup({ onResolve }) {
onResolve({ filter: /.*/ }, args => {
const pathMapping = pathAliases[args.path];

if (!pathMapping) {
return null;
}

const absoluteImportPath = path.join(tsConfig.absoluteBaseUrl, pathMapping[0]);

return { path: absoluteImportPath };
});
},
};

return pluginConfig;
}

0 comments on commit 25027bf

Please sign in to comment.