diff --git a/packages/tscc/src/graph/TypescriptDependencyGraph.ts b/packages/tscc/src/graph/TypescriptDependencyGraph.ts new file mode 100755 index 0000000..06786a3 --- /dev/null +++ b/packages/tscc/src/graph/TypescriptDependencyGraph.ts @@ -0,0 +1,123 @@ +/** + * @fileoverview Starting from a provided set of files, it walks Typescript SourceFiles that are + * referenced from previous SourceFiles. + * + * This information is provided to tsickleHost so that only such referenced files are processed by + * tsickle. This is mainly concerned with what files to use to generate externs. Why not just feed + * every `.d.ts` file to generate externs? Currently Typescript's type inclusion often includes "too + * many files" -- If tsconfig.json does not specify `types` compiler option, it will include every + * type declarations in `./node_modules/@types`, `../node_modules/@types`, + * `../../node_modules/@types`. Such a behavior is actually OK for usual TS usecase, because types + * anyway do not affect the Typescript transpilation output. However, in our setup they are all used + * to generate externs, and the more the type declarations, the more it takes to compile and the + * more it is prone to errors. + * + * An easy way(for me) would be to require users to provide every such package's name. But sometimes + * a package(A) may implicitly refers to another package(B)'s type declarations, and that package B + * also needs to be provided to tsickle, so this way requires users to __know__ what other packages + * this package A refers to, which requires users to inspect its contents, and this is not + * ergonomic. + * + * At the other extreme, we can include every .d.ts that typescript "sees". This will lead to the + * most correct behavior in some sense, because this is something you see in your IDE. But this may + * potentially lead to enormous amount of externs file and slow down the compilation as it will + * include everything in `node_modules/@types` directory unless you use types[root] compiler option. + * This may also cause more bugs coming from incompatibility between typescript and the closure + * side. + * + * Therefore, an intermediate approach is taken here. We use the same module resolution logic to + * find out which files were explicitly referenced by user-provided file. This requires discovering + * files that are either (1) imported (2) triple-slash-path-referenced (3) + * triple-slash-types-referenced. However, some declaration files that augments the global scope may + * not be discoverable in this way, so we add external modules provided in spec file and any module + * that is indicated in `compilerOptions.types` tsconfig key to this. + * + * There are some work going on from TS's side in a similar vein. + * {@link https://github.com/microsoft/TypeScript/issues/40124} + * + * Currently, this is done using an unexposed API of Typescript. I'm not sure why this is unexposed + * -- there are APIs such as `getResolvedModuleFileName/setResolvedModuleFileName`, but not + * something to iterate over resolved module file names. + */ +import * as ts from 'typescript'; +import {getPackageBoundary} from '../tsickle_patches/patch_tsickle_module_resolver'; +import path = require('path'); + +interface SourceFileWithInternalAPIs extends ts.SourceFile { + resolvedModules?: Map; + resolvedTypeReferenceDirectiveNames: Map +} + +export default class TypescriptDependencyGraph { + constructor( + private host: ts.ScriptReferenceHost + ) {} + private visited: Set = new Set(); + private defaultLibDir = path.dirname(ts.getDefaultLibFilePath(this.host.getCompilerOptions())); + + private isDefaultLib(fileName:string) { + return fileName.startsWith(this.defaultLibDir); + } + private isTslib(fileName:string) { + return getPackageBoundary(fileName).endsWith(path.sep + 'tslib' + path.sep); + } + private walk(fileName: string) { + if (typeof fileName !== 'string') return; + + // Default libraries (lib.*.d.ts) files and tslib.d.ts are not processed by tsickle. + if (this.isDefaultLib(fileName)) return; + if (this.isTslib(fileName)) return; + + // add file to visited + if (this.visited.has(fileName)) return; + this.visited.add(fileName); + + const sf = this.host.getSourceFile(fileName); + + /** + * Files imported to the current file are available in `resolvedModules` property. + * See: Microsoft/Typescript/src/compiler/programs.ts `ts.createProgram > processImportedModules` + * function. It calls `setResolvedModule` function for all external module references --> + * This is the (only, presumably) place where all the external module references are available. + */ + if (sf.resolvedModules) { + for (let entry of sf.resolvedModules) { + this.walk(entry?.[1]?.resolvedFileName); + } + } + /** + * Files referenced from the current file via /// are available in + * `referencedFiles` property. Unlike the previous `resolvedModules`, this is a public API. + * See: Microsoft/Typescript/src/compiler/programs.ts `ts.createProgram > processReferencedFiles` + * These are always initialized, so no if check is needed: see ts.Parser.parseSourceFile + */ + for (let ref of sf.referencedFiles) { + // Unlike the above API, this is not a resolved path, so we have to call TS API + // to resolve it first. See the function body of `processReferencedFiles`. + const resolvedReferencedFileName = ts.resolveTripleslashReference(ref?.fileName, fileName); + this.walk(resolvedReferencedFileName); + } + /** + * Files referenced from the current file via /// are available in + * `resolvedTypeReferenceDirectiveNames` internal API. This is also available in `typeReferencedFile`, + * but it does not contain information about the file path a type reference is resolved. + * See: Microsoft/Typescript/src/compiler/programs.ts `ts.createProgram > processTypeReferenceDirectives` + * see how this function calls `setResolvedTypeReferenceDirective` to mutate `sf.resolvedTypeRefernceDirectiveNames`. + */ + if (sf.resolvedTypeReferenceDirectiveNames) { + for (let entry of sf.resolvedTypeReferenceDirectiveNames) { + this.walk(entry?.[1]?.resolvedFileName); + } + } + } + addRootFile(fileName:string) { + this.walk(fileName); + } + hasFile(fileName:string) { + return this.visited.has(fileName); + } + // Currently this is only used in tests. + iterateFiles() { + return this.visited.values(); + } +} diff --git a/packages/tscc/src/shared/array_utils.ts b/packages/tscc/src/shared/array_utils.ts index 3bd7918..dabc85a 100644 --- a/packages/tscc/src/shared/array_utils.ts +++ b/packages/tscc/src/shared/array_utils.ts @@ -14,3 +14,12 @@ export function flatten(array: T[][]): T[] { return out; } +export function union(array1: T[], array2: T[]): T[] { + let out: T[] = array1.slice(); + for (let i = 0, l = array2.length; i < l; i++) { + let el = array2[i]; + if (out.includes(el)) continue; + out.push(el); + } + return out; +} diff --git a/packages/tscc/src/shared/escape_goog_identifier.ts b/packages/tscc/src/shared/escape_goog_identifier.ts new file mode 100755 index 0000000..ee17e22 --- /dev/null +++ b/packages/tscc/src/shared/escape_goog_identifier.ts @@ -0,0 +1,112 @@ +/** + * @fileoverview A valid goog.module name must start with [a-zA-Z_$] end only contain [a-zA-Z0-9._$]. + * This file provides an analogue of Javascript escape/unescape function pair for string identifiers + * for goog.module, goog.provide, etc. + * One does not lose information after escaping so that we can faithfully map converted module names + * to the original TS source file's name. + */ +import path = require('path'); + +function codePoint(char: string) {return char.codePointAt(0);} + +const LOWERCASE_A_CODE_POINT = codePoint('a'); +const LOWERCASE_Z_CODE_POINT = codePoint('z'); +const UPPERCASE_A_CODE_POINT = codePoint('A'); +const UPPERCASE_Z_CODE_POINT = codePoint('Z'); + +const PERIOD_CODE_POINT = codePoint('.'); +const LOWER_DASH_CODE_POINT = codePoint('_'); +const DOLLAR_SIGN_CODE_POINT = codePoint('$'); + +const ZERO_CODE_POINT = codePoint('0'); +const NINE_CODE_POINT = codePoint('9'); + +const SEP = path.sep + +function isLatin(code: number) { + return (LOWERCASE_A_CODE_POINT <= code && code <= LOWERCASE_Z_CODE_POINT) + || (UPPERCASE_A_CODE_POINT <= code && code <= UPPERCASE_Z_CODE_POINT); +} +function isNumber(code: number) { + return ZERO_CODE_POINT <= code && code <= NINE_CODE_POINT; +} +function isLowerDash(code: number) { + return code === LOWER_DASH_CODE_POINT; +} +function isPeriod(code: number) { + return code === PERIOD_CODE_POINT; +} +function isDollarSign(code: number) { + return code === DOLLAR_SIGN_CODE_POINT; +} + +/** + * Latin ⟹ Latin + * number ⟹ number + * "_" ⟹ "_" + * path separator ⟹ "." (for ergonomical reason) + * "." ⟹ "$." + * Any other character ⟹ "$" followed by length 4 base36 representation of its code point, + * left-padded with 0. + * + * This requires that the first character is not a path separator, in order to make sure that + * the resulting escaped name does not start with ".", which is disallowed in goog.module. One should + * always feed relative paths. + */ +export function escapeGoogAdmissibleName(name: string): string { + let out = ""; + if (name[0] === SEP) throw new TypeError("Name cannot start with a path separator"); + for (let char of name) { + let code = codePoint(char); + if (isLatin(code) || isNumber(code) || isLowerDash(code)) { + out += char; + } else if (char === SEP) { + out += "."; + } else if (isPeriod(code)) { + out += "$."; + } else { + out += "$" + code.toString(36).padStart(4, "0"); + } + } + return out; +} + +export function unescapeGoogAdmissibleName(escapedName: string): string { + let out = ""; + let i = 0; + let code: number; + // charCodeAt returns NaN when an index is out of range. + while (!isNaN(code = escapedName.charCodeAt(i))) { + if (isLatin(code) || isNumber(code) || isLowerDash(code)) { + out += escapedName[i]; + i++; + } else if (isPeriod(code)) { + out += SEP; + i++; + } else if (isDollarSign(code)) { + // If the next character is ".", add "." + if (isPeriod(escapedName.charCodeAt(i + 1))) { + out += "."; + i += 2; + } else { + // Read next 4 chars + try { + let base32Codes = parseInt(escapedName.substr(i + 1, 4), 36) + out += String.fromCodePoint(base32Codes); + i += 5; + } catch (e) { + console.log(escapedName); + throw new RangeError(`Invalid characters between position ${i + 1} and ${i + 4}`); + } + + } + } else { + throw new RangeError(`Invalid character at position ${i}`); + } + } + return out; +} + +export function escapedGoogNameIsDts(escapedName: string) { + return escapedName.endsWith("$.d$.ts"); +} diff --git a/packages/tscc/src/transformer/dts_requiretype_transformer.ts b/packages/tscc/src/transformer/dts_requiretype_transformer.ts new file mode 100755 index 0000000..a11ce81 --- /dev/null +++ b/packages/tscc/src/transformer/dts_requiretype_transformer.ts @@ -0,0 +1,104 @@ +/** + * @fileoverview Transforms `const tsickle_aaaa = goog.requireType(.....)` calls to external modules + * into const tsickle_aaaa = mangled$namespace$declared$in$externs. When certain external module's + * main type declaration file merely reexports some other file, + * + * (details to be tested, some other file or some other file in another module?) + * + * tsickle inserts such requireType statements referencing that file directly. + * + * Type declarations in such files are already declared in externs, so we can just alias that variable + * with a namespace on which the file's declarations are written. + * + * This code was mostly same as the one we've used to transform goog.require("a-external_module") + * before we've switched to gluing module method. + * + * Codes are copied from commit + * 1c9824461fcb71814466729b9c1424c4a60ef4ce (feat: use gluing modules for external module support) + * + * TODO: improve comment here and documentation. + */ + +import * as ts from 'typescript'; +import ITsccSpecWithTS from '../spec/ITsccSpecWithTS'; +import {TsickleHost} from 'tsickle'; +import {moduleNameAsIdentifier} from 'tsickle/src/annotator_host'; +import {namespaceToQualifiedName, isGoogRequireLikeStatement} from './transformer_utils'; +import {escapedGoogNameIsDts, unescapeGoogAdmissibleName} from '../shared/escape_goog_identifier'; + +/** + * This is a transformer run after ts transformation, before googmodule transformation. + * + * In order to wire imports of external modules to their global symbols, we replace + * top-level `require`s of external modules to an assignment of a local variable to + * a global symbol. This results in no `goog.require` or `goog.requireType` emit. + */ +export default function dtsRequireTypeTransformer(spec: ITsccSpecWithTS, tsickleHost: TsickleHost) + : (context: ts.TransformationContext) => ts.Transformer { + const externalModuleNames = spec.getExternalModuleNames(); + return (context: ts.TransformationContext): ts.Transformer => { + return (sf: ts.SourceFile): ts.SourceFile => { + function maybeExternalModuleRequireType( + original: ts.Statement, importedUrl: string, newIdent: ts.Identifier + ) { + const setOriginalNode = (range: ts.Statement) => { + return ts.setOriginalNode(ts.setTextRange(range, original), original); + } + // We are only interested in `requireType`ing .d.ts files. + if (!escapedGoogNameIsDts(importedUrl)) return null; + // If imported url is external module, no need to handle it further. + if (externalModuleNames.includes(importedUrl)) return null; + + // origUrl will be a file path relative to the ts project root. + let origUrl = unescapeGoogAdmissibleName(importedUrl); + + // We must figure out on what namespace the extern for this module is defined. + // See tsickle/src/externs.js for precise logic. In our case, goog.requireType(....d.ts) + // will be emitted for "module .d.ts", in which case a mangled name derived from a + // .d.ts file's path is used. See how `moduleNamespace`, `rootNamespace` is constructed + // in tsickle/src/externs.js. + // This relies on the heuristic of tsickle, so must be carefully validated whenever tsickle updates. + let mangledNamespace = moduleNameAsIdentifier(tsickleHost, origUrl); + + if (newIdent.escapedText === mangledNamespace) { + // Name of the introduced identifier coincides with the global identifier, + // no need to emit things. + return setOriginalNode(ts.createEmptyStatement()); + } + // Convert `const importedName = goog.requireType("module d.ts")` to: + // `const importedName = mangledNamespace;` + return setOriginalNode(ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList( + [ + ts.createVariableDeclaration( + newIdent, + undefined, + namespaceToQualifiedName(mangledNamespace) + ) + ], + tsickleHost.es5Mode ? undefined : ts.NodeFlags.Const) + )); + } + + function visitTopLevelStatement(statements: ts.Statement[], sf: ts.SourceFile, node: ts.Statement) { + lookupExternalModuleRequire: { + let _ = isGoogRequireLikeStatement(node, "requireType"); + if (!_) break lookupExternalModuleRequire; + + // Do Things TODO + let {importedUrl, newIdent} = _; + const require = maybeExternalModuleRequireType(node, importedUrl, newIdent); + if (!require) break lookupExternalModuleRequire; + statements.push(require); + return; + } + statements.push(node); + } + + const stmts: ts.Statement[] = []; + for (const stmt of sf.statements) visitTopLevelStatement(stmts, sf, stmt); + return ts.updateSourceFileNode(sf, ts.setTextRange(ts.createNodeArray(stmts), sf.statements)); + } + } +} diff --git a/packages/tscc/src/transformer/transformer_utils.ts b/packages/tscc/src/transformer/transformer_utils.ts index 2b64e54..ff80206 100644 --- a/packages/tscc/src/transformer/transformer_utils.ts +++ b/packages/tscc/src/transformer/transformer_utils.ts @@ -13,12 +13,17 @@ function extractRequire(call: ts.CallExpression): string | null { return getRequiredModuleName(call); } -function extractGoogRequire(call: ts.CallExpression): string | null { - // Verify that the call is a call of a form goog.require(...). +type TGoogRequireLike = "require" | "requireType"; + +/** + * Verify that the call is a call of a form goog.require(...). + * @param requireLike require, requireType, provides, ... + */ +function extractGoogRequireLike(call: ts.CallExpression, requireLike:TGoogRequireLike): string | null { let exp = call.expression; if (!ts.isPropertyAccessExpression(exp)) return null; if (!ts.isIdentifier(exp.expression) || exp.expression.escapedText !== 'goog') return null; - if (exp.name.escapedText !== 'require') return null; + if (exp.name.escapedText !== requireLike) return null; return getRequiredModuleName(call); } @@ -53,7 +58,7 @@ function isVariableRequireStatement(stmt: ts.Statement): IImportedVariable { return {importedUrl, newIdent: decl.name}; } -function isGoogRequireStatement(stmt: ts.Statement): IImportedVariable { +export function isGoogRequireLikeStatement(stmt: ts.Statement, requireLike:TGoogRequireLike): IImportedVariable { if (!ts.isVariableStatement(stmt)) return; // Verify it's a single decl (and not "var x = ..., y = ...;"). if (stmt.declarationList.declarations.length !== 1) return; @@ -64,7 +69,7 @@ function isGoogRequireStatement(stmt: ts.Statement): IImportedVariable { if (!decl.initializer || !ts.isCallExpression(decl.initializer)) { return; } - const importedUrl = extractGoogRequire(decl.initializer); + const importedUrl = extractGoogRequireLike(decl.initializer, requireLike); if (!importedUrl) return; return {importedUrl, newIdent: decl.name}; } @@ -80,7 +85,7 @@ export function findImportedVariable(sf: ts.SourceFile, moduleName: string): ts. export function findGoogRequiredVariable(sf: ts.SourceFile, moduleName: string): ts.Identifier { for (let stmt of sf.statements) { - let _ = isGoogRequireStatement(stmt); + let _ = isGoogRequireLikeStatement(stmt, "require"); if (!_) continue; if (_.importedUrl !== moduleName) continue; return _.newIdent; diff --git a/packages/tscc/src/tscc.ts b/packages/tscc/src/tscc.ts index 9959817..c35c6e9 100755 --- a/packages/tscc/src/tscc.ts +++ b/packages/tscc/src/tscc.ts @@ -5,20 +5,22 @@ import * as ts from "typescript"; import getDefaultLibs from './default_libs'; import {Cache, FSCacheAccessor} from './graph/Cache'; import ClosureDependencyGraph from './graph/ClosureDependencyGraph'; +import TypescriptDependencyGraph from './graph/TypescriptDependencyGraph'; import {ISourceNode} from './graph/ISourceNode'; import {sourceNodeFactory} from './graph/source_node_factory'; import Logger from './log/Logger'; import * as spinner from './log/spinner'; import {applyPatches, restorePatches} from './tsickle_patches/facade' -import {getPackageBoundary} from './tsickle_patches/patch_tsickle_module_resolver'; -import {riffle} from './shared/array_utils'; +import {riffle, union} from './shared/array_utils'; import PartialMap from './shared/PartialMap'; import {ClosureJsonToVinyl, IClosureCompilerInputJson, RemoveTempGlobalAssignments} from './shared/vinyl_utils'; +import {escapeGoogAdmissibleName} from './shared/escape_goog_identifier' import spawnCompiler from './spawn_compiler'; import ITsccSpecWithTS from "./spec/ITsccSpecWithTS"; import TsccSpecWithTS, {TsError} from "./spec/TsccSpecWithTS"; import decoratorPropertyTransformer from './transformer/decorator_property_transformer'; import restPropertyTransformer from './transformer/rest_property_transformer'; +import dtsRequireTypeTransformer from './transformer/dts_requiretype_transformer'; import {getExternsForExternalModules, getGluingModules} from './external_module_support'; import fs = require('fs'); import path = require('path'); @@ -29,6 +31,7 @@ import vfs = require('vinyl-fs'); import upath = require('upath'); import chalk = require('chalk'); + export const TEMP_DIR = ".tscc_temp"; /** @@ -77,7 +80,15 @@ export default async function tscc( const diagnostics = ts.getPreEmitDiagnostics(program); if (diagnostics.length) throw new TsError(diagnostics); - const transformerHost = getTsickleHost(tsccSpec, tsLogger); + const tsDepsGraph = new TypescriptDependencyGraph(program) + tsccSpec.getOrderedModuleSpecs().forEach(moduleSpec => tsDepsGraph.addRootFile(moduleSpec.entry)); + union(tsccSpec.getExternalModuleNames(), tsccSpec.getCompilerOptions().types ?? []) + .map(tsccSpec.resolveExternalModuleTypeReference, tsccSpec) + .map(tsDepsGraph.addRootFile, tsDepsGraph); + // If user explicitly provided `types` compiler option, it is more likely that its type is actually + // used in user code. + tsccLogger.log(JSON.stringify([...tsDepsGraph.iterateFiles()])) + const transformerHost = getTsickleHost(tsccSpec, tsDepsGraph, tsLogger); /** * Ideally, the dependency graph should be determined from ts sourceFiles, and the compiler @@ -129,6 +140,7 @@ export default async function tscc( applyPatches(); result = tsickle.emit(program, transformerHost, writeFile, undefined, undefined, false, { afterTs: [ + dtsRequireTypeTransformer(tsccSpec, transformerHost), decoratorPropertyTransformer(transformerHost), restPropertyTransformer(transformerHost) ] @@ -333,38 +345,23 @@ function pushTsickleOutputToStream( }); } -function getTsickleHost(tsccSpec: ITsccSpecWithTS, logger: Logger): tsickle.TsickleHost { +function getTsickleHost(tsccSpec: ITsccSpecWithTS, tsDependencyGraph:TypescriptDependencyGraph, logger: Logger): tsickle.TsickleHost { const options = tsccSpec.getCompilerOptions(); const compilerHost = tsccSpec.getCompilerHost(); - - // Non-absolute file names are resolved from the TS project root. - const fileNamesSet = tsccSpec.getAbsoluteFileNamesSet(); - const externalModuleNames = tsccSpec.getExternalModuleNames(); - const resolvedExternalModuleTypeRefs: string[] = []; - - for (let name of externalModuleNames) { - let typeRefFileName = tsccSpec.resolveExternalModuleTypeReference(name); - if (typeRefFileName) { - resolvedExternalModuleTypeRefs.push(typeRefFileName); - } - } - - const externalModuleRoots = resolvedExternalModuleTypeRefs - .map(getPackageBoundary); const ignoreWarningsPath = tsccSpec.debug().ignoreWarningsPath || ["/node_modules/"]; const transformerHost: tsickle.TsickleHost = { shouldSkipTsickleProcessing(fileName: string) { - const absFileName = path.resolve(fileName); - if (fileNamesSet.has(absFileName)) return false; - // .d.ts files in node_modules for external modules are needed to generate - // externs file. - if (externalModuleRoots.findIndex(root => absFileName.startsWith(root)) !== -1) { - return false; - } - return true; + // Non-absolute files are resolved relative to a typescript project root. + const absFileName = path.resolve(tsccSpec.getTSRoot(), fileName); + // Previously, we've processed all files that are in the same node_modules directory of type + // declaration file for external modules. The current behavior with including transitive + // dependencies only will have the same effect on such files, because `ts.createProgram` + // anyway adds only such files to the program. So this update will in effect include strictly + // larger set of files. + return !tsDependencyGraph.hasFile(absFileName); }, shouldIgnoreWarningsForPath(fileName: string) { return true; // Just a stub, maybe add configuration later. @@ -412,22 +409,9 @@ function getTsickleHost(tsccSpec: ITsccSpecWithTS, logger: Logger): tsickle.Tsic } // resolve relative to the ts project root. fileName = path.relative(tsccSpec.getTSRoot(), fileName); - return convertToGoogModuleAdmissibleName(fileName); + return escapeGoogAdmissibleName(fileName); } } return transformerHost; } - -/** - * A valid goog.module name must start with [a-zA-Z_$] end only contain [a-zA-Z0-9._$]. - * Maps path separators to ".", - */ -function convertToGoogModuleAdmissibleName(modulePath: string): string { - return modulePath - .replace(/\.[tj]sx?$/, '') //remove file extension - .replace(/[\/\\]/g, '.') - .replace(/^[^a-zA-Z_$]/, '_') - .replace(/[^a-zA-Z0-9._$]/g, '_'); -} - diff --git a/packages/tscc/test/e2e/sample/case_1/tsconfig.json b/packages/tscc/test/e2e/sample/case_1/tsconfig.json index 231027e..831c372 100644 --- a/packages/tscc/test/e2e/sample/case_1/tsconfig.json +++ b/packages/tscc/test/e2e/sample/case_1/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5" + "target": "es5", + "types": [] }, "include": [ "./*.ts" diff --git a/packages/tscc/test/e2e/sample/case_2_sourcemaps/tsconfig.json b/packages/tscc/test/e2e/sample/case_2_sourcemaps/tsconfig.json index bd501e5..bc771fd 100644 --- a/packages/tscc/test/e2e/sample/case_2_sourcemaps/tsconfig.json +++ b/packages/tscc/test/e2e/sample/case_2_sourcemaps/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { - "sourceMap": true + "sourceMap": true, + "types": [] } } diff --git a/packages/tscc/test/e2e/sample/case_3_sourcemaps_with_decorators/tsconfig.json b/packages/tscc/test/e2e/sample/case_3_sourcemaps_with_decorators/tsconfig.json index 40d84c4..faf9774 100644 --- a/packages/tscc/test/e2e/sample/case_3_sourcemaps_with_decorators/tsconfig.json +++ b/packages/tscc/test/e2e/sample/case_3_sourcemaps_with_decorators/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "experimentalDecorators": true, "target": "es2016", - "sourceMap": true + "sourceMap": true, + "types": [] } } diff --git a/packages/tscc/test/e2e/sample/case_4_external/tsconfig.json b/packages/tscc/test/e2e/sample/case_4_external/tsconfig.json index 231027e..831c372 100644 --- a/packages/tscc/test/e2e/sample/case_4_external/tsconfig.json +++ b/packages/tscc/test/e2e/sample/case_4_external/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5" + "target": "es5", + "types": [] }, "include": [ "./*.ts" diff --git a/packages/tscc/test/e2e/sample/case_5_object_spread/tsconfig.json b/packages/tscc/test/e2e/sample/case_5_object_spread/tsconfig.json index 78d7134..b1c6f65 100644 --- a/packages/tscc/test/e2e/sample/case_5_object_spread/tsconfig.json +++ b/packages/tscc/test/e2e/sample/case_5_object_spread/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "experimentalDecorators": true, - "target": "es2016" + "target": "es2016", + "types": [] } } diff --git a/packages/tscc/test/e2e/sample/case_6_type_only_references/tsconfig.json b/packages/tscc/test/e2e/sample/case_6_type_only_references/tsconfig.json index da62800..28565a4 100644 --- a/packages/tscc/test/e2e/sample/case_6_type_only_references/tsconfig.json +++ b/packages/tscc/test/e2e/sample/case_6_type_only_references/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { - "target": "es6" + "target": "es6", + "types": [] } } diff --git a/packages/tscc/test/e2e/sample/case_7_lodash_style_external_module_declaration/tsconfig.json b/packages/tscc/test/e2e/sample/case_7_lodash_style_external_module_declaration/tsconfig.json index 231027e..831c372 100755 --- a/packages/tscc/test/e2e/sample/case_7_lodash_style_external_module_declaration/tsconfig.json +++ b/packages/tscc/test/e2e/sample/case_7_lodash_style_external_module_declaration/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5" + "target": "es5", + "types": [] }, "include": [ "./*.ts" diff --git a/packages/tscc/test/e2e/sample/case_8_dts_require_type/imported.d.ts b/packages/tscc/test/e2e/sample/case_8_dts_require_type/imported.d.ts new file mode 100755 index 0000000..0364729 --- /dev/null +++ b/packages/tscc/test/e2e/sample/case_8_dts_require_type/imported.d.ts @@ -0,0 +1,2 @@ +import {a} from './transtively_imported'; +export function b():a; diff --git a/packages/tscc/test/e2e/sample/case_8_dts_require_type/module.ts b/packages/tscc/test/e2e/sample/case_8_dts_require_type/module.ts new file mode 100755 index 0000000..42e728d --- /dev/null +++ b/packages/tscc/test/e2e/sample/case_8_dts_require_type/module.ts @@ -0,0 +1,4 @@ +import * as i from './imported'; +import * as e from 'external'; +window["a"] = i.b(); +window["b"] = e.a; diff --git a/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external/package.json b/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external/package.json new file mode 100755 index 0000000..2d41f8a --- /dev/null +++ b/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external/package.json @@ -0,0 +1,3 @@ +{ + "types": "types.d.ts" +} diff --git a/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external/types.d.ts b/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external/types.d.ts new file mode 100755 index 0000000..55f2fcf --- /dev/null +++ b/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external/types.d.ts @@ -0,0 +1,5 @@ +import {external2} from 'external_reexported'; +export const a: external; +export interface external { + some_property: external2 +} diff --git a/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external_reexported/package.json b/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external_reexported/package.json new file mode 100755 index 0000000..2d41f8a --- /dev/null +++ b/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external_reexported/package.json @@ -0,0 +1,3 @@ +{ + "types": "types.d.ts" +} diff --git a/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external_reexported/types.d.ts b/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external_reexported/types.d.ts new file mode 100755 index 0000000..7b0312f --- /dev/null +++ b/packages/tscc/test/e2e/sample/case_8_dts_require_type/node_modules/external_reexported/types.d.ts @@ -0,0 +1,3 @@ +export interface external2 { + some_property:any; +} diff --git a/packages/tscc/test/e2e/sample/case_8_dts_require_type/transtively_imported.d.ts b/packages/tscc/test/e2e/sample/case_8_dts_require_type/transtively_imported.d.ts new file mode 100755 index 0000000..800beb5 --- /dev/null +++ b/packages/tscc/test/e2e/sample/case_8_dts_require_type/transtively_imported.d.ts @@ -0,0 +1,3 @@ +import {number} from "yargs" + +export type a = ()=>number; diff --git a/packages/tscc/test/e2e/sample/case_8_dts_require_type/tscc.spec.json b/packages/tscc/test/e2e/sample/case_8_dts_require_type/tscc.spec.json new file mode 100755 index 0000000..20fcbd5 --- /dev/null +++ b/packages/tscc/test/e2e/sample/case_8_dts_require_type/tscc.spec.json @@ -0,0 +1,10 @@ +{ + "modules": { + "module": "./module.ts" + }, + "external": { + "external": "external", + "./imported": "imported" + }, + "prefix": ".tscc_temp/case_8_dts_requiretype/" +} diff --git a/packages/tscc/test/e2e/sample/case_8_dts_require_type/tsconfig.json b/packages/tscc/test/e2e/sample/case_8_dts_require_type/tsconfig.json new file mode 100755 index 0000000..cde3131 --- /dev/null +++ b/packages/tscc/test/e2e/sample/case_8_dts_require_type/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "es5", + "moduleResolution": "node" + }, + "include": [ + "./*.ts" + ] +} diff --git a/packages/tscc/test/graph/TypescriptDependencyGraph.ts b/packages/tscc/test/graph/TypescriptDependencyGraph.ts new file mode 100755 index 0000000..bb569a2 --- /dev/null +++ b/packages/tscc/test/graph/TypescriptDependencyGraph.ts @@ -0,0 +1,42 @@ +/// +import TypescriptDependencyGraph from '../../src/graph/TypescriptDependencyGraph'; +import * as ts from 'typescript'; +import path = require('path'); + +describe(`TypescriptDependencyGraph`, () => { + it(`discovers transitive dependencies`, () => { + const testProjectRoot = path.resolve(__dirname, '../sample/tsdepsgraph'); + const program = createProgramFromConfigFile(path.join(testProjectRoot, 'tsconfig.json')); + const graph = new TypescriptDependencyGraph(program); + graph.addRootFile( + path.join(testProjectRoot, 'entry.ts') + ); + expect(new Set(graph.iterateFiles())).toEqual(new Set([ + "entry.ts", + "a.ts", + "node_modules/aa/types2.d.ts", + "ab.d.ts", + "node_modules/ac/ac.d.ts", + "b.d.ts", + "bb.d.ts", + "node_modules/bc/types3.d.ts", + "node_modules/c/types.d.ts", + "node_modules/c/cb.d.ts", + "node_modules/c/node_modules/cc/cc.d.ts" + ].map(relPath => path.normalize(path.join(testProjectRoot, relPath))))); + }); +}); + +function createProgramFromConfigFile(configFilePath: string): ts.Program { + const {fileNames, options} = ts.getParsedCommandLineOfConfigFile( + path.resolve(__dirname, configFilePath), + {}, + ts.sys + ); + const compilerHost = ts.createCompilerHost(options); + return ts.createProgram( + fileNames, + options, + compilerHost + ); +} diff --git a/packages/tscc/test/sample/tsdepsgraph/a.ts b/packages/tscc/test/sample/tsdepsgraph/a.ts new file mode 100755 index 0000000..ebaa70b --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/a.ts @@ -0,0 +1,4 @@ +/// +/// +export * from 'aa'; +export const a = 1; diff --git a/packages/tscc/test/sample/tsdepsgraph/ab.d.ts b/packages/tscc/test/sample/tsdepsgraph/ab.d.ts new file mode 100755 index 0000000..9342fa6 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/ab.d.ts @@ -0,0 +1 @@ +declare var ab:PromiseLike; diff --git a/packages/tscc/test/sample/tsdepsgraph/b.d.ts b/packages/tscc/test/sample/tsdepsgraph/b.d.ts new file mode 100755 index 0000000..4cef154 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/b.d.ts @@ -0,0 +1,3 @@ +/// +/// +declare const b:WeakSet; diff --git a/packages/tscc/test/sample/tsdepsgraph/bb.d.ts b/packages/tscc/test/sample/tsdepsgraph/bb.d.ts new file mode 100755 index 0000000..cc59b1b --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/bb.d.ts @@ -0,0 +1,3 @@ +declare namespace bb { + const $: Atomics; +} diff --git a/packages/tscc/test/sample/tsdepsgraph/entry.ts b/packages/tscc/test/sample/tsdepsgraph/entry.ts new file mode 100755 index 0000000..0773b01 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/entry.ts @@ -0,0 +1,14 @@ +/// +/// +import * as a from './a'; + +a.a; +a.aa; +ab; +ac; + +b; +bb.$; +bc; + +c; diff --git a/packages/tscc/test/sample/tsdepsgraph/node_modules/aa/package.json b/packages/tscc/test/sample/tsdepsgraph/node_modules/aa/package.json new file mode 100755 index 0000000..c4e3688 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/node_modules/aa/package.json @@ -0,0 +1,3 @@ +{ + "types": "types2.d.ts" +} diff --git a/packages/tscc/test/sample/tsdepsgraph/node_modules/aa/types2.d.ts b/packages/tscc/test/sample/tsdepsgraph/node_modules/aa/types2.d.ts new file mode 100755 index 0000000..cf5d381 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/node_modules/aa/types2.d.ts @@ -0,0 +1 @@ +export const aa:Uint16Array; diff --git a/packages/tscc/test/sample/tsdepsgraph/node_modules/ac/ac.d.ts b/packages/tscc/test/sample/tsdepsgraph/node_modules/ac/ac.d.ts new file mode 100755 index 0000000..02cab4a --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/node_modules/ac/ac.d.ts @@ -0,0 +1,5 @@ +declare global { + var ac:number; +} + +export {}; diff --git a/packages/tscc/test/sample/tsdepsgraph/node_modules/ac/package.json b/packages/tscc/test/sample/tsdepsgraph/node_modules/ac/package.json new file mode 100755 index 0000000..ded9486 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/node_modules/ac/package.json @@ -0,0 +1,3 @@ +{ + "types": "ac.d.ts" +} diff --git a/packages/tscc/test/sample/tsdepsgraph/node_modules/bc/package.json b/packages/tscc/test/sample/tsdepsgraph/node_modules/bc/package.json new file mode 100755 index 0000000..a23f599 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/node_modules/bc/package.json @@ -0,0 +1,3 @@ +{ + "types": "types3.d.ts" +} diff --git a/packages/tscc/test/sample/tsdepsgraph/node_modules/bc/types3.d.ts b/packages/tscc/test/sample/tsdepsgraph/node_modules/bc/types3.d.ts new file mode 100755 index 0000000..c107983 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/node_modules/bc/types3.d.ts @@ -0,0 +1 @@ +declare const bc: MediaDeviceInfo; diff --git a/packages/tscc/test/sample/tsdepsgraph/node_modules/c/cb.d.ts b/packages/tscc/test/sample/tsdepsgraph/node_modules/c/cb.d.ts new file mode 100755 index 0000000..508d9e4 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/node_modules/c/cb.d.ts @@ -0,0 +1,3 @@ +declare module "ca" { + export const ca:Set>>> +} diff --git a/packages/tscc/test/sample/tsdepsgraph/node_modules/c/node_modules/cc/cc.d.ts b/packages/tscc/test/sample/tsdepsgraph/node_modules/c/node_modules/cc/cc.d.ts new file mode 100755 index 0000000..976811d --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/node_modules/c/node_modules/cc/cc.d.ts @@ -0,0 +1,3 @@ +declare module "cd" { + export const cd:IDBCursorDirection; +} diff --git a/packages/tscc/test/sample/tsdepsgraph/node_modules/c/node_modules/cc/package.json b/packages/tscc/test/sample/tsdepsgraph/node_modules/c/node_modules/cc/package.json new file mode 100755 index 0000000..fa4c161 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/node_modules/c/node_modules/cc/package.json @@ -0,0 +1,3 @@ +{ + "types": "cc.d.ts" +} diff --git a/packages/tscc/test/sample/tsdepsgraph/node_modules/c/package.json b/packages/tscc/test/sample/tsdepsgraph/node_modules/c/package.json new file mode 100755 index 0000000..c7896cb --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/node_modules/c/package.json @@ -0,0 +1,3 @@ +{ + "types": "./types.d.ts" +} diff --git a/packages/tscc/test/sample/tsdepsgraph/node_modules/c/types.d.ts b/packages/tscc/test/sample/tsdepsgraph/node_modules/c/types.d.ts new file mode 100755 index 0000000..0ca5b37 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/node_modules/c/types.d.ts @@ -0,0 +1,7 @@ +/// +/// +export * as ca from 'ca'; +export * as cd from 'cd'; +declare global { + const c:string; +} diff --git a/packages/tscc/test/sample/tsdepsgraph/tsconfig.json b/packages/tscc/test/sample/tsdepsgraph/tsconfig.json new file mode 100755 index 0000000..dc139b7 --- /dev/null +++ b/packages/tscc/test/sample/tsdepsgraph/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "importHelpers": true + }, + "includes": [ + "entry.ts" + ] +} diff --git a/packages/tscc/test/transformer/transformers.ts b/packages/tscc/test/transformer/transformers.ts index b8a0dc2..53aa708 100755 --- a/packages/tscc/test/transformer/transformers.ts +++ b/packages/tscc/test/transformer/transformers.ts @@ -105,6 +105,10 @@ describe(`restPropertyTransformer`, function () { }) }); +describe(`dts_requiretype_transformer`, () => { + test.todo(`modifies requireType calls to a global variable`) +}); + type EmitTransformerFactory = (host: tsickle.TsickleHost) => tsickle.EmitTransformers; function emit(tsconfigPath: string, files: string[], transformerFactory: EmitTransformerFactory, override: Partial = {}) {