From 7d27e914750b37128953a9b75dc39179f90d3681 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 29 Apr 2024 18:15:56 +0200 Subject: [PATCH 1/3] feat(test-ssr): add custom esbuild-plugin to properly resolve TS path aliases which stopped working starting esbuild 0.19 --- scripts/test-ssr/src/commands/main.ts | 2 + scripts/test-ssr/src/esbuild-plugin.js | 47 +++++++++++++++++++ .../test-ssr/src/utils/buildAssets.test.ts | 47 +++++++++++++++---- scripts/test-ssr/src/utils/buildAssets.ts | 10 +++- 4 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 scripts/test-ssr/src/esbuild-plugin.js diff --git a/scripts/test-ssr/src/commands/main.ts b/scripts/test-ssr/src/commands/main.ts index e403c23283137..4938257104001 100644 --- a/scripts/test-ssr/src/commands/main.ts +++ b/scripts/test-ssr/src/commands/main.ts @@ -141,6 +141,8 @@ export async function main(params: MainParams) { await buildAssets({ chromeVersion: CHROME_VERSION, + distDirectory, + esmEntryPoint, cjsEntryPoint, diff --git a/scripts/test-ssr/src/esbuild-plugin.js b/scripts/test-ssr/src/esbuild-plugin.js new file mode 100644 index 0000000000000..5fdd2c7280d07 --- /dev/null +++ b/scripts/test-ssr/src/esbuild-plugin.js @@ -0,0 +1,47 @@ +// @ts-check +const path = require('node:path'); + +const { loadConfig } = require('tsconfig-paths'); + +exports.tsConfigPathsPlugin = tsConfigPathsPlugin; + +/** + * + * @param {{cwd:string}} options + * @returns {import('esbuild').Plugin} + */ +function tsConfigPathsPlugin(options) { + const tsConfig = loadConfig(options.cwd); + + if (tsConfig.resultType === 'failed') { + throw new Error(tsConfig.message); + } + + const pathAliases = tsConfig.paths; + + /** @type {import('esbuild').Plugin} */ + const pluginConfig = { + name: 'tsconfig-paths', + setup({ onResolve }) { + onResolve({ filter: /.*/ }, args => { + const pathMapping = pathAliases[args.path]; + + if (!pathMapping) { + return null; + } + + for (const dir of pathMapping) { + const absoluteImportPath = path.join(tsConfig.absoluteBaseUrl, dir); + + if (absoluteImportPath) { + return { path: absoluteImportPath }; + } + } + + return { path: args.path }; + }); + }, + }; + + return pluginConfig; +} diff --git a/scripts/test-ssr/src/utils/buildAssets.test.ts b/scripts/test-ssr/src/utils/buildAssets.test.ts index 531bfef206522..6bbaea492a73e 100644 --- a/scripts/test-ssr/src/utils/buildAssets.test.ts +++ b/scripts/test-ssr/src/utils/buildAssets.test.ts @@ -11,7 +11,11 @@ function stripComments(content: string): string { describe('buildAssets', () => { it('compiles code to CJS & ESM', async () => { - const template = `export const Foo = 'foo'`; + const template = ` + import { hello } from '@proj/hello'; + + export const Foo = 'foo' + hello + `; const filesDir = tmp.dirSync({ unsafeCleanup: true }).name; @@ -21,10 +25,30 @@ describe('buildAssets', () => { const cjsOutfile = path.resolve(filesDir, 'cjs-out.js'); const esmOutfile = path.resolve(filesDir, 'esm-out.js'); + await fs.promises.writeFile( + path.resolve(filesDir, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + baseUrl: '.', + paths: { + '@proj/hello': ['packages/hello/index.ts'], + }, + }, + }), + ); + 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';`); await fs.promises.writeFile(cjsEntryPoint, template); await fs.promises.writeFile(esmEntryPoint, template); - await buildAssets({ chromeVersion: 100, cjsEntryPoint, cjsOutfile, esmEntryPoint, esmOutfile }); + await buildAssets({ + chromeVersion: 100, + cjsEntryPoint, + cjsOutfile, + esmEntryPoint, + esmOutfile, + distDirectory: filesDir, + }); const cjsContent = stripComments(await fs.promises.readFile(cjsOutfile, { encoding: 'utf8' })); const esmContent = stripComments(await fs.promises.readFile(esmOutfile, { encoding: 'utf8' })); @@ -32,19 +56,22 @@ describe('buildAssets', () => { const cjsContentWithoutHelpers = cjsContent.split('\n').slice(-8).join('\n'); expect(cjsContentWithoutHelpers).toMatchInlineSnapshot(` - " - var cjs_exports = {}; - __export(cjs_exports, { - Foo: () => Foo - }); - module.exports = __toCommonJS(cjs_exports); - var Foo = \\"foo\\"; + "module.exports = __toCommonJS(cjs_exports); + + + var hello = \\"hello\\"; + + + var Foo = \\"foo\\" + hello; " `); expect(esmContent).toMatchInlineSnapshot(` "(() => { - var Foo = \\"foo\\"; + var hello = \\"hello\\"; + + + var Foo = \\"foo\\" + hello; })(); " `); diff --git a/scripts/test-ssr/src/utils/buildAssets.ts b/scripts/test-ssr/src/utils/buildAssets.ts index 9686f70536509..6e16efbe4dff7 100644 --- a/scripts/test-ssr/src/utils/buildAssets.ts +++ b/scripts/test-ssr/src/utils/buildAssets.ts @@ -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 = { @@ -21,10 +23,14 @@ type BuildConfig = { esmOutfile: string; chromeVersion: number; + + distDirectory: string; }; export async function buildAssets(config: BuildConfig): Promise { - 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 @@ -38,6 +44,7 @@ export async function buildAssets(config: BuildConfig): Promise { 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 @@ -54,6 +61,7 @@ export async function buildAssets(config: BuildConfig): Promise { ], format: 'iife', target: `chrome${chromeVersion}`, + plugins: [pluginInstance], }); } catch (err) { throw new Error( From fcf31e551ddaa4ae5d762fe597917b65b64ecdfb Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 30 Apr 2024 11:21:54 +0200 Subject: [PATCH 2/3] fixup! feat(test-ssr): add custom esbuild-plugin to properly resolve TS path aliases which stopped working starting esbuild 0.19 --- scripts/test-ssr/src/utils/buildAssets.ts | 2 +- .../esbuild-plugin.ts} | 18 +++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) rename scripts/test-ssr/src/{esbuild-plugin.js => utils/esbuild-plugin.ts} (67%) diff --git a/scripts/test-ssr/src/utils/buildAssets.ts b/scripts/test-ssr/src/utils/buildAssets.ts index 6e16efbe4dff7..058566a137dd9 100644 --- a/scripts/test-ssr/src/utils/buildAssets.ts +++ b/scripts/test-ssr/src/utils/buildAssets.ts @@ -1,7 +1,7 @@ import { build } from 'esbuild'; import type { BuildOptions } from 'esbuild'; -import { tsConfigPathsPlugin } from '../esbuild-plugin'; +import { tsConfigPathsPlugin } from './esbuild-plugin'; const NODE_MAJOR_VERSION = process.versions.node.split('.')[0]; diff --git a/scripts/test-ssr/src/esbuild-plugin.js b/scripts/test-ssr/src/utils/esbuild-plugin.ts similarity index 67% rename from scripts/test-ssr/src/esbuild-plugin.js rename to scripts/test-ssr/src/utils/esbuild-plugin.ts index 5fdd2c7280d07..30ac023059706 100644 --- a/scripts/test-ssr/src/esbuild-plugin.js +++ b/scripts/test-ssr/src/utils/esbuild-plugin.ts @@ -1,16 +1,9 @@ -// @ts-check -const path = require('node:path'); +import * as path from 'node:path'; -const { loadConfig } = require('tsconfig-paths'); +import type { Plugin } from 'esbuild'; +import { loadConfig } from 'tsconfig-paths'; -exports.tsConfigPathsPlugin = tsConfigPathsPlugin; - -/** - * - * @param {{cwd:string}} options - * @returns {import('esbuild').Plugin} - */ -function tsConfigPathsPlugin(options) { +export function tsConfigPathsPlugin(options: { cwd: string }): Plugin { const tsConfig = loadConfig(options.cwd); if (tsConfig.resultType === 'failed') { @@ -19,8 +12,7 @@ function tsConfigPathsPlugin(options) { const pathAliases = tsConfig.paths; - /** @type {import('esbuild').Plugin} */ - const pluginConfig = { + const pluginConfig: Plugin = { name: 'tsconfig-paths', setup({ onResolve }) { onResolve({ filter: /.*/ }, args => { From 1ec8ffdec8b7e58d601ea3094afea54854839567 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 30 Apr 2024 12:44:29 +0200 Subject: [PATCH 3/3] fixup! fixup! feat(test-ssr): add custom esbuild-plugin to properly resolve TS path aliases which stopped working starting esbuild 0.19 --- .../test-ssr/src/utils/buildAssets.test.ts | 179 ++++++++++++++---- scripts/test-ssr/src/utils/esbuild-plugin.ts | 22 ++- 2 files changed, 151 insertions(+), 50 deletions(-) diff --git a/scripts/test-ssr/src/utils/buildAssets.test.ts b/scripts/test-ssr/src/utils/buildAssets.test.ts index 6bbaea492a73e..1362cb4a75629 100644 --- a/scripts/test-ssr/src/utils/buildAssets.test.ts +++ b/scripts/test-ssr/src/utils/buildAssets.test.ts @@ -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'; @@ -9,71 +10,165 @@ function stripComments(content: string): string { return content.replace(/\/\/ .+/g, ''); } +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 tsConfigPath = path.resolve(filesDir, 'tsconfig.json'); + const tsConfigContent = { + compilerOptions: { + baseUrl: '.', + paths: { + '@proj/hello': ['packages/hello/index.ts'], + }, + }, + }; + + 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) => 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, + }; +} + describe('buildAssets', () => { it('compiles code to CJS & ESM', async () => { - const template = ` - import { hello } from '@proj/hello'; + const template = stripIndents` + export const Foo = 'foo'; + `; - export const Foo = 'foo' + hello - `; + const { getCjsContent, getEsmContent, getCjsContentWithoutHelpers, ...apiArgs } = await setup(template); - const filesDir = tmp.dirSync({ unsafeCleanup: true }).name; + await buildAssets({ + chromeVersion: 100, + ...apiArgs, + }); - const cjsEntryPoint = path.resolve(filesDir, 'cjs.js'); - const esmEntryPoint = path.resolve(filesDir, 'esm.js'); + const cjsContent = await getCjsContent(); + const esmContent = await getEsmContent(); - const cjsOutfile = path.resolve(filesDir, 'cjs-out.js'); - const esmOutfile = path.resolve(filesDir, 'esm-out.js'); + const cjsContentWithoutHelpers = getCjsContentWithoutHelpers(cjsContent); - await fs.promises.writeFile( - path.resolve(filesDir, 'tsconfig.json'), - JSON.stringify({ - compilerOptions: { - baseUrl: '.', - paths: { - '@proj/hello': ['packages/hello/index.ts'], - }, - }, - }), - ); - 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';`); - await fs.promises.writeFile(cjsEntryPoint, template); - await fs.promises.writeFile(esmEntryPoint, template); + expect(cjsContentWithoutHelpers).toMatchInlineSnapshot(` + " + + var cjs_exports = {}; + __export(cjs_exports, { + Foo: () => Foo + }); + module.exports = __toCommonJS(cjs_exports); + var Foo = \\"foo\\";" + `); + expect(esmContent).toMatchInlineSnapshot(` + "(() => { + + 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, - cjsEntryPoint, - cjsOutfile, - esmEntryPoint, - esmOutfile, - distDirectory: filesDir, + ...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(` - "module.exports = __toCommonJS(cjs_exports); + "}); + module.exports = __toCommonJS(cjs_exports); var hello = \\"hello\\"; - var Foo = \\"foo\\" + hello; - " + var Foo = \\"foo\\" + hello;" `); expect(esmContent).toMatchInlineSnapshot(` "(() => { - - var hello = \\"hello\\"; - - var Foo = \\"foo\\" + hello; - })(); - " + var hello = \\"hello\\"; + + + var Foo = \\"foo\\" + hello; + })();" `); - }, /* Sets 15s timeout to allow for the build to complete */ 15000); + }); + + 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 ]"]`, + ); + }); }); diff --git a/scripts/test-ssr/src/utils/esbuild-plugin.ts b/scripts/test-ssr/src/utils/esbuild-plugin.ts index 30ac023059706..b9d2fef404d8e 100644 --- a/scripts/test-ssr/src/utils/esbuild-plugin.ts +++ b/scripts/test-ssr/src/utils/esbuild-plugin.ts @@ -3,6 +3,16 @@ import * as path from 'node:path'; import type { Plugin } from 'esbuild'; import { loadConfig } from 'tsconfig-paths'; +function assertPathAliasesSetup(paths: Record): 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); @@ -12,6 +22,8 @@ export function tsConfigPathsPlugin(options: { cwd: string }): Plugin { const pathAliases = tsConfig.paths; + assertPathAliasesSetup(pathAliases); + const pluginConfig: Plugin = { name: 'tsconfig-paths', setup({ onResolve }) { @@ -22,15 +34,9 @@ export function tsConfigPathsPlugin(options: { cwd: string }): Plugin { return null; } - for (const dir of pathMapping) { - const absoluteImportPath = path.join(tsConfig.absoluteBaseUrl, dir); - - if (absoluteImportPath) { - return { path: absoluteImportPath }; - } - } + const absoluteImportPath = path.join(tsConfig.absoluteBaseUrl, pathMapping[0]); - return { path: args.path }; + return { path: absoluteImportPath }; }); }, };