From 5bdf02278af45804d1c7bc6edf13659bce257397 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Sun, 19 Nov 2023 01:15:52 +0900 Subject: [PATCH] test: add tests for transform (#397) --- package.json | 2 + pnpm-lock.yaml | 89 ++++++++++++++ src/utils/transform/get-esbuild-options.ts | 81 +++++++------ src/utils/transform/index.ts | 45 ++++--- tests/index.ts | 1 + tests/specs/smoke.ts | 22 ++-- tests/specs/transform.ts | 131 +++++++++++++++++++++ tests/specs/watch.ts | 1 + 8 files changed, 306 insertions(+), 66 deletions(-) create mode 100644 tests/specs/transform.ts diff --git a/package.json b/package.json index df68eb91a..e8f40a923 100644 --- a/package.json +++ b/package.json @@ -75,11 +75,13 @@ "eslint": "^8.49.0", "execa": "^8.0.1", "fs-fixture": "^1.2.0", + "fs-require": "^1.6.0", "get-node": "^14.2.1", "kolorist": "^1.8.0", "lint-staged": "^14.0.1", "magic-string": "^0.30.3", "manten": "^1.2.0", + "memfs": "^4.6.0", "node-pty": "^1.0.0", "outdent": "^0.8.0", "pkgroll": "^1.11.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c36b1b7a..4c6a1cc00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ devDependencies: fs-fixture: specifier: ^1.2.0 version: 1.2.0 + fs-require: + specifier: ^1.6.0 + version: 1.6.0 get-node: specifier: ^14.2.1 version: 14.2.1 @@ -89,6 +92,9 @@ devDependencies: manten: specifier: ^1.2.0 version: 1.2.0 + memfs: + specifier: ^4.6.0 + version: 4.6.0(quill-delta@5.1.0)(rxjs@7.8.1)(tslib@2.6.2) node-pty: specifier: ^1.0.0 version: 1.0.0 @@ -935,6 +941,10 @@ packages: picomatch: 2.3.1 dev: true + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true @@ -1967,6 +1977,10 @@ packages: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + /fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} dev: true @@ -2079,6 +2093,10 @@ packages: engines: {node: '>=16.7.0'} dev: true + /fs-require@1.6.0: + resolution: {integrity: sha512-zk5lFDV09Ef+CwOuPI/Q46lNdyzypPH/dB4Ywnyt40l2yqe5Z8ZJBGFaaYHwOwXAtKJEW4lSsn6V26erlEXVGA==} + dev: true + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -2340,6 +2358,11 @@ packages: engines: {node: '>=16.17.0'} dev: true + /hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + dev: true + /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -2750,6 +2773,22 @@ packages: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true + /json-joy@9.9.1(quill-delta@5.1.0)(rxjs@7.8.1)(tslib@2.6.2): + resolution: {integrity: sha512-/d7th2nbQRBQ/nqTkBe6KjjvDciSwn9UICmndwk3Ed/Bk9AqkTRm4PnLVfXG4DKbT0rEY0nKnwE7NqZlqKE6kg==} + engines: {node: '>=10.0'} + hasBin: true + peerDependencies: + quill-delta: ^5 + rxjs: '7' + tslib: '2' + dependencies: + arg: 5.0.2 + hyperdyperid: 1.2.0 + quill-delta: 5.1.0 + rxjs: 7.8.1 + tslib: 2.6.2 + dev: true + /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: true @@ -2882,6 +2921,14 @@ packages: p-locate: 5.0.0 dev: true + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + dev: true + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: true + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -2976,6 +3023,20 @@ packages: mimic-fn: 4.0.0 dev: true + /memfs@4.6.0(quill-delta@5.1.0)(rxjs@7.8.1)(tslib@2.6.2): + resolution: {integrity: sha512-I6mhA1//KEZfKRQT9LujyW6lRbX7RkC24xKododIDO3AGShcaFAMKElv1yFGWX8fD4UaSiwasr3NeQ5TdtHY1A==} + engines: {node: '>= 4.0.0'} + peerDependencies: + tslib: '2' + dependencies: + json-joy: 9.9.1(quill-delta@5.1.0)(rxjs@7.8.1)(tslib@2.6.2) + thingies: 1.12.0(tslib@2.6.2) + tslib: 2.6.2 + transitivePeerDependencies: + - quill-delta + - rxjs + dev: true + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -3479,6 +3540,15 @@ packages: engines: {node: '>=10'} dev: true + /quill-delta@5.1.0: + resolution: {integrity: sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==} + engines: {node: '>= 12.0.0'} + dependencies: + fast-diff: 1.3.0 + lodash.clonedeep: 4.5.0 + lodash.isequal: 4.5.0 + dev: true + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true @@ -3657,6 +3727,12 @@ packages: queue-microtask: 1.2.3 dev: true + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.6.2 + dev: true + /safe-array-concat@1.0.1: resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} engines: {node: '>=0.4'} @@ -3969,6 +4045,15 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /thingies@1.12.0(tslib@2.6.2): + resolution: {integrity: sha512-AiGqfYC1jLmJagbzQGuoZRM48JPsr9yB734a7K6wzr34NMhjUPrWSQrkF7ZBybf3yCerCL2Gcr02kMv4NmaZfA==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + dependencies: + tslib: 2.6.2 + dev: true + /tmp-promise@3.0.3: resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} dependencies: @@ -4007,6 +4092,10 @@ packages: strip-bom: 3.0.0 dev: true + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} diff --git a/src/utils/transform/get-esbuild-options.ts b/src/utils/transform/get-esbuild-options.ts index 5d532b3f4..8f5ad7106 100644 --- a/src/utils/transform/get-esbuild-options.ts +++ b/src/utils/transform/get-esbuild-options.ts @@ -1,44 +1,47 @@ import path from 'path'; -import type { TransformOptions } from 'esbuild'; - -const nodeVersion = process.versions.node; +import type { TransformOptions, TransformResult } from 'esbuild'; + +export const baseConfig = Object.freeze({ + target: `node${process.versions.node}`, + + // "default" tells esbuild to infer loader from file name + // https://github.com/evanw/esbuild/blob/4a07b17adad23e40cbca7d2f8931e8fb81b47c33/internal/bundler/bundler.go#L158 + loader: 'default', +}); + +export const cacheConfig = { + ...baseConfig, + + sourcemap: true, + + /** + * Smaller output for cache and marginal performance improvement: + * https://twitter.com/evanwallace/status/1396336348366180359?s=20 + * + * minifyIdentifiers is disabled because debuggers don't use the + * `names` property from the source map + * + * minifySyntax is disabled because it does some tree-shaking + * eg. unused try-catch error variable + */ + minifyWhitespace: true, + + // TODO: Is this necessary if minifyIdentifiers is false? + keepNames: true, +}; -export const getEsbuildOptions = ( - extendOptions: TransformOptions, +export const patchOptions = ( + options: TransformOptions, ) => { - const options: TransformOptions = { - target: `node${nodeVersion}`, - - // "default" tells esbuild to infer loader from file name - // https://github.com/evanw/esbuild/blob/4a07b17adad23e40cbca7d2f8931e8fb81b47c33/internal/bundler/bundler.go#L158 - loader: 'default', - - sourcemap: true, - - /** - * Smaller output for cache and marginal performance improvement: - * https://twitter.com/evanwallace/status/1396336348366180359?s=20 - * - * minifyIdentifiers is disabled because debuggers don't use the - * `names` property from the source map - * - * minifySyntax is disabled because it does some tree-shaking - * eg. unused try-catch error variable - */ - minifyWhitespace: true, - keepNames: true, - - ...extendOptions, - }; + const originalSourcefile = options.sourcefile; - if (options.sourcefile) { - const { sourcefile } = options; - const extension = path.extname(sourcefile); + if (originalSourcefile) { + const extension = path.extname(originalSourcefile); if (extension) { // https://github.com/evanw/esbuild/issues/1932 if (extension === '.cts' || extension === '.mts') { - options.sourcefile = `${sourcefile.slice(0, -3)}ts`; + options.sourcefile = `${originalSourcefile.slice(0, -3)}ts`; } } else { // esbuild errors to detect loader when a file doesn't have an extension @@ -46,5 +49,15 @@ export const getEsbuildOptions = ( } } - return options; + return ( + result: TransformResult, + ) => { + if (options.sourcefile !== originalSourcefile) { + result.map = result.map.replace( + JSON.stringify(options.sourcefile), + JSON.stringify(originalSourcefile), + ); + } + return result; + }; }; diff --git a/src/utils/transform/index.ts b/src/utils/transform/index.ts index 11d93dae5..1c279a3e6 100644 --- a/src/utils/transform/index.ts +++ b/src/utils/transform/index.ts @@ -13,7 +13,10 @@ import { applyTransformers, type Transformed, } from './apply-transformers'; -import { getEsbuildOptions } from './get-esbuild-options'; +import { + cacheConfig, + patchOptions, +} from './get-esbuild-options'; // Used by cjs-loader export function transformSync( @@ -27,14 +30,15 @@ export function transformSync( define['import.meta.url'] = `'${pathToFileURL(filePath)}'`; } - const esbuildOptions = getEsbuildOptions({ + const esbuildOptions = { + ...cacheConfig, format: 'cjs', sourcefile: filePath, define, banner: '(()=>{', footer: '})()', ...extendOptions, - }); + } as const; const hash = sha1([ code, @@ -50,16 +54,11 @@ export function transformSync( code, [ // eslint-disable-next-line @typescript-eslint/no-shadow - (filePath, code) => { - // eslint-disable-next-line @typescript-eslint/no-shadow - const transformed = esbuildTransformSync(code, esbuildOptions); - if (esbuildOptions.sourcefile !== filePath) { - transformed.map = transformed.map.replace( - JSON.stringify(esbuildOptions.sourcefile), - JSON.stringify(filePath), - ); - } - return transformed; + (_filePath, code) => { + const patchResults = patchOptions(esbuildOptions); + return patchResults( + esbuildTransformSync(code, esbuildOptions), + ); }, transformDynamicImport, ] as const, @@ -84,11 +83,12 @@ export async function transform( filePath: string, extendOptions?: TransformOptions, ): Promise { - const esbuildOptions = getEsbuildOptions({ + const esbuildOptions = { + ...cacheConfig, format: 'esm', sourcefile: filePath, ...extendOptions, - }); + } as const; const hash = sha1([ code, @@ -104,16 +104,11 @@ export async function transform( code, [ // eslint-disable-next-line @typescript-eslint/no-shadow - async (filePath, code) => { - // eslint-disable-next-line @typescript-eslint/no-shadow - const transformed = await esbuildTransform(code, esbuildOptions); - if (esbuildOptions.sourcefile !== filePath) { - transformed.map = transformed.map.replace( - JSON.stringify(esbuildOptions.sourcefile), - JSON.stringify(filePath), - ); - } - return transformed; + async (_filePath, code) => { + const patchResults = patchOptions(esbuildOptions); + return patchResults( + await esbuildTransform(code, esbuildOptions), + ); }, transformDynamicImport, ] as const, diff --git a/tests/index.ts b/tests/index.ts index cdfb4f4e0..7d1807b18 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -8,6 +8,7 @@ import { nodeVersions } from './utils/node-versions'; await runTestSuite(import('./specs/watch')); await runTestSuite(import('./specs/repl')); await runTestSuite(import('./specs/source-map')); + await runTestSuite(import('./specs/transform')); for (const nodeVersion of nodeVersions) { const node = await createNode(nodeVersion); diff --git a/tests/specs/smoke.ts b/tests/specs/smoke.ts index 0b6c5580c..3aa1bf568 100644 --- a/tests/specs/smoke.ts +++ b/tests/specs/smoke.ts @@ -79,7 +79,9 @@ export default 1; `; const sourcemap = { - test: 'const { stack } = new Error(); assert(stack.includes(\':SOURCEMAP_LINE\'), \'Expected SOURCEMAP_LINE in stack:\' + stack)', + test: ( + extension: string, + ) => `const { stack } = new Error(); const searchString = 'index.${extension}:SOURCEMAP_LINE'; assert(stack.includes(searchString), \`Expected \${searchString} in stack: \${stack}\`)`, tag: ( strings: TemplateStringsArray, ...values: string[] @@ -104,7 +106,7 @@ const files = { const assert = require('node:assert'); assert(${cjsContextCheck}, 'Should have CJS context'); ${preserveName} - ${sourcemap.test} + ${sourcemap.test('cjs')} exports.named = 'named'; `, @@ -143,7 +145,7 @@ const files = { const bar = (value: T) => fn(); ${preserveName} - ${sourcemap.test} + ${sourcemap.test('ts')} export const cjsContext = ${cjsContextCheck}; ${tsCheck}; `, @@ -156,7 +158,7 @@ const files = { ${declareReact} export const jsx = ${jsxCheck}; ${preserveName} - ${sourcemap.test} + ${sourcemap.test('jsx')} `, 'tsx/index.tsx': sourcemap.tag` @@ -166,7 +168,7 @@ const files = { ${declareReact} export const jsx = ${jsxCheck}; ${preserveName} - ${sourcemap.test} + ${sourcemap.test('tsx')} `, 'mts/index.mts': sourcemap.tag` @@ -174,7 +176,7 @@ const files = { export const mjsHasCjsContext = ${cjsContextCheck}; ${tsCheck}; ${preserveName} - ${sourcemap.test} + ${sourcemap.test('mts')} `, 'cts/index.cts': sourcemap.tag` @@ -182,7 +184,7 @@ const files = { assert(${cjsContextCheck}, 'Should have CJS context'); ${tsCheck}; ${preserveName} - ${sourcemap.test} + ${sourcemap.test('cts')} `, 'expect-errors.js': ` @@ -336,6 +338,10 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => { //# sourceMappingURL=shouldnt affect the file + // const shouldntAffectFile = \` + // //# sourceMappingURL=\`; + //# sourceMappingURL=shouldnt affect the file + // node: prefix import 'node:fs'; @@ -460,6 +466,8 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => { import assert from 'assert'; import { expectErrors } from './expect-errors'; + // const shouldntAffectFile = \` + // //# sourceMappingURL=\`; //# sourceMappingURL=shouldnt affect the file // node: prefix diff --git a/tests/specs/transform.ts b/tests/specs/transform.ts new file mode 100644 index 000000000..1f4b10db4 --- /dev/null +++ b/tests/specs/transform.ts @@ -0,0 +1,131 @@ +import { testSuite, expect } from 'manten'; +import { createFsRequire } from 'fs-require'; +import { Volume } from 'memfs'; +import outdent from 'outdent'; +import { transform, transformSync } from '../../src/utils/transform'; + +const base64Module = (code: string) => `data:text/javascript;base64,${Buffer.from(code).toString('base64')}`; + +const fixtures = { + ts: outdent` + const __filename = 'filename'; + const __dirname = 'dirname'; + try { + const unusedVariable1 = 1; + } catch (unusedError) { + const unusedVariable2 = 2; + } + export default 'default value' as string; + export const named: string = 'named'; + export const functionName: string = (function named() {}).name; + `, + + esm: outdent` + const __filename = 'filename'; + const __dirname = 'dirname'; + try { + const unusedVariable1 = 1; + } catch (unusedError) { + const unusedVariable2 = 2; + } + + export default 'default value'; + export const named = 'named'; + export const functionName = (function named() {}).name; + `, +}; + +export default testSuite(({ describe }) => { + describe('transform', ({ describe }) => { + describe('sync', ({ test }) => { + test('transforms ESM to CJS', () => { + const transformed = transformSync( + fixtures.esm, + 'file.js', + { format: 'cjs' }, + ); + + // For debuggers + expect(transformed.code).toMatch('unusedVariable1'); + expect(transformed.code).toMatch('unusedVariable2'); + + const fsRequire = createFsRequire(Volume.fromJSON({ + '/file.js': transformed.code, + })); + + expect(fsRequire('/file.js')).toStrictEqual({ + default: 'default value', + functionName: 'named', + named: 'named', + }); + }); + + test('dynamic import', () => { + const dynamicImport = transformSync( + 'import((0, _url.pathToFileURL)(path).href)', + 'file.js', + { format: 'cjs' }, + ); + + expect(dynamicImport.code).toMatch('.href).then'); + }); + + test('sourcemap file', () => { + const fileName = 'file.mts'; + const transformed = transformSync( + fixtures.ts, + fileName, + { format: 'esm' }, + ); + + expect(transformed.map).not.toBe(''); + + const { map } = transformed; + if (typeof map !== 'string') { + expect(map.sources.length).toBe(1); + expect(map.sources[0]).toBe(fileName); + expect(map.names).toStrictEqual(['named']); + } + }); + }); + + describe('async', ({ test }) => { + test('transforms TS to ESM', async () => { + const transformed = await transform( + fixtures.ts, + 'file.ts', + { format: 'esm' }, + ); + + // For debuggers + expect(transformed.code).toMatch('unusedVariable1'); + expect(transformed.code).toMatch('unusedVariable2'); + + const imported = await import(base64Module(transformed.code)); + expect({ ...imported }).toStrictEqual({ + default: 'default value', + functionName: 'named', + named: 'named', + }); + }); + + test('sourcemap file', async () => { + const fileName = 'file.cts'; + const transformed = await transform( + fixtures.ts, + fileName, + { format: 'esm' }, + ); + + expect(transformed.map).not.toBe(''); + + const { map } = transformed; + if (typeof map !== 'string') { + expect(map.sources.length).toBe(1); + expect(map.sources[0]).toBe(fileName); + expect(map.names).toStrictEqual(['named']); + } + }); + }); + }); +}); diff --git a/tests/specs/watch.ts b/tests/specs/watch.ts index ecb60c60b..8d3f46558 100644 --- a/tests/specs/watch.ts +++ b/tests/specs/watch.ts @@ -57,6 +57,7 @@ export default testSuite(async ({ describe }) => { [ async (data) => { if (data.includes('hello world\n')) { + await setTimeout(1000); fixtureWatch.writeFile('value.js', 'export const value = \'goodbye world\';'); return true; }