Skip to content

Commit

Permalink
Fix swc issues (#2062)
Browse files Browse the repository at this point in the history
* Bump minimum version of swc to 1.3.84

* Fix type error caused by swc's mismatched types between @swc/wasm and @swc/core

* Update swc options to match latest swc version, fix bug where swc was emitting import assertions as `with` which node does not understand

* Pass correct useDefineForClassFields, derived from tsconfig

* Fix bug where ts-node errored when swc or custom transpiler returned undefined for sourcemap

* pass jsxImportSource to swc

* Bump min swc version to 1.3.85

Fix issue introduced by swc 1.3.85 with module options that are now forbidden instead of ignored for certain module types

remove failing useDefineForClassFields case because I forgot that implicit compiler options were in effect, so default target is actually much higher than I expected
  • Loading branch information
cspotcode authored Sep 15, 2023
1 parent 9a2a275 commit 0022f38
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 75 deletions.
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@
"@cspotcode/ava-lib": "https://github.com/cspotcode/ava-lib#805aab17b2b89c388596b6dc2b4eece403c5fb87",
"@cspotcode/expect-stream": "https://github.com/cspotcode/node-expect-stream#4e425ff1eef240003af8716291e80fbaf3e3ae8f",
"@microsoft/api-extractor": "^7.19.4",
"@swc/core": "1.3.32",
"@swc/wasm": "1.3.32",
"@swc/core": "1.3.85",
"@swc/wasm": "1.3.85",
"@types/diff": "^4.0.2",
"@types/lodash": "^4.14.151",
"@types/node": "13.13.5",
Expand Down Expand Up @@ -145,8 +145,8 @@
"workaround-broken-npm-prepack-behavior": "https://github.com/cspotcode/workaround-broken-npm-prepack-behavior#1a7adbbb8a527784daf97edad6ba42d6e96611f6"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@swc/core": ">=1.3.85",
"@swc/wasm": ">=1.3.85",
"@types/node": "*",
"typescript": ">=4.4"
},
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1158,7 +1158,7 @@ export function createFromPreloadedConfig(foundConfigResult: ReturnType<typeof f
const diagnosticList = filterDiagnostics(result.diagnostics || [], diagnosticFilters);
if (diagnosticList.length) reportTSError(diagnosticList);

return [result.outputText, result.sourceMapText as string, false];
return [result.outputText, result.sourceMapText ?? '{}', false];
};
}

Expand Down
189 changes: 181 additions & 8 deletions src/test/transpilers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@
// Should consolidate them here.

import { context } from './testlib';
import { ctxTsNode, testsDirRequire, tsSupportsImportAssertions, tsSupportsReact17JsxFactories } from './helpers';
import {
CMD_TS_NODE_WITHOUT_PROJECT_FLAG,
createExec,
ctxTsNode,
testsDirRequire,
TEST_DIR,
tsSupportsImportAssertions,
tsSupportsReact17JsxFactories,
} from './helpers';
import { createSwcOptions } from '../transpilers/swc';
import * as expect from 'expect';
import { outdent } from 'outdent';
import { join } from 'path';

const test = context(ctxTsNode);

