Skip to content

Commit 0b6621f

Browse files
feat(typescript): upgrade to TypeScript 5 (#4315)
This upgrades the version of TypeScript bundled with Stencil to 5.0.4. In order to support the upgrade we have to do a few things: - edit the Rollup plugin we use to bundle typescript to account for changes in how typescript is bundled now (this changed for the 5.0.0 release) - fix tests which assert on e.g. transformer output to account for small, mostly one-character differences in output with the strings we have in tests - move the location where we prevent `.d.ts` files being written to disk during `stencil build` for test files See also a documentation PR here: stenciljs/site#1117 It was also necessary to make a few tweaks to how the Karma tests are run to account for some changes in how TypeScript resolves modules.
1 parent feaae32 commit 0b6621f

22 files changed

+8368
-417
lines changed

jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = {
1111
'@stencil/core/mock-doc': '<rootDir>/mock-doc/index.cjs',
1212
'@stencil/core/testing': '<rootDir>/testing/index.js',
1313
'@utils': '<rootDir>/src/utils',
14+
'^typescript$': '<rootDir>/scripts/build/typescript-modified-for-jest.js',
1415
},
1516
coverageDirectory: './coverage/',
1617
coverageReporters: ['json', 'lcov', 'text', 'clover'],

package-lock.json

+8-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
"semver": "^7.3.7",
123123
"sizzle": "^2.3.6",
124124
"terser": "5.17.4",
125-
"typescript": "4.9.5",
125+
"typescript": "~5.0.4",
126126
"webpack": "^5.75.0",
127127
"ws": "8.13.0"
128128
},

scripts/bundles/plugins/typescript-source-plugin.ts

+83-33
Original file line numberDiff line numberDiff line change
@@ -62,57 +62,107 @@ async function bundleTypeScriptSource(tsPath: string, opts: BuildOptions): Promi
6262

6363
// remove the default ts.getDefaultLibFilePath because it uses some
6464
// node apis and we'll be replacing it with our own anyways
65-
code = removeFromSource(code, `ts.getDefaultLibFilePath = getDefaultLibFilePath;`);
65+
// TODO(STENCIL-816): remove in-browser compilation
66+
code = removeFromSource(code, `getDefaultLibFilePath: () => getDefaultLibFilePath,`);
6667

6768
// remove the CPUProfiler since it uses node apis
68-
code = removeFromSource(code, `enableCPUProfiler: enableCPUProfiler,`);
69-
code = removeFromSource(code, `disableCPUProfiler: disableCPUProfiler,`);
69+
// TODO(STENCIL-816): remove in-browser compilation
70+
code = removeFromSource(code, `enableCPUProfiler,`);
71+
// TODO(STENCIL-816): remove in-browser compilation
72+
code = removeFromSource(code, `disableCPUProfiler,`);
73+
74+
// As of 5.0, because typescript is now bundled with esbuild the structure of
75+
// the file we're dealing with here (`lib/typescript.js`) has changed.
76+
// Previously there was an iife which got an object as an argument and just
77+
// stuck properties onto it, something like
78+
//
79+
// ```js
80+
// var ts = (function (ts) {
81+
// ts.someMethod = () => { ... };
82+
// })(ts || ts = {});
83+
// ```
84+
//
85+
// as of 5.0 it instead looks (conceptually) something like:
86+
//
87+
// ```js
88+
// var ts = (function () {
89+
// const ts = {}
90+
// const define = (name, value) => {
91+
// Object.defineProperty(ts, name, value, { enumerable: true })
92+
// }
93+
// define('someMethod', () => { ... })
94+
// return ts;
95+
// })();
96+
// ```
97+
//
98+
// Note that the call to `Object.defineProperty` does not set `configurable` to `true`
99+
// (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description)
100+
// which means that later calls to do something like
101+
//
102+
// ```ts
103+
// import ts from 'typescript';
104+
//
105+
// ts.someMethod = function myReplacementForSomeMethod () {
106+
// ...
107+
// };
108+
// ```
109+
//
110+
// will fail because without `configurable: true` you can't re-assign
111+
// properties.
112+
//
113+
// All well and good, except for the fact that our patching of typescript to
114+
// use for instance the in-memory file system depends on us being able to
115+
// monkey-patch typescript in exactly this way. So in order to retain our
116+
// current approach to patching TypeScript we need to edit this file in order
117+
// to add `configurable: true` to the options passed to
118+
// `Object.defineProperty`:
119+
const TS_PROP_DEFINER = `__defProp(target, name, { get: all[name], enumerable: true });`;
120+
const TS_PROP_DEFINER_RECONFIGURABLE = `__defProp(target, name, { get: all[name], enumerable: true, configurable: true });`;
121+
122+
code = code.replace(TS_PROP_DEFINER, TS_PROP_DEFINER_RECONFIGURABLE);
123+
124+
const jestTypesciptFilename = join(opts.scriptsBuildDir, 'typescript-modified-for-jest.js');
125+
await fs.writeFile(jestTypesciptFilename, code);
126+
127+
// Here we transform the TypeScript source from a commonjs to an ES module.
128+
// We do this so that we can add an import from the `@environment` module.
70129

