Skip to content

Commit

Permalink
feat(typescript): upgrade to TypeScript 5 (#4315)
Browse files Browse the repository at this point in the history
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: ionic-team/stencil-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.
  • Loading branch information
alicewriteswrongs committed May 22, 2023
1 parent feaae32 commit 0b6621f
Show file tree
Hide file tree
Showing 22 changed files with 8,368 additions and 417 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Expand Up @@ -11,6 +11,7 @@ module.exports = {
'@stencil/core/mock-doc': '<rootDir>/mock-doc/index.cjs',
'@stencil/core/testing': '<rootDir>/testing/index.js',
'@utils': '<rootDir>/src/utils',
'^typescript$': '<rootDir>/scripts/build/typescript-modified-for-jest.js',
},
coverageDirectory: './coverage/',
coverageReporters: ['json', 'lcov', 'text', 'clover'],
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -122,7 +122,7 @@
"semver": "^7.3.7",
"sizzle": "^2.3.6",
"terser": "5.17.4",
"typescript": "4.9.5",
"typescript": "~5.0.4",
"webpack": "^5.75.0",
"ws": "8.13.0"
},
Expand Down
116 changes: 83 additions & 33 deletions scripts/bundles/plugins/typescript-source-plugin.ts
Expand Up @@ -62,57 +62,107 @@ async function bundleTypeScriptSource(tsPath: string, opts: BuildOptions): Promi

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

// remove the CPUProfiler since it uses node apis
code = removeFromSource(code, `enableCPUProfiler: enableCPUProfiler,`);
code = removeFromSource(code, `disableCPUProfiler: disableCPUProfiler,`);
// TODO(STENCIL-816): remove in-browser compilation
code = removeFromSource(code, `enableCPUProfiler,`);
// TODO(STENCIL-816): remove in-browser compilation
code = removeFromSource(code, `disableCPUProfiler,`);

// As of 5.0, because typescript is now bundled with esbuild the structure of
// the file we're dealing with here (`lib/typescript.js`) has changed.
// Previously there was an iife which got an object as an argument and just
// stuck properties onto it, something like
//
// ```js
// var ts = (function (ts) {
// ts.someMethod = () => { ... };
// })(ts || ts = {});
// ```
//
// as of 5.0 it instead looks (conceptually) something like:
//
// ```js
// var ts = (function () {
// const ts = {}
// const define = (name, value) => {
// Object.defineProperty(ts, name, value, { enumerable: true })
// }
// define('someMethod', () => { ... })
// return ts;
// })();
// ```
//
// Note that the call to `Object.defineProperty` does not set `configurable` to `true`
// (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description)
// which means that later calls to do something like
//
// ```ts
// import ts from 'typescript';
//
// ts.someMethod = function myReplacementForSomeMethod () {
// ...
// };
// ```
//
// will fail because without `configurable: true` you can't re-assign
// properties.
//
// All well and good, except for the fact that our patching of typescript to
// use for instance the in-memory file system depends on us being able to
// monkey-patch typescript in exactly this way. So in order to retain our
// current approach to patching TypeScript we need to edit this file in order
// to add `configurable: true` to the options passed to
// `Object.defineProperty`:
const TS_PROP_DEFINER = `__defProp(target, name, { get: all[name], enumerable: true });`;
const TS_PROP_DEFINER_RECONFIGURABLE = `__defProp(target, name, { get: all[name], enumerable: true, configurable: true });`;

code = code.replace(TS_PROP_DEFINER, TS_PROP_DEFINER_RECONFIGURABLE);

const jestTypesciptFilename = join(opts.scriptsBuildDir, 'typescript-modified-for-jest.js');
await fs.writeFile(jestTypesciptFilename, code);

// Here we transform the TypeScript source from a commonjs to an ES module.
// We do this so that we can add an import from the `@environment` module.

// trim off the last part that sets module.exports and polyfills globalThis since
// we don't want typescript to add itself to module.exports when in a node env
const tsEnding = `})(ts || (ts = {}));`;
const tsEnding = `if (typeof module !== "undefined" && module.exports) { module.exports = ts; }`;

if (!code.includes(tsEnding)) {
throw new Error(`"${tsEnding}" not found`);
}
const lastEnding = code.lastIndexOf(tsEnding);
code = code.slice(0, lastEnding + tsEnding.length);

// there's a billion unnecessary "var ts;" for namespaces
// but we'll be using the top level "const ts" instead
code = code.replace(/var ts;/g, '');
code = code.slice(0, lastEnding);

// minification is crazy better if it doesn't use typescript's
// namespace closures, like (function(ts) {...})(ts = ts || {});
code = code.replace(/ \|\| \(ts \= \{\}\)/g, '');

// make a nice clean default export
// "process.browser" is used by typescript to know if it should use the node sys or not
const o: string[] = [];
o.push(`// TypeScript ${opts.typescriptVersion}`);
o.push(`import { IS_NODE_ENV } from '@environment';`);
o.push(`process.browser = !IS_NODE_ENV;`);
o.push(`const ts = {};`);
o.push(code);
o.push(`export default ts;`);
code = o.join('\n');

const { minify } = await import('terser');

if (opts.isProd) {
const minified = await minify(code, {
ecma: 2018,
module: true,
compress: {
ecma: 2018,
passes: 2,
},
format: {
ecma: 2018,
comments: false,
},
});
code = minified.code;
}
// TODO(STENCIL-839): investigate minification issue w/ typescript 5.0
// const { minify } = await import('terser');

// if (opts.isProd) {
// const minified = await minify(code, {
// ecma: 2018,
// // module: true,
// compress: {
// ecma: 2018,
// passes: 2,
// },
// format: {
// ecma: 2018,
// comments: false,
// },
// });
// code = minified.code;
// }

await fs.writeFile(cacheFile, code);

Expand Down
24 changes: 0 additions & 24 deletions src/compiler/sys/typescript/typescript-sys.ts
Expand Up @@ -198,7 +198,6 @@ export const patchTypescript = (config: d.Config, inMemoryFs: InMemoryFileSystem
patchTsSystemWatch(config.sys, ts.sys);
}
patchTypeScriptResolveModule(config, inMemoryFs);
patchTypeScriptGetParsedCommandLineOfConfigFile();
(ts as any).__patched = true;
}
};
Expand Down Expand Up @@ -243,26 +242,3 @@ export const getTypescriptPathFromUrl = (config: d.Config, tsExecutingUrl: strin
}
return url;
};

export const patchTypeScriptGetParsedCommandLineOfConfigFile = () => {
const orgGetParsedCommandLineOfConfigFile = ts.getParsedCommandLineOfConfigFile;

ts.getParsedCommandLineOfConfigFile = (configFileName, optionsToExtend, host, extendedConfigCache) => {
const results = orgGetParsedCommandLineOfConfigFile(configFileName, optionsToExtend, host, extendedConfigCache);

// manually filter out any .spec or .e2e files
results.fileNames = results.fileNames.filter((f) => {
// filter e2e tests
if (f.includes('.e2e.') || f.includes('/e2e.')) {
return false;
}
// filter spec tests
if (f.includes('.spec.') || f.includes('/spec.')) {
return false;
}
return true;
});

return results;
};
};

0 comments on commit 0b6621f

Please sign in to comment.