Expand Down Expand Up @@ -78,10 +87,7 @@ test.suite('swc', (test) => {
.create({
swc: true,
skipProject: true,
compilerOptions: {
module: 'esnext',
...compilerOptions,
},
compilerOptions,
})
.compile(input, 'input.tsx');
expect(code.replace(/\/\/# sourceMappingURL.*/, '').trim()).toBe(expectedOutput);
Expand All @@ -93,12 +99,17 @@ test.suite('swc', (test) => {
const div = <div></div>;
`;

test(compileMacro, { jsx: 'react' }, input, `const div = /*#__PURE__*/ React.createElement("div", null);`);
test(
compileMacro,
{ module: 'esnext', jsx: 'react' },
input,
`const div = /*#__PURE__*/ React.createElement("div", null);`
);
test.suite('react 17 jsx factories', (test) => {
test.if(tsSupportsReact17JsxFactories);
test(
compileMacro,
{ jsx: 'react-jsx' },
{ module: 'esnext', jsx: 'react-jsx' },
input,
outdent`
import { jsx as _jsx } from "react/jsx-runtime";
Expand All @@ -107,7 +118,7 @@ test.suite('swc', (test) => {
);
test(
compileMacro,
{ jsx: 'react-jsxdev' },
{ module: 'esnext', jsx: 'react-jsxdev' },
input,
outdent`
import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
Expand Down Expand Up @@ -139,4 +150,166 @@ test.suite('swc', (test) => {
`
);
});

test.suite('useDefineForClassFields', (test) => {
const input = outdent`
class Foo {
bar = 1;
}
`;
const outputNative = outdent`
let Foo = class Foo {
bar = 1;
};
`;
const outputCtorAssignment = outdent`
let Foo = class Foo {
constructor(){
this.bar = 1;
}
};
`;
const outputDefine = outdent`
function _define_property(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
let Foo = class Foo {
constructor(){
_define_property(this, "bar", 1);
}
};
`;
test(
'useDefineForClassFields unset, should default to true and emit native property assignment b/c `next` target',
compileMacro,
{ module: 'esnext', target: 'ESNext' },
input,
outputNative
);
test(
'useDefineForClassFields unset, should default to true and emit native property assignment b/c new target',
compileMacro,
{ module: 'esnext', target: 'ES2022' },
input,
outputNative
);
test(
'useDefineForClassFields unset, should default to false b/c old target',
compileMacro,
{ module: 'esnext', target: 'ES2021' },
input,
outputCtorAssignment
);
test(
'useDefineForClassFields=true, should emit native property assignment b/c new target',
compileMacro,
{
module: 'esnext',
useDefineForClassFields: true,
target: 'ES2022',
},
input,
outputNative
);
test(
'useDefineForClassFields=true, should emit define b/c old target',
compileMacro,
{
module: 'esnext',
useDefineForClassFields: true,
target: 'ES2021',
},
input,
outputDefine
);
test(
'useDefineForClassFields=false, new target, should still emit legacy property assignment in ctor',
compileMacro,
{
module: 'esnext',
useDefineForClassFields: false,
target: 'ES2022',
},
input,
outputCtorAssignment
);
test(
'useDefineForClassFields=false, old target, should emit legacy property assignment in ctor',
compileMacro,
{
module: 'esnext',
useDefineForClassFields: false,
},
input,
outputCtorAssignment
);
});

test.suite('jsx and jsxImportSource', (test) => {
test(
'jsx=react-jsx',
compileMacro,
{
module: 'esnext',
jsx: 'react-jsx',
},
outdent`
<div></div>
`,
outdent`
/*#__PURE__*/ import { jsx as _jsx } from "react/jsx-runtime";
_jsx("div", {});
`
);
test(
'jsx=react-jsx w/custom jsxImportSource',
compileMacro,
{
module: 'esnext',
jsx: 'react-jsx',
jsxImportSource: 'foo',
},
outdent`
<div></div>
`,
outdent`
/*#__PURE__*/ import { jsx as _jsx } from "foo/jsx-runtime";
_jsx("div", {});
`
);
});

test.suite(
'#1996 regression: ts-node gracefully allows swc to not return a sourcemap for type-only files',
(test) => {
// https://github.com/TypeStrong/ts-node/issues/1996
// @swc/core 1.3.51 returned `undefined` instead of sourcemap if the file was empty or only exported types.
// Newer swc versions do not do this. But our typedefs technically allow it.
const exec = createExec({
cwd: join(TEST_DIR, '1996'),
});
test('import empty file w/swc', async (t) => {
const r = await exec(`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ./index.ts`);
expect(r.err).toBe(null);
expect(r.stdout).toMatch(/#1996 regression test./);
});
test('use custom transpiler which never returns a sourcemap', async (t) => {
const r = await exec(
`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --project tsconfig.custom-transpiler.json ./empty.ts`
);
expect(r.err).toBe(null);
expect(r.stdout).toMatch(/#1996 regression test with custom transpiler./);
});
}
);
});
31 changes: 21 additions & 10 deletions src/transpilers/swc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import type * as swcWasm from '@swc/wasm';
import type * as swcTypes from '@swc/core';
import type { CreateTranspilerOptions, Transpiler } from './types';
import type { NodeModuleEmitKind } from '..';
import { getUseDefineForClassFields } from '../ts-internals';

type SwcInstance = typeof swcWasm;
type SwcInstance = typeof swcTypes;
export interface SwcTranspilerOptions extends CreateTranspilerOptions {
/**
* swc compiler to use for compilation
Expand All @@ -28,7 +29,7 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler {
let swcDepName: string = 'swc';
if (typeof swc === 'string') {
swcDepName = swc;
swcInstance = require(transpilerConfigLocalResolveHelper(swc, true)) as typeof swcWasm;
swcInstance = require(transpilerConfigLocalResolveHelper(swc, true)) as SwcInstance;
} else if (swc == null) {
let swcResolved;
try {
Expand All @@ -44,9 +45,9 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler {
);
}
}
swcInstance = require(swcResolved) as typeof swcWasm;
swcInstance = require(swcResolved) as SwcInstance;
} else {
swcInstance = swc;
swcInstance = swc as any as SwcInstance;
}

// Prepare SWC options derived from typescript compiler options
Expand Down Expand Up @@ -142,6 +143,7 @@ export function createSwcOptions(
strict,
alwaysStrict,
noImplicitUseStrict,
jsxImportSource,
} = compilerOptions;

let swcTarget = targetMapping.get(target!) ?? 'es3';
Expand Down Expand Up @@ -194,6 +196,8 @@ export function createSwcOptions(
jsx === JsxEmit.ReactJSX || jsx === JsxEmit.ReactJSXDev ? 'automatic' : undefined;
const jsxDevelopment: swcTypes.ReactConfig['development'] = jsx === JsxEmit.ReactJSXDev ? true : undefined;

const useDefineForClassFields = getUseDefineForClassFields(compilerOptions);

const nonTsxOptions = createVariant(false);
const tsxOptions = createVariant(true);
return { nonTsxOptions, tsxOptions };
Expand All @@ -204,11 +208,15 @@ export function createSwcOptions(
// isModule: true,
module: moduleType
? {
noInterop: !esModuleInterop,
type: moduleType,
strictMode,
// For NodeNext and Node12, emit as CJS but do not transform dynamic imports
ignoreDynamic: nodeModuleEmitKind === 'nodecjs',
...(moduleType === 'amd' || moduleType === 'commonjs' || moduleType === 'umd'
? {
noInterop: !esModuleInterop,
strictMode,
// For NodeNext and Node12, emit as CJS but do not transform dynamic imports
ignoreDynamic: nodeModuleEmitKind === 'nodecjs',
}
: {}),
}
: undefined,
swcrc: false,
Expand All @@ -232,12 +240,15 @@ export function createSwcOptions(
pragma: jsxFactory!,
pragmaFrag: jsxFragmentFactory!,
runtime: jsxRuntime,
importSource: jsxImportSource,
},
useDefineForClassFields,
},
keepClassNames,
experimental: {
keepImportAssertions: true,
},
keepImportAttributes: true,
emitAssertForImportAttributes: true,
} as swcTypes.JscConfig['experimental'],
},
};

Expand Down
25 changes: 25 additions & 0 deletions src/ts-internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,28 @@ function replaceWildcardCharacter(match: string, singleAsteriskRegexFragment: st
function isImplicitGlob(lastPathComponent: string): boolean {
return !/[.*?]/.test(lastPathComponent);
}

const ts_ScriptTarget_ES5 = 1;
const ts_ScriptTarget_ES2022 = 9;
const ts_ScriptTarget_ESNext = 99;
const ts_ModuleKind_Node16 = 100;
const ts_ModuleKind_NodeNext = 199;
// https://github.com/microsoft/TypeScript/blob/fc418a2e611c88cf9afa0115ff73490b2397d311/src/compiler/utilities.ts#L8761
export function getUseDefineForClassFields(compilerOptions: _ts.CompilerOptions): boolean {
return compilerOptions.useDefineForClassFields === undefined
? getEmitScriptTarget(compilerOptions) >= ts_ScriptTarget_ES2022
: compilerOptions.useDefineForClassFields;
}

// https://github.com/microsoft/TypeScript/blob/fc418a2e611c88cf9afa0115ff73490b2397d311/src/compiler/utilities.ts#L8556
export function getEmitScriptTarget(compilerOptions: {
module?: _ts.CompilerOptions['module'];
target?: _ts.CompilerOptions['target'];
}): _ts.ScriptTarget {
return (
compilerOptions.target ??
((compilerOptions.module === ts_ModuleKind_Node16 && ts_ScriptTarget_ES2022) ||
(compilerOptions.module === ts_ModuleKind_NodeNext && ts_ScriptTarget_ESNext) ||
ts_ScriptTarget_ES5)
);
}
1 change: 1 addition & 0 deletions tests/1996/empty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
3 changes: 3 additions & 0 deletions tests/1996/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as empty from './empty';
empty;
console.log('#1996 regression test.');
13 changes: 13 additions & 0 deletions tests/1996/transpiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// A custom transpiler that returns `undefined` instead of a sourcemap, which is
// allowed according to our typedefs.

exports.create = function () {
return {
transpile(input, options) {
return {
outputText: 'console.log("#1996 regression test with custom transpiler.")',
sourceMapText: undefined,
};
},
};
};
Loading

1 comment on commit 0022f38

@RobinTail
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cspotcode , please release the compatibility fix with SWC

Please sign in to comment.