Skip to content

Commit

Permalink
Merge pull request #2058 from GoogleChrome/typescript
Browse files Browse the repository at this point in the history
Convert workbox-core to Typescript
  • Loading branch information
philipwalton authored Jun 13, 2019
2 parents b10f13f + 25a699b commit e33e8dc
Show file tree
Hide file tree
Showing 51 changed files with 1,357 additions and 736 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ generated-release-files/
workbox-*.json
.esm-cache/
.nyc_output/
.rpt2_cache/
docs
local-builds/
firebase-debug.log
.firebase

# Generated files from TypeScript source
packages/workbox-core/**/*.js
packages/workbox-core/**/*.mjs
packages/workbox-core/**/*.d.ts
41 changes: 31 additions & 10 deletions gulp-tasks/build-packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,37 @@
https://opensource.org/licenses/MIT.
*/

const fse = require('fs-extra');
const del = require('del');
const fs = require('fs-extra');
const gulp = require('gulp');
const path = require('path');

const constants = require('./utils/constants');
const packageRunnner = require('./utils/package-runner');

const cleanPackage = (packagePath) => {
const outputDirectory = path.join(packagePath,
constants.PACKAGE_BUILD_DIRNAME);
return fse.remove(outputDirectory);
const cleanPackage = async (packagePath) => {
// Delete generated files from the the TypeScript transpile.
if (await fs.pathExists(path.join(packagePath, 'src', 'index.ts'))) {
// Store the list of deleted files, so we can delete directories after.
const deletedPaths = await del([
path.posix.join(packagePath, '**/*.+(js|mjs|d.ts)'),
// Don't delete files in node_modules
'!**/node_modules', '!**/node_modules/**/*',
]);

// Any directories in `deletedPaths` that are top-level directories to the
// package should also be deleted since those directories should only
// contain generated `.mjs` and `.d.ts` files.
const directoriesToDelete = new Set();
for (const deletedPath of deletedPaths) {
const relativePath = path.relative(packagePath, deletedPath);
const directory = relativePath.split(path.sep)[0];
directoriesToDelete.add(path.join(packagePath, directory));
}
await del([...directoriesToDelete]);
}
// Delete build files.
await del(path.join(packagePath, constants.PACKAGE_BUILD_DIRNAME));
};

gulp.task('build-packages:clean', gulp.series(
Expand All @@ -27,11 +47,12 @@ gulp.task('build-packages:clean', gulp.series(
)
));

gulp.task('build-packages:build', gulp.parallel(
'build-node-packages',
'build-browser-packages',
'build-window-packages'
));
gulp.task('build-packages:build', gulp.series(
'transpile-typescript',
gulp.parallel(
'build-node-packages',
'build-browser-packages',
'build-window-packages')));

gulp.task('build-packages', gulp.series(
'build-packages:clean',
Expand Down
8 changes: 4 additions & 4 deletions gulp-tasks/test-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const handleExit = () => {
};

const startServer = () => {
process.env.NODE_ENV = process.env.NODE_ENV || constants.BUILD_TYPES.dev;

const eventNames = [
'exit',
'SIGINT',
Expand All @@ -31,7 +33,5 @@ const startServer = () => {
return testServer.start();
};

gulp.task('test-server', () => {
process.env.NODE_ENV = process.env.NODE_ENV || constants.BUILD_TYPES.dev;
startServer();
});
gulp.task('test-server', gulp.series(
'transpile-typescript', startServer, 'transpile-typescript:watch'));
179 changes: 179 additions & 0 deletions gulp-tasks/transpile-typescript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/

const fs = require('fs-extra');
const gulp = require('gulp');
const ol = require('common-tags').oneLine;
const path = require('path');
const {rollup} = require('rollup');
const resolve = require('rollup-plugin-node-resolve');
const typescript2 = require('rollup-plugin-typescript2');
const packageRunnner = require('./utils/package-runner');
const logHelper = require('../infra/utils/log-helper');
const {AsyncDebounce} = require('../infra/utils/AsyncDebounce');


/**
* @type {string}
*/
const packagesDir = path.join(__dirname, '..', 'packages');


/**
* Returns a Rollup plugin that generates both `.js` and `.mjs` files for each
* `.ts` source file. This is a bit of a hack, but it's needed until to solve:
* https://github.com/rollup/rollup/issues/2847
*/
const generateMjsOutputFiles = () => {
return {
generateBundle: (options, bundle) => {
const importPattern =
new RegExp(`((?:import|export)[^']+')([^']+?)\\.ts';`, 'g');

for (const chunkInfo of Object.values(bundle)) {
if (chunkInfo.fileName.endsWith('.ts')) {
// Rename the `.ts` imports and filenames to `.js`;
const assetBasename = chunkInfo.fileName.replace(/.ts$/, '');

chunkInfo.fileName = assetBasename + '.js';
chunkInfo.code = chunkInfo.code.replace(importPattern, `$1$2.js';`);

// Create mirror `.mjs` files for each `.js` file.
const mjsSource =
`export * from './${path.basename(assetBasename)}.js';`;

fs.outputFileSync(
path.join(options.dir, assetBasename + '.mjs'), mjsSource);
}
}
},
};
};

/**
* @type {RollupCache}
*/
let moduleBundleCache;

/**
* Transpiles a package's source TypeScript files to `.mjs` JavaScript files
* in the root package directory, along with the corresponding definition
* files.
*
* @param {string} packageName
*/
const transpilePackage = async (packageName) => {
const packagePath = path.join(packagesDir, packageName);
const input = path.join(packagePath, 'src', 'index.ts');

const plugins = [
resolve(),
typescript2(),
generateMjsOutputFiles(),
];

const bundle = await rollup({
input,
plugins,
cache: moduleBundleCache,
preserveModules: true,
// We don't need to tree shake the TS to JS conversion, as that'll happen
// later, and we don't need to add to the transpilation time.
treeshake: false,
});

// Ensure rebuilds are as fast as possible in watch mode.
moduleBundleCache = bundle.cache;

await bundle.write({
dir: packagePath,
format: 'esm',
});
};

/**
* Transpiles a package iff it has TypeScript source files.
*
* @param {string} packagePath
*/
const transpilePackagesOrSkip = async (packagePath) => {
// `packagePath` will be posix style because it comes from `glog()`.
const packageName = packagePath.split('/').slice(-1)[0];

if (await fs.pathExists(path.join(packagePath, 'src'))) {
await queueTranspile(packageName);
} else {
logHelper.log(ol`Skipping package '${packageName}'
not yet converted to typescript.`);
}
};

/**
* A mapping between each package name and its corresponding `AsyncDebounce`
* instance of the `transpilePackage()` function.
*
* @type {{[key: string]: AsyncDebounce}}
*/
const debouncedTranspilerMap = {};

/**
* Takes a package name and schedules that package's source TypeScript code
* to be converted to JavaScript. If a transpilation is already scheduled, it
* won't be queued twice, so this function is safe to call as frequenty as
* needed.
*
* @param {string} packageName
*/
const queueTranspile = async (packageName) => {
if (!debouncedTranspilerMap[packageName]) {
debouncedTranspilerMap[packageName] = new AsyncDebounce(async () => {
await transpilePackage(packageName);
});
}
await debouncedTranspilerMap[packageName].call();
};

/**
* A mapping between package names and whether or not they have pending
* file changes
*
* @type {{[key: string]: boolean}}
*/
const pendingChangesMap = {};

/**
* Returns true if a TypeScript file has been modified in the package since
* the last time it was transpiled.
*
* @param {string} packageName
*/
const needsTranspile = (packageName) => {
return pendingChangesMap[packageName] === true;
};

gulp.task('transpile-typescript', gulp.series(packageRunnner(
'transpile-typescript', 'browser', transpilePackagesOrSkip)));

gulp.task('transpile-typescript:watch', gulp.series(packageRunnner(
'transpile-typescript', 'browser', transpilePackagesOrSkip)));

gulp.task('transpile-typescript:watch', () => {
const watcher = gulp.watch(`./${global.packageOrStar}/workbox-*/src/**/*.ts`);
watcher.on('all', async (event, file) => {
const changedPkg = path.relative(packagesDir, file).split(path.sep)[0];

pendingChangesMap[changedPkg] = true;
await queueTranspile(changedPkg);
pendingChangesMap[changedPkg] = false;
});
});

module.exports = {
queueTranspile,
needsTranspile,
};
15 changes: 11 additions & 4 deletions gulp-tasks/utils/version-module.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ const getDetails = (packagePath) => {
return ['workbox', name, pkgJson.version].join(':');
};

module.exports = (packagePath, buildType) => {
module.exports = async (packagePath, buildType) => {
const versionString =
`try{self['${getDetails(packagePath)}']&&_()}catch(e){}` +
`// eslint-disable-line`;
return fs.writeFile(path.join(packagePath, '_version.mjs'), versionString);
`try{self['${getDetails(packagePath)}']&&_()}catch(e){}`;

if (await fs.pathExists(path.join(packagePath, 'src', 'index.ts'))) {
const tsVersionString = `// @ts-ignore\n${versionString}`;
await fs.writeFile(
path.join(packagePath, 'src', '_version.ts'), tsVersionString);
}

const mjsVersionString = `${versionString}// eslint-disable-line`;
return fs.writeFile(path.join(packagePath, '_version.mjs'), mjsVersionString);
};
1 change: 1 addition & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ global.cliOptions = options;
// Forward referencing means the order of gulp-task
// requires is important
const gulpTaskFiles = [
'transpile-typescript',
'build-node-packages',
'build-browser-packages',
'build-window-packages',
Expand Down
9 changes: 6 additions & 3 deletions infra/testing/server/routes/build-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ async function handler(req, res) {
} else {
const pkg = outputFilenameToPkgMap[packageFile];

// If the pkg.module references something in the build directory, use
// that, otherwise use pkg.main.
// If the pkg.module or pkg.main references something in the build
// directory, use that. Otherwise base the build file on the pkg name.

if (pkg.module && pkg.module.startsWith('build/')) {
file = path.join(pkgDir, pkg.module);
} else {
} else if (pkg.main && pkg.main.startsWith('build/')) {
file = path.join(pkgDir, pkg.main);
} else {
file = path.join(pkgDir, 'build', `${pkg.name}.prod.js`);
}

// When not specifying a dev or prod build via the filename,
Expand Down
9 changes: 8 additions & 1 deletion infra/testing/server/routes/sw-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const replace = require('rollup-plugin-replace');
const resolve = require('rollup-plugin-node-resolve');
const multiEntry = require('rollup-plugin-multi-entry');
const commonjs = require('rollup-plugin-commonjs');
const {needsTranspile, queueTranspile} = require('../../../../gulp-tasks/transpile-typescript');
const {getPackages} = require('../../../../gulp-tasks/utils/get-packages');


Expand All @@ -23,9 +24,15 @@ const caches = {};

async function handler(req, res) {
const env = process.env.NODE_ENV || 'development';
const packageName = req.params.package;

// Ensure the TypeScript transpile step has completed first.
if (needsTranspile(packageName)) {
await queueTranspile(packageName);
}

const bundle = await rollup({
input: `./test/${req.params.package}/sw/**/test-*.mjs`,
input: `./test/${packageName}/sw/**/test-*.mjs`,
plugins: [
multiEntry(),
resolve({
Expand Down
27 changes: 27 additions & 0 deletions infra/type-overrides.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// IndexedDB
// ------------------------------------------------------------------------- //

interface IDBIndex {
openCursor(range?: IDBValidKey | IDBKeyRange | null, direction?: IDBCursorDirection): IDBRequest<IDBCursorWithValue | null>;
openKeyCursor(range?: IDBValidKey | IDBKeyRange | null, direction?: IDBCursorDirection): IDBRequest<IDBCursor | null>;
}

interface IDBObjectStore {
openCursor(range?: IDBValidKey | IDBKeyRange | null, direction?: IDBCursorDirection): IDBRequest<IDBCursorWithValue | null>;
openKeyCursor(query?: IDBValidKey | IDBKeyRange | null, direction?: IDBCursorDirection): IDBRequest<IDBCursor | null>;
}

// Service Worker
// ------------------------------------------------------------------------- //

interface Clients {
claim(): Promise<any>;
}

// NOTE(philipwalton): we need to use `WorkerGlobalScope` here because
// TypeScript does not allow us to re-declare self to a different type, and
// currently TypeScript only has a webworker types files (no SW types).
interface WorkerGlobalScope {
clients: Clients;
skipWaiting(): void;
}
Loading

0 comments on commit e33e8dc

Please sign in to comment.