Internal only refactoring
PiperOrigin-RevId: 221698301
diff --git a/examples/googmodule/a.ts b/examples/googmodule/a.ts
index d8c9a7d..4f633f6 100644
--- a/examples/googmodule/a.ts
+++ b/examples/googmodule/a.ts
@@ -1 +1 @@
-export const a: number = 1;
+const a: number = 1;
diff --git a/internal/BUILD.bazel b/internal/BUILD.bazel
index 908addd..3762b8c 100644
--- a/internal/BUILD.bazel
+++ b/internal/BUILD.bazel
@@ -107,7 +107,10 @@
srcs = [],
deps = [
":test_lib",
+ "@npm//bytebuffer",
"@npm//jasmine",
+ "@npm//protobufjs",
+ "@npm//source-map",
"@npm//typescript",
],
)
diff --git a/internal/tsc_wrapped/tsc_wrapped.ts b/internal/tsc_wrapped/tsc_wrapped.ts
index 768127e..259f7d7 100644
--- a/internal/tsc_wrapped/tsc_wrapped.ts
+++ b/internal/tsc_wrapped/tsc_wrapped.ts
@@ -1,3 +1,4 @@
+import * as fs from 'fs';
import * as path from 'path';
import * as tsickle from 'tsickle';
import * as ts from 'typescript';
@@ -7,6 +8,7 @@
import {CachedFileLoader, FileLoader, ProgramAndFileCache, UncachedFileLoader} from './cache';
import {CompilerHost} from './compiler_host';
import * as bazelDiagnostics from './diagnostics';
+import {constructManifest} from './manifest';
import * as perfTrace from './perf_trace';
import {PLUGIN as strictDepsPlugin} from './strict_deps';
import {BazelOptions, parseTsconfig, resolveNormalizedPath} from './tsconfig';
@@ -200,43 +202,21 @@
cache.putProgram(bazelOpts.target, program);
- const compilationTargets =
- program.getSourceFiles().filter(f => isCompilationTarget(bazelOpts, f));
- const emitResults: ts.EmitResult[] = [];
- const diagnostics: ts.Diagnostic[] = [];
- if (bazelOpts.tsickle) {
- // The 'tsickle' import above is only used in type positions, so it won't
- // result in a runtime dependency on tsickle.
- // If the user requests the tsickle emit, then we dynamically require it
- // here for use at runtime.
- let optTsickle: typeof tsickle;
- try {
- // tslint:disable-next-line:no-require-imports dependency on tsickle only
- // if requested
- optTsickle = require('tsickle');
- } catch {
- throw new Error(
- 'When setting bazelOpts { tsickle: true }, ' +
- 'you must also add a devDependency on the tsickle npm package');
- }
- for (const sf of compilationTargets) {
- emitResults.push(optTsickle.emitWithTsickle(
- program, compilerHost, compilerHost, options, sf));
- }
- diagnostics.push(
- ...optTsickle.mergeEmitResults(emitResults as tsickle.EmitResult[])
- .diagnostics);
- } else {
- for (const sf of compilationTargets) {
- emitResults.push(program.emit(sf));
- }
+ const compilationTargets = program.getSourceFiles().filter(
+ fileName => isCompilationTarget(bazelOpts, fileName));
- for (const d of emitResults) {
- diagnostics.push(...d.diagnostics);
- }
+ let diagnostics: ts.Diagnostic[] = [];
+ let useTsickleEmit = bazelOpts.tsickle;
+ if (useTsickleEmit) {
+ diagnostics = emitWithTsickle(
+ program, compilerHost, compilationTargets, options, bazelOpts);
+ } else {
+ diagnostics = emitWithTypescript(program, compilationTargets);
}
+
if (diagnostics.length > 0) {
console.error(bazelDiagnostics.format(bazelOpts.target, diagnostics));
+ debug('compilation failed at', new Error().stack!);
return false;
}
@@ -244,6 +224,113 @@
return true;
}
+function emitWithTypescript(
+ program: ts.Program, compilationTargets: ts.SourceFile[]): ts.Diagnostic[] {
+ const diagnostics: ts.Diagnostic[] = [];
+ for (const sf of compilationTargets) {
+ const result = program.emit(sf);
+ diagnostics.push(...result.diagnostics);
+ }
+ return diagnostics;
+}
+
+function emitWithTsickle(
+ program: ts.Program, compilerHost: CompilerHost,
+ compilationTargets: ts.SourceFile[], options: ts.CompilerOptions,
+ bazelOpts: BazelOptions): ts.Diagnostic[] {
+ const emitResults: tsickle.EmitResult[] = [];
+ const diagnostics: ts.Diagnostic[] = [];
+ // The 'tsickle' import above is only used in type positions, so it won't
+ // result in a runtime dependency on tsickle.
+ // If the user requests the tsickle emit, then we dynamically require it
+ // here for use at runtime.
+ let optTsickle: typeof tsickle;
+ try {
+ // tslint:disable-next-line:no-require-imports
+ optTsickle = require('tsickle');
+ } catch (e) {
+ if (e.code !== 'MODULE_NOT_FOUND') {
+ throw e;
+ }
+ throw new Error(
+ 'When setting bazelOpts { tsickle: true }, ' +
+ 'you must also add a devDependency on the tsickle npm package');
+ }
+ perfTrace.wrap('emit', () => {
+ for (const sf of compilationTargets) {
+ perfTrace.wrap(`emit ${sf.fileName}`, () => {
+ emitResults.push(optTsickle.emitWithTsickle(
+ program, compilerHost, compilerHost, options, sf));
+ });
+ }
+ });
+ const emitResult = optTsickle.mergeEmitResults(emitResults);
+ diagnostics.push(...emitResult.diagnostics);
+
+ // If tsickle reported diagnostics, don't produce externs or manifest outputs.
+ if (diagnostics.length > 0) {
+ return diagnostics;
+ }
+
+ let externs = '/** @externs */\n' +
+ '// generating externs was disabled using generate_externs=False\n';
+ if (bazelOpts.tsickleGenerateExterns) {
+ externs =
+ optTsickle.getGeneratedExterns(emitResult.externs, options.rootDir!);
+ }
+
+ if (bazelOpts.tsickleExternsPath) {
+ // Note: when tsickleExternsPath is provided, we always write a file as a
+ // marker that compilation succeeded, even if it's empty (just containing an
+ // @externs).
+ fs.writeFileSync(bazelOpts.tsickleExternsPath, externs);
+
+ // When generating externs, generate an externs file for each of the input
+ // .d.ts files.
+ if (bazelOpts.tsickleGenerateExterns &&
+ compilerHost.provideExternalModuleDtsNamespace) {
+ for (const extern of compilationTargets) {
+ if (!extern.isDeclarationFile) continue;
+ const outputBaseDir = options.outDir!;
+ const relativeOutputPath =
+ compilerHost.relativeOutputPath(extern.fileName);
+ mkdirp(outputBaseDir, path.dirname(relativeOutputPath));
+ const outputPath = path.join(outputBaseDir, relativeOutputPath);
+ const moduleName = compilerHost.pathToModuleName('', extern.fileName);
+ fs.writeFileSync(
+ outputPath,
+ `goog.module('${moduleName}');\n` +
+ `// Export an empty object of unknown type to allow imports.\n` +
+ `// TODO: use typeof once available\n` +
+ `exports = /** @type {?} */ ({});\n`);
+ }
+ }
+ }
+
+ if (bazelOpts.manifest) {
+ perfTrace.wrap('manifest', () => {
+ const manifest =
+ constructManifest(emitResult.modulesManifest, compilerHost);
+ fs.writeFileSync(bazelOpts.manifest, manifest);
+ });
+ }
+
+ return diagnostics;
+}
+
+/**
+ * Creates directories subdir (a slash separated relative path) starting from
+ * base.
+ */
+function mkdirp(base: string, subdir: string) {
+ const steps = subdir.split(path.sep);
+ let current = base;
+ for (let i = 0; i < steps.length; i++) {
+ current = path.join(current, steps[i]);
+ if (!fs.existsSync(current)) fs.mkdirSync(current);
+ }
+}
+
if (require.main === module) {
// Do not call process.exit(), as that terminates the binary before
diff --git a/internal/tsc_wrapped/tsc_wrapped_test.ts b/internal/tsc_wrapped/tsc_wrapped_test.ts
new file mode 100644
index 0000000..cc5af0a
--- /dev/null
+++ b/internal/tsc_wrapped/tsc_wrapped_test.ts
@@ -0,0 +1,287 @@
+import * as path from 'path';
+import {ModulesManifest} from 'tsickle';
+import * as ts from 'typescript';
+
+import {UncachedFileLoader} from './cache';
+import {CompilerHost} from './compiler_host';
+import {constructManifest} from './manifest';
+import {writeTempFile} from './test_support';
+import {BazelOptions} from './tsconfig';
+
+// tslint:disable-next-line:no-any mock for testing.
+const throwingCompilerHostFake: ts.CompilerHost = null as any;
+
+const testFileLoader = new UncachedFileLoader();
+
+const relativeOutputPath = (f: string) => f;
+
+type ModuleResolver =
+ (moduleName: string, containingFile: string,
+ compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost) =>
+ ts.ResolvedModuleWithFailedLookupLocations;
+
+describe('tsc wrapped', () => {
+ function f(
+ res: ModulesManifest, fname: string, module: string, deps: string[]) {
+ res.addModule(fname, module);
+ for (let i = 0; i < deps.length; i++) {
+ res.addReferencedModule(fname, deps[i]);
+ }
+ return res;
+ }
+
+ it('produces a topo-sorted manifest', () => {
+ const res = new ModulesManifest();
+ f(res, 'src/f3.js', 'test$f3', ['test$f2', 'test$f1']);
+ f(res, 'src/f2.js', 'test$f2',
+ ['external$ts_source_not_included', 'test$f1']);
+ f(res, 'src/f1.js', 'test$f1', []);
+ expect(constructManifest(res, {relativeOutputPath})).toBe([
+ 'src/f1.js\n', 'src/f2.js\n', 'src/f3.js\n'
+ ].join(''));
+ });
+
+ it('reports cyclical dependencies', () => {
+ const res = new ModulesManifest();
+ f(res, 'src/f2', 'src$f2', ['src$f3']);
+ f(res, 'src/f3', 'src$f3', ['src$f1']);
+ f(res, 'src/f1', 'src$f1', ['src$f2']);
+ expect(() => constructManifest(res, {relativeOutputPath}))
+ .toThrowError(/src\/f2 -> src\/f3 -> src\/f1 -> src\/f2/g);
+ });
+
+ it('toposorts diamonds', () => {
+ // t
+ // l r
+ // b
+ const res = new ModulesManifest();
+ f(res, 'bottom.js', 'bottom', ['left', 'right']);
+ f(res, 'right.js', 'right', ['top']);
+ f(res, 'left.js', 'left', ['top']);
+ f(res, 'top.js', 'top', []);
+ expect(constructManifest(res, {relativeOutputPath})).toBe([
+ 'top.js\n',
+ 'left.js\n',
+ 'right.js\n',
+ 'bottom.js\n',
+ ].join(''));
+ });
+
+});
+
+// Create something that looks like CompilerOptions.
+const COMPILER_OPTIONS: ts.CompilerOptions = {
+ rootDirs: [
+ // Sorted by inverse length, as done by tsconfig.ts in production.
+ '/root/google3/blaze-out/k8-fastbuild/genfiles',
+ '/root/google3/blaze-out/k8-fastbuild/bin',
+ '/root/google3',
+ ],
+ outDir: '/root/google3/blaze-out/k8-fastbuild/bin',
+ rootDir: '/root/google3'
+};
+
+
+const defaultBazelOpts = {
+ googmodule: true,
+ workspaceName: 'google3',
+ prelude:
+ `goog.require('google3.third_party.javascript.tslib.tslib_closure');`,
+} as BazelOptions;
+
+describe('compiler host', () => {
+ const bazelOpts = {
+ ...defaultBazelOpts,
+ es5Mode: false,
+ } as BazelOptions;
+
+ it('looks up files', () => {
+ const fn = writeTempFile('file_lookup', 'let x: number = 123;');
+ const fn2 = writeTempFile('file_lookup2', 'let x: number = 124;');
+ const host = new CompilerHost(
+ [fn /* but not fn2! */], COMPILER_OPTIONS, bazelOpts,
+ throwingCompilerHostFake, testFileLoader);
+ expect(host.fileExists(fn)).toBe(true);
+ expect(host.fileExists(fn2)).toBe(false);
+ });
+
+ describe('file writing', () => {
+ let writtenFiles: {[key: string]: string};
+ beforeEach(() => writtenFiles = {});
+
+ const delegateHost = {
+ writeFile: (fn: string, d: string) => {
+ writtenFiles[fn.replace(/\\/g, '/')] = d;
+ }
+ // tslint:disable-next-line:no-any mock for testing.
+ } as any;
+
+ function createFakeModuleResolver(
+ moduleRoots: {[moduleName: string]: string}): ModuleResolver {
+ return (moduleName: string, containingFile: string,
+ compilerOptions: ts.CompilerOptions,
+ host: ts.ModuleResolutionHost) => {
+ if (moduleName[0] === '.') {
+ moduleName =
+ path.posix.join(path.dirname(containingFile), moduleName);
+ }
+ for (const moduleRoot in moduleRoots) {
+ if (moduleName.indexOf(moduleRoot) === 0) {
+ const resolvedFileName = moduleRoots[moduleRoot] +
+ moduleName.substring(moduleRoot.length) + '.d.ts';
+ return {
+ resolvedModule: {resolvedFileName, extension: ts.Extension.Dts},
+ failedLookupLocations: []
+ };
+ }
+ }
+
+ return {
+ resolvedModule:
+ {resolvedFileName: moduleName, extension: ts.Extension.Dts},
+ failedLookupLocations: []
+ };
+ };
+ }
+
+ function createFakeGoogle3Host({
+ es5 = false,
+ moduleRoots = {} as {[moduleName: string]: string},
+ isJsTranspilation = false,
+ transpiledJsOutputFileName = undefined as string | undefined,
+ } = {}) {
+ const bazelOpts = {
+ ...defaultBazelOpts,
+ es5Mode: es5,
+ isJsTranspilation,
+ transpiledJsOutputFileName,
+ } as BazelOptions;
+ return new CompilerHost(
+ [], COMPILER_OPTIONS, bazelOpts, delegateHost, testFileLoader,
+ createFakeModuleResolver(moduleRoots));
+ }
+
+ describe('converts path to module names', () => {
+ let host: CompilerHost;
+ beforeEach(() => {
+ host = createFakeGoogle3Host({
+ moduleRoots: {
+ 'module': 'path/to/module',
+ 'module2': 'path/to/module2',
+ 'path/to/module2': 'path/to/module2',
+ },
+ });
+ });
+
+ function expectPath(context: string, path: string) {
+ return expect(host.pathToModuleName(context, path));
+ }
+
+ it('mangles absolute paths', () => {
+ expectPath('whatever/context', 'some/absolute/module')
+ .toBe('google3.some.absolute.module');
+ });
+
+ it('escapes special symbols', () => {
+ expectPath('', 'some|123').toBe('google3.some$7c123');
+ expectPath('', '1some|').toBe('google3.1some$7c');
+ expectPath('', 'bar/foo.bam.ts').toBe('google3.bar.foo$2ebam');
+ // Underscore is unmodified, because it is common in google3 paths.
+ expectPath('', 'foo_bar').toBe('google3.foo_bar');
+ });
+
+ it('resolves paths', () => {
+ const context = 'path/to/module';
+
+ expectPath(context, './module2').toBe('google3.path.to.module2');
+ expectPath(context, '././module2').toBe('google3.path.to.module2');
+ expectPath(context, '../to/module2').toBe('google3.path.to.module2');
+ expectPath(context, '../to/.././to/module2')
+ .toBe('google3.path.to.module2');
+ });
+
+ it('ignores extra google3 sections in paths', () => {
+ expectPath('', 'google3/foo/bar').toBe('google3.foo.bar');
+ });
+
+ it('resolves absolute paths', () => {
+ const context = 'path/to/module/dir/file';
+
+ expectPath(context, '/root/google3/some/file.ts')
+ .toBe('google3.some.file');
+ expectPath(context, '/root/google3/path/to/some/file')
+ .toBe('google3.path.to.some.file');
+ expectPath(
+ context, '/root/google3/blaze-out/k8-fastbuild/bin/some/file')
+ .toBe('google3.some.file');
+ expectPath(
+ context, '/root/google3/blaze-out/k8-fastbuild/genfiles/some/file')
+ .toBe('google3.some.file');
+ });
+
+ describe('uses module name for resolved file paths', () => {
+ it('for goog.module module names', () => {
+ expectPath('', 'module/dir/file2')
+ .toBe('google3.path.to.module.dir.file2');
+ expectPath('', 'module2/dir/file2')
+ .toBe('google3.path.to.module2.dir.file2');
+ });
+
+ it('for imports of files from the same module', () => {
+ const context = 'path/to/module/dir/file';
+
+ expectPath(context, 'module/dir/file2')
+ .toBe('google3.path.to.module.dir.file2');
+ expectPath(context, './foo/bar')
+ .toBe('google3.path.to.module.dir.foo.bar');
+ expectPath(context, '../foo/bar')
+ .toBe('google3.path.to.module.foo.bar');
+ expectPath(context, 'path/to/module/dir/file2')
+ .toBe('google3.path.to.module.dir.file2');
+ });
+
+ it('for imports of files from a different module', () => {
+ const context = 'path/to/module/dir/file';
+
+ expectPath(context, 'module2/dir/file')
+ .toBe('google3.path.to.module2.dir.file');
+ expectPath(context, '../../module2/dir/file')
+ .toBe('google3.path.to.module2.dir.file');
+ expectPath(context, 'path/to/module2/dir/file')
+ .toBe('google3.path.to.module2.dir.file');
+ });
+ });
+ });
+
+ describe('output files', () => {
+ it('writes to .closure.js in ES6 mode', () => {
+ createFakeGoogle3Host({
+ es5: false,
+ }).writeFile('a.js', 'some.code();', false, undefined, []);
+ expect(Object.keys(writtenFiles)).toEqual([
+ '/root/google3/blaze-out/k8-fastbuild/bin/a.closure.js'
+ ]);
+ });
+
+ it('writes to .js in ES5 mode', () => {
+ createFakeGoogle3Host({
+ es5: true,
+ }).writeFile('a/b.js', 'some.code();', false, undefined, []);
+ expect(Object.keys(writtenFiles)).toEqual([
+ '/root/google3/blaze-out/k8-fastbuild/bin/a/b.js'
+ ]);
+ });
+
+ it('writes to closureOptions.transpiledJsOutputFileName in JS transpilation mode',
+ () => {
+ createFakeGoogle3Host({
+ isJsTranspilation: true,
+ transpiledJsOutputFileName: 'foo/bar/a/b.dev_es5.js',
+ }).writeFile('a/b.js', 'some.code();', false, undefined, []);
+ expect(Object.keys(writtenFiles)).toEqual([
+ '/root/google3/blaze-out/k8-fastbuild/bin/foo/bar/a/b.dev_es5.js'
+ ]);
+ });
+ });
+ });
+});
diff --git a/package.json b/package.json
index e4323aa..641b98f 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,7 @@
"http-server": "^0.11.1",
"protractor": "^5.2.0",
"shelljs": "^0.8.2",
- "tsickle": "0.32.1",
+ "tsickle": "0.33.1",
"typescript": "~2.9.1",
"which": "~1.0.5"
},
diff --git a/yarn.lock b/yarn.lock
index b3206b4..8d09cb4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1045,11 +1045,6 @@
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=
-diff@^3.2.0:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
- integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
-
dom-serialize@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
@@ -2153,13 +2148,6 @@
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e"
integrity sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=
-jasmine-diff@^0.1.3:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/jasmine-diff/-/jasmine-diff-0.1.3.tgz#93ccc2dcc41028c5ddd4606558074839f2deeaa8"
- integrity sha1-k8zC3MQQKMXd1GBlWAdIOfLe6qg=
- dependencies:
- diff "^3.2.0"
-
jasmine@^2.5.3:
version "2.8.0"
resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e"
@@ -3756,6 +3744,11 @@
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+source-map@^0.7.3:
+ version "0.7.3"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+ integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+
spawn-command@^0.0.2-1:
version "0.0.2"
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e"
@@ -4020,16 +4013,14 @@
source-map "^0.6.0"
source-map-support "^0.5.0"
-tsickle@0.32.1:
- version "0.32.1"
- resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.32.1.tgz#f16e94ba80b32fc9ebe320dc94fbc2ca7f3521a5"
- integrity sha512-JW9j+W0SaMSZGejIFZBk0AiPfnhljK3oLx5SaqxrJhjlvzFyPml5zqG1/PuScUj6yTe1muEqwk5CnDK0cOZmKw==
+tsickle@0.33.1:
+ version "0.33.1"
+ resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.33.1.tgz#eee4ebabeda3bcd8afc32cee34c822cbe3e839ec"
+ integrity sha512-SpW2G3PvDGs4a5sMXPlWnCWHWRviWjSlI3U0734e3fU3U39VAE0NPr8M3W1cuL/OU/YXheYipGeEwtIJ5k0NHQ==
dependencies:
- jasmine-diff "^0.1.3"
minimist "^1.2.0"
mkdirp "^0.5.1"
- source-map "^0.6.0"
- source-map-support "^0.5.0"
+ source-map "^0.7.3"
tslib@^1.8.1:
version "1.9.0"