71130
// trim off the last part that sets module.exports and polyfills globalThis since
72131
// we don't want typescript to add itself to module.exports when in a node env
73-
const tsEnding = `})(ts || (ts = {}));`;
132+
const tsEnding = `if (typeof module !== "undefined" && module.exports) { module.exports = ts; }`;
133+
74134
if (!code.includes(tsEnding)) {
75135
throw new Error(`"${tsEnding}" not found`);
76136
}
77137
const lastEnding = code.lastIndexOf(tsEnding);
78-
code = code.slice(0, lastEnding + tsEnding.length);
79-
80-
// there's a billion unnecessary "var ts;" for namespaces
81-
// but we'll be using the top level "const ts" instead
82-
code = code.replace(/var ts;/g, '');
138+
code = code.slice(0, lastEnding);
83139

84-
// minification is crazy better if it doesn't use typescript's
85-
// namespace closures, like (function(ts) {...})(ts = ts || {});
86-
code = code.replace(/ \|\| \(ts \= \{\}\)/g, '');
87-
88-
// make a nice clean default export
89-
// "process.browser" is used by typescript to know if it should use the node sys or not
90140
const o: string[] = [];
91141
o.push(`// TypeScript ${opts.typescriptVersion}`);
92142
o.push(`import { IS_NODE_ENV } from '@environment';`);
93143
o.push(`process.browser = !IS_NODE_ENV;`);
94-
o.push(`const ts = {};`);
95144
o.push(code);
96145
o.push(`export default ts;`);
97146
code = o.join('\n');
98147

99-
const { minify } = await import('terser');
100-
101-
if (opts.isProd) {
102-
const minified = await minify(code, {
103-
ecma: 2018,
104-
module: true,
105-
compress: {
106-
ecma: 2018,
107-
passes: 2,
108-
},
109-
format: {
110-
ecma: 2018,
111-
comments: false,
112-
},
113-
});
114-
code = minified.code;
115-
}
148+
// TODO(STENCIL-839): investigate minification issue w/ typescript 5.0
149+
// const { minify } = await import('terser');
150+
151+
// if (opts.isProd) {
152+
// const minified = await minify(code, {
153+
// ecma: 2018,
154+
// // module: true,
155+
// compress: {
156+
// ecma: 2018,
157+
// passes: 2,
158+
// },
159+
// format: {
160+
// ecma: 2018,
161+
// comments: false,
162+
// },
163+
// });
164+
// code = minified.code;
165+
// }
116166

117167
await fs.writeFile(cacheFile, code);
118168

src/compiler/sys/typescript/typescript-sys.ts

-24
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,6 @@ export const patchTypescript = (config: d.Config, inMemoryFs: InMemoryFileSystem
198198
patchTsSystemWatch(config.sys, ts.sys);
199199
}
200200
patchTypeScriptResolveModule(config, inMemoryFs);
201-
patchTypeScriptGetParsedCommandLineOfConfigFile();
202201
(ts as any).__patched = true;
203202
}
204203
};
@@ -243,26 +242,3 @@ export const getTypescriptPathFromUrl = (config: d.Config, tsExecutingUrl: strin
243242
}
244243
return url;
245244
};
246-
247-
export const patchTypeScriptGetParsedCommandLineOfConfigFile = () => {
248-
const orgGetParsedCommandLineOfConfigFile = ts.getParsedCommandLineOfConfigFile;
249-
250-
ts.getParsedCommandLineOfConfigFile = (configFileName, optionsToExtend, host, extendedConfigCache) => {
251-
const results = orgGetParsedCommandLineOfConfigFile(configFileName, optionsToExtend, host, extendedConfigCache);
252-
253-
// manually filter out any .spec or .e2e files
254-
results.fileNames = results.fileNames.filter((f) => {
255-
// filter e2e tests
256-
if (f.includes('.e2e.') || f.includes('/e2e.')) {
257-
return false;
258-
}
259-
// filter spec tests
260-
if (f.includes('.spec.') || f.includes('/spec.')) {
261-
return false;
262-
}
263-
return true;
264-
});
265-
266-
return results;
267-
};
268-
};

0 commit comments

Comments
 (0)