Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(test-ssr): add custom esbuild-plugin to properly resolve TS path aliases which stopped working starting esbuild 0.19 #31227

Merged
merged 3 commits into from
Apr 30, 2024
Merged
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
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;
}
Loading