Skip to content

Commit

Permalink
feat(typescript): bundle typescript
Browse files Browse the repository at this point in the history
- Stencil dependency free
- Faster compiler startup times
- Reduce overall download and install time
- Simplify requiring typescript for the various platforms, such as deno, browser and node
- Ensure the stencil compiler is always using the typescript version it's built with
  • Loading branch information
adamdbradley committed Aug 4, 2020
1 parent 4b9f8ab commit 1973032
Show file tree
Hide file tree
Showing 25 changed files with 331 additions and 351 deletions.
50 changes: 38 additions & 12 deletions scripts/bundles/compiler.ts
Expand Up @@ -14,6 +14,7 @@ import { sysModulesPlugin } from './plugins/sys-modules-plugin';
import { writePkgJson } from '../utils/write-pkg-json';
import { BuildOptions } from '../utils/options';
import { RollupOptions, OutputChunk } from 'rollup';
import { typescriptSourcePlugin } from './plugins/typescript-source-plugin';
import terser from 'terser';

export async function compiler(opts: BuildOptions) {
Expand Down Expand Up @@ -46,17 +47,23 @@ export async function compiler(opts: BuildOptions) {
intro: cjsIntro,
outro: cjsOutro,
strict: false,
banner: getBanner(opts, 'Stencil Compiler', true),
banner: getBanner(opts, `Stencil Compiler`, true),
esModule: false,
preferConst: true,
freeze: false,
sourcemap: false,
},
plugins: [
typescriptSourcePlugin(opts),
{
name: 'compilerMockDocResolvePlugin',
resolveId(id) {
if (id === '@stencil/core/mock-doc') {
return join(opts.buildDir, 'mock-doc', 'index.js');
}
if (id === '@microsoft/typescript-etw' || id === 'inspector') {
return id;
}
return null;
},
},
Expand All @@ -68,7 +75,7 @@ export async function compiler(opts: BuildOptions) {
}
},
load(id) {
if (id === 'fsevents') {
if (id === 'fsevents' || id === '@microsoft/typescript-etw' || id === 'inspector') {
return '';
}
if (id === rollupWatchPath) {
Expand Down Expand Up @@ -107,7 +114,7 @@ export async function compiler(opts: BuildOptions) {
if (opts.isProd) {
const compilerFilename = Object.keys(bundleFiles).find(f => f.includes('stencil'));
const compilerBundle = bundleFiles[compilerFilename] as OutputChunk;
const minified = minifyStencilCompiler(compilerBundle.code);
const minified = minifyStencilCompiler(compilerBundle.code, opts);
await fs.writeFile(join(opts.output.compilerDir, compilerFilename.replace('.js', '.min.js')), minified);
}
},
Expand All @@ -118,28 +125,47 @@ export async function compiler(opts: BuildOptions) {
propertyReadSideEffects: false,
unknownGlobalSideEffects: false,
},
onwarn(warning) {
if (warning.code === `THIS_IS_UNDEFINED`) {
return;
}
console.warn(warning.message || warning);
},
};

// copy typescript default lib dts files
const dtsFiles = (await fs.readdir(opts.typescriptLibDir)).filter(f => {
return f.startsWith('lib.') && f.endsWith('.d.ts');
});

await Promise.all(dtsFiles.map(f => fs.copy(join(opts.typescriptLibDir, f), join(opts.output.compilerDir, f))));

return [compilerBundle];
}

function minifyStencilCompiler(code: string) {
const opts: terser.MinifyOptions = {
ecma: 2017,
function minifyStencilCompiler(code: string, opts: BuildOptions) {
const minifyOpts: terser.MinifyOptions = {
ecma: 2018,
compress: {
ecma: 2018,
passes: 2,
ecma: 2017,
side_effects: false,
unsafe_arrows: true,
unsafe_methods: true,
},
output: {
ecma: 2017,
ecma: 2018,
comments: false,
},
};

const minifyResults = terser.minify(code, opts);
const results = terser.minify(code, minifyOpts);

if (minifyResults.error) {
throw minifyResults.error;
if (results.error) {
throw results.error;
}

return minifyResults.code;
code = getBanner(opts, `Stencil Compiler`, true) + '\n' + results.code;

return code;
}
3 changes: 2 additions & 1 deletion scripts/bundles/helpers/compiler-cjs-intro.js
Expand Up @@ -32,4 +32,5 @@ if (!process.platform) {
}
if (!process.version) {
process.version = 'v12.0.0';
}
}
process.browser = !!globalThis.location;
2 changes: 1 addition & 1 deletion scripts/bundles/plugins/sys-modules-plugin.ts
@@ -1,7 +1,7 @@
import path from 'path';
import { Plugin } from 'rollup';

const modules = new Set(['crypto', 'events', 'fs', 'module', 'os', 'path', 'stream', 'typescript', 'url', 'util']);
const modules = new Set(['crypto', 'events', 'fs', 'module', 'os', 'path', 'stream', 'url', 'util']);

export function sysModulesPlugin(inputDir: string): Plugin {
return {
Expand Down
105 changes: 105 additions & 0 deletions scripts/bundles/plugins/typescript-source-plugin.ts
@@ -0,0 +1,105 @@
import fs from 'fs-extra';
import { Plugin } from 'rollup';
import { join } from 'path';
import { BuildOptions } from '../../utils/options';
import terser from 'terser';

export function typescriptSourcePlugin(opts: BuildOptions): Plugin {
const tsPath = require.resolve('typescript');
return {
name: 'typescriptSourcePlugin',
resolveId(id) {
if (id === 'typescript') {
return tsPath;
}
return null;
},
load(id) {
if (id === tsPath) {
return bundleTypeScriptSource(tsPath, opts);
}
return null;
},
};
}

async function bundleTypeScriptSource(tsPath: string, opts: BuildOptions) {
const fileName = `typescript-${opts.typescriptVersion.replace(/\./g, '_')}-bundle-cache${opts.isProd ? '.min' : ''}.js`;
const cacheFile = join(opts.buildDir, fileName);

try {
// check if we've already cached this bundle
return await fs.readFile(cacheFile, 'utf8');
} catch (e) {}

// get the source typescript.js file to modify
let code = await fs.readFile(tsPath, 'utf8');

// remove the default ts.getDefaultLibFilePath because it uses some
// node apis and we'll be replacing it withour own anyways and
code = removeFromSource(code, `ts.getDefaultLibFilePath = getDefaultLibFilePath;`);

// remove the CPUProfiler since it uses node apis
code = removeFromSource(code, `enableCPUProfiler: enableCPUProfiler,`);
code = removeFromSource(code, `disableCPUProfiler: disableCPUProfiler,`);

// 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 = {}));`;
if (!code.includes(tsEnding)) {
throw new Error(`"${tsEnding}" not found`);
}
const lastEnding = code.lastIndexOf(tsEnding);
code = code.substr(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, '');

// 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
// this ensures its using our checks. Deno should also use process.browser = true
// because we don't want deno using the node apis
const o: string[] = [];
o.push(`import { IS_NODE_ENV } from '@utils';`);
o.push(`process.browser = !IS_NODE_ENV;`);
o.push(`const ts = {};`);
o.push(code);
o.push(`export default ts;`);
code = o.join('\n');

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

if (minified.error) {
throw minified.error;
}
code = minified.code;
}

await fs.writeFile(cacheFile, code);

return code;
}

function removeFromSource(srcCode: string, removeCode: string) {
if (!srcCode.includes(removeCode)) {
throw new Error(`"${removeCode}" not found`);
}
return srcCode.replace(removeCode, `/* commented out: ${removeCode} */`);
}
12 changes: 11 additions & 1 deletion scripts/utils/options.ts
Expand Up @@ -9,6 +9,8 @@ export function getOptions(rootDir: string, inputOpts: BuildOptions = {}) {
const packageLockJsonPath = join(rootDir, 'package-lock.json');
const changelogPath = join(rootDir, 'CHANGELOG.md');
const nodeModulesDir = join(rootDir, 'node_modules');
const typescriptDir = join(nodeModulesDir, 'typescript');
const typescriptLibDir = join(typescriptDir, 'lib');
const buildDir = join(rootDir, 'build');
const scriptsDir = join(rootDir, 'scripts');
const scriptsBundlesDir = join(scriptsDir, 'bundles');
Expand All @@ -23,6 +25,8 @@ export function getOptions(rootDir: string, inputOpts: BuildOptions = {}) {
packageLockJsonPath,
changelogPath,
nodeModulesDir,
typescriptDir,
typescriptLibDir,
buildDir,
scriptsDir,
scriptsBundlesDir,
Expand Down Expand Up @@ -78,7 +82,7 @@ export function getOptions(rootDir: string, inputOpts: BuildOptions = {}) {
export function createReplaceData(opts: BuildOptions) {
const CACHE_BUSTER = 6;

const typescriptPkg = require(join(opts.nodeModulesDir, 'typescript', 'package.json'));
const typescriptPkg = require(join(opts.typescriptDir, 'package.json'));
opts.typescriptVersion = typescriptPkg.version;
const transpileId = typescriptPkg.name + typescriptPkg.version + '_' + CACHE_BUSTER;

Expand All @@ -95,6 +99,9 @@ export function createReplaceData(opts: BuildOptions) {

const optimizeCssId = autoprefixerPkg.name + autoprefixerPkg.version + '_' + postcssPkg.name + postcssPkg.version + '_' + CACHE_BUSTER;

const parse5Pkg = require(join(opts.nodeModulesDir, 'parse5', 'package.json'));
opts.parse5Verion = parse5Pkg.version;

const data = readJSONSync(join(opts.srcDir, 'compiler', 'sys', 'dependencies.json'));
data.dependencies[0].version = opts.version;
data.dependencies[1].version = typescriptPkg.version;
Expand Down Expand Up @@ -124,6 +131,8 @@ export interface BuildOptions {
rootDir?: string;
srcDir?: string;
nodeModulesDir?: string;
typescriptDir?: string;
typescriptLibDir?: string;
buildDir?: string;
scriptsDir?: string;
scriptsBundlesDir?: string;
Expand Down Expand Up @@ -154,6 +163,7 @@ export interface BuildOptions {
tag?: string;
typescriptVersion?: string;
rollupVersion?: string;
parse5Verion?: string;
terserVersion?: string;
}

Expand Down
1 change: 0 additions & 1 deletion src/cli/run.ts
Expand Up @@ -74,7 +74,6 @@ export const run = async (init: CliInitOptions) => {
configPath: findConfigResults.configPath,
logger,
sys,
typescriptPath: ensureDepsResults.typescriptPath,
});

if (validated.diagnostics.length > 0) {
Expand Down
10 changes: 8 additions & 2 deletions src/compiler/compiler.ts
Expand Up @@ -7,8 +7,10 @@ import { createSysWorker } from './sys/worker/sys-worker';
import { createWatchBuild } from './build/watch-build';
import { getConfig } from './sys/config';
import { patchFs } from './sys/fs-patch';
import { patchTypescript } from './sys/typescript/typescript-patch';
import { patchTypescript } from './sys/typescript/typescript-sys';
import { resolveModuleIdAsync } from './sys/resolve/resolve-module-async';
import { isFunction } from '@utils';
import ts from 'typescript';

export const createCompiler = async (config: Config) => {
// actual compiler code
Expand All @@ -19,6 +21,10 @@ export const createCompiler = async (config: Config) => {
const sys = config.sys;
const compilerCtx = new CompilerContext();

if (isFunction(config.sys.setupCompiler)) {
config.sys.setupCompiler({ ts });
}

patchFs(sys);

compilerCtx.fs = createInMemoryFs(sys);
Expand All @@ -32,7 +38,7 @@ export const createCompiler = async (config: Config) => {
// Pipe events from sys.events to compilerCtx
sys.events.on(compilerCtx.events.emit);
}
await patchTypescript(config, diagnostics, compilerCtx.fs);
patchTypescript(config, compilerCtx.fs);

const build = () => createFullBuild(config, compilerCtx);

Expand Down

0 comments on commit 1973032

Please sign in to comment.