From 0e9b2a21cb02fdf7c8f0a3f12424d7a918659a45 Mon Sep 17 00:00:00 2001 From: Matt Mackay Date: Tue, 7 Apr 2020 17:21:15 -0400 Subject: [PATCH] feat: support tsconfig path mappings (#33) --- .bzlgenrc | 2 +- package.json | 1 + src/BUILD | 1 + src/generators/generator.ts | 15 +++++- src/generators/sass/sass.generator.ts | 8 +--- src/generators/ts/ts.generator.flags.ts | 10 ++++ src/generators/ts/ts.generator.ts | 63 ++++++++++++++++++++++--- src/main.ts | 1 + src/workspace.ts | 14 +++--- test/generators/ts.generator.spec.ts | 38 ++++++++++++++- yarn.lock | 27 +++++++++++ 11 files changed, 156 insertions(+), 24 deletions(-) diff --git a/.bzlgenrc b/.bzlgenrc index d67ac40..2818228 100644 --- a/.bzlgenrc +++ b/.bzlgenrc @@ -5,4 +5,4 @@ --ng_module_bundle_load=//tools/rules_bazel/defs.bzl # label mappings ---label_mapping=rxjs/operators=@npm//rxjs +--label_mapping=rxjs/*=@npm//rxjs diff --git a/package.json b/package.json index 79419ce..2765122 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "shelljs": "0.8.3", "signale": "1.4.0", "typescript": "3.8.3", + "tsconfig-paths": "3.9.0", "yargs": "14.2.0" }, "devDependencies": { diff --git a/src/BUILD b/src/BUILD index e7a7be7..23ae005 100644 --- a/src/BUILD +++ b/src/BUILD @@ -10,6 +10,7 @@ RUNTIME_NPM_DEPS = [ "@npm//@phenomnomnominal/tsquery", "@npm//typescript", "@npm//minimatch", + "@npm//tsconfig-paths", ] ts_library( diff --git a/src/generators/generator.ts b/src/generators/generator.ts index a041211..5a1c879 100644 --- a/src/generators/generator.ts +++ b/src/generators/generator.ts @@ -1,6 +1,17 @@ -import { GeneratorType } from '../flags'; +import { Flags, GeneratorType } from '../flags'; +import { Workspace } from '../workspace'; +import { Buildozer } from '../buildozer'; export abstract class BuildFileGenerator { + private readonly flags: Flags; + + protected readonly buildozer: Buildozer; + + protected constructor(protected readonly workspace: Workspace) { + this.buildozer = workspace.getBuildozer(); + this.flags = workspace.getFlags(); + } + /** * Run any validation rules here on the current flags or workspace * If an error is thrown then the message is printed to the user and the generator exits with code 1, @@ -24,4 +35,4 @@ export abstract class BuildFileGenerator { * Return if this generator supports directories */ public abstract supportsDirectories(): boolean; -} \ No newline at end of file +} diff --git a/src/generators/sass/sass.generator.ts b/src/generators/sass/sass.generator.ts index 3c475c6..82ad10e 100644 --- a/src/generators/sass/sass.generator.ts +++ b/src/generators/sass/sass.generator.ts @@ -1,18 +1,14 @@ import * as gonzales from 'gonzales-pe'; import { ParsedPath } from 'path'; -import { Buildozer } from '../../buildozer'; import { GeneratorType } from '../../flags'; import { log } from '../../logger'; import { Workspace } from '../../workspace'; import { BuildFileGenerator } from '../generator'; export class SassGenerator extends BuildFileGenerator { - private readonly buildozer: Buildozer; - - constructor(private readonly workspace: Workspace) { - super(); - this.buildozer = workspace.getBuildozer(); + constructor(workspace: Workspace) { + super(workspace); } async generate(): Promise { diff --git a/src/generators/ts/ts.generator.flags.ts b/src/generators/ts/ts.generator.flags.ts index caaf4e8..3a348b3 100644 --- a/src/generators/ts/ts.generator.flags.ts +++ b/src/generators/ts/ts.generator.flags.ts @@ -10,6 +10,11 @@ export function setupGeneratorCommand(y) { description: 'The label used for any tsconfig attrs', requiresArg: true, group: 'TS Generator' + }).option('ts_config', { + type: 'string', + description: 'Path to a tsconfig.json file that is used to attempt to resolve path mappings', + requiresArg: true, + group: 'TS Generator' }); } @@ -23,4 +28,9 @@ export interface TsGeneratorFlags { * The label used for any tsconfig attrs */ ts_config_label: string; + + /** + * Path (relative to base_dir) to a tsconfig.json file that is used to attempt to resolve path mappings + */ + ts_config: string; } diff --git a/src/generators/ts/ts.generator.ts b/src/generators/ts/ts.generator.ts index 279c3f6..70d0db5 100644 --- a/src/generators/ts/ts.generator.ts +++ b/src/generators/ts/ts.generator.ts @@ -1,7 +1,17 @@ import { parse, posix } from 'path'; import { tsquery } from '@phenomnomnominal/tsquery'; -import { ExportDeclaration, Expression, ImportDeclaration, SourceFile } from 'typescript'; -import { Buildozer } from '../../buildozer'; +import { + ExportDeclaration, + Expression, + ImportDeclaration, + ParseConfigHost, + parseJsonSourceFileConfigFileContent, + readJsonConfigFile, + SourceFile, + sys +} from 'typescript'; +import { createMatchPath, MatchPath } from 'tsconfig-paths'; + import { GeneratorType } from '../../flags'; import { Label } from '../../label'; import { fatal, log } from '../../logger'; @@ -12,11 +22,12 @@ const IMPORTS_QUERY = `ImportDeclaration:has(StringLiteral)`; const EXPORTS_QUERY = `ExportDeclaration:has(StringLiteral)`; export class TsGenerator extends BuildFileGenerator { - protected readonly buildozer: Buildozer; + protected readonly tsPathsMatcher: MatchPath; + + constructor(workspace: Workspace) { + super(workspace); - constructor(protected readonly workspace: Workspace) { - super(); - this.buildozer = workspace.getBuildozer(); + this.tsPathsMatcher = this.createPathMatcherForTsPaths(); } async generate(): Promise { @@ -102,6 +113,9 @@ export class TsGenerator extends BuildFileGenerator { } private calculateTsDependencyLabel(imp: string, npmWorkspace: string): Label | undefined { + // see if there is a tsconfig.json, and if we need to adjust the import + imp = this.tryResolveFromTsPaths(imp); + let label = this.workspace.tryResolveLabelFromStaticMapping(imp, undefined, '.'); if (label) { return label; } @@ -130,4 +144,41 @@ export class TsGenerator extends BuildFileGenerator { } } + private tryResolveFromTsPaths(imp: string): string { + if (!this.tsPathsMatcher) { + return imp; + } + + const path = this.tsPathsMatcher(imp, undefined, undefined, ['.ts']); + return path ? path.replace(this.workspace.getFlags().base_dir + '/', '') : imp; + } + + private createPathMatcherForTsPaths(): MatchPath | undefined { + if (!this.workspace.getFlags().ts_config) { + return; + } + + const tsconfigPath = this.workspace.resolveRelativeToWorkspace(this.workspace.getFlags().ts_config); + const tsConfigSourceFile = readJsonConfigFile(tsconfigPath, path => this.workspace.readFile(path)); + + if (!tsConfigSourceFile) { + throw new Error(`--ts_config flag set, but failed to load tsconfig.json from ${tsconfigPath}`); + } + + const parseConfigHost: ParseConfigHost = { + fileExists: sys.fileExists, + readFile: path => this.workspace.readFile(path), + readDirectory: sys.readDirectory, + useCaseSensitiveFileNames: true + }; + + const parsed = parseJsonSourceFileConfigFileContent(tsConfigSourceFile, parseConfigHost, this.workspace.getPath()); + + // we _could_ throw parse errors here, but it may be not worth it if we can access the paths attr + if (!parsed.options.paths) { + return; + } + + return createMatchPath(this.workspace.getAbsolutePath(), parsed.options.paths); + } } diff --git a/src/main.ts b/src/main.ts index 07ba413..bdfdd34 100644 --- a/src/main.ts +++ b/src/main.ts @@ -72,6 +72,7 @@ export async function run() { } const isValid = generator.validate(); + if (isValid) { await wrap('generate', async () => await generator.generate()); diff --git a/src/workspace.ts b/src/workspace.ts index 9c199cb..5029769 100644 --- a/src/workspace.ts +++ b/src/workspace.ts @@ -13,7 +13,7 @@ import { debug, fatal, isDebugEnabled, lb, log, warn } from './logger'; export class Workspace { private readonly buildozer: Buildozer; - private readonly staticLabels: Map; + private readonly staticLabels: Map; private readonly resolvedStaticLabelsCache: Map = new Map(); private readonly fileQueryResultCache: Map = new Map(); @@ -23,8 +23,8 @@ export class Workspace { constructor(private readonly flags: Flags) { this.buildozer = new Buildozer(this.flags.load_mapping); - const regexLabels: Array<[RegExp, string]> = Array.from(this.flags.label_mapping.entries()) - .map(pair => [minimatch.makeRe(pair[0]), pair[1]]); + const regexLabels: Array<[string, RegExp]> = Array.from(this.flags.label_mapping.entries()) + .map(pair => [pair[1], minimatch.makeRe(pair[0])]); this.staticLabels = new Map(regexLabels); @@ -313,17 +313,17 @@ export class Workspace { return this.resolvedStaticLabelsCache.get(imp); } - // find returns the first found truthy match, so there _may_ be a more speciffic glob in the map + // find returns the first found truthy match, so there _may_ be a more specific glob in the map // but we won't find it - room for improvement, but the consumer can move them up the list const result = Array.from(this.staticLabels.entries()) - .find(([key, _]) => !!key.exec(imp)); + .find(([_, value]) => !!value.exec(imp)); if (!result) { return defaultValue ? Label.parseAbsolute(defaultValue) : undefined; } - const staticMaped = result[1]; - const label = Label.parseAbsolute(staticMaped); + const staticMapped = result[0]; + const label = Label.parseAbsolute(staticMapped); this.resolvedStaticLabelsCache.set(imp, label); diff --git a/test/generators/ts.generator.spec.ts b/test/generators/ts.generator.spec.ts index c993ba9..5549ae5 100644 --- a/test/generators/ts.generator.spec.ts +++ b/test/generators/ts.generator.spec.ts @@ -3,6 +3,7 @@ import * as mockfs from 'mock-fs'; import { setupAndParseArgs } from '../../src/flags'; import { TsGenerator } from '../../src/generators/ts/ts.generator'; import { Workspace } from '../../src/workspace'; +import { TsGeneratorFlags } from '../../src/generators/ts/ts.generator.flags'; describe('ng generator', () => { const TS_ONE = @@ -36,9 +37,10 @@ export class Some {} workspace = new Workspace(setupAndParseArgs(argv, true, 0)); gen = new TsGenerator(workspace); - }) + }); afterEach(() => mockfs.restore()); + it('can generate ts_library with deps', async () => { mockfs({ '/home/workspace/src/some': { @@ -67,6 +69,38 @@ export class Some {} expect(commands.join('\n')).toEqual(expected); }); + it('can use tsconfig paths', async () => { + mockfs({ + '/home/workspace': { + src: { + some: { + nested: { + 'main.ts': '' + }, + 'one.ts': `import { BAR } from '@foo/main'`, + 'tsconfig.json': `{"compilerOptions":{"paths":{"@foo/*":["nested/*"]}}}` + } + } + }, + }); + + (workspace.getFlags() as TsGeneratorFlags).ts_config = 'tsconfig.json'; + gen = new TsGenerator(workspace); + + await gen.generate(); + + const commands = workspace.getBuildozer().toCommands(); + + const expected = + 'new_load @npm_bazel_typescript//:index.bzl ts_library|//src/some:__pkg__\n' + + 'new ts_library some|//src/some:__pkg__\n' + + 'add srcs one.ts|//src/some:some\n' + + 'add deps //src/some/nested:main|//src/some:some\n' + + 'set tsconfig "//:tsconfig"|//src/some:some'; + + expect(commands.join('\n')).toEqual(expected); + }); + it('can strip all deep imports', async () => { mockfs({ '/home/workspace/src/some': { @@ -82,7 +116,7 @@ export class Some {} new ts_library some|//src/some:__pkg__ add srcs one.ts|//src/some:some add deps @npm//package:package @npm//@scope/package:package|//src/some:some -set tsconfig "//:tsconfig"|//src/some:some` +set tsconfig "//:tsconfig"|//src/some:some`; expect(commands.join('\n')).toEqual(expected); }); diff --git a/yarn.lock b/yarn.lock index 0634694..cd29ab7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -106,6 +106,11 @@ resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.0.tgz#2ad2006c8a937d20df20a8fee86071d0f730ef99" integrity sha512-kGCRI9oiCxFS6soGKlyzhMzDydfcPix9PpTkr7h11huxOxhWwP37Tg7DYBaQ18eQTNreZEuLkhpbGSqVNZPnnw== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/long@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" @@ -529,6 +534,13 @@ json-parse-better-errors@^1.0.1: resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + lcid@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" @@ -618,6 +630,11 @@ minimist@1.1.x: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" integrity sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag= +minimist@^1.2.0: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" @@ -1076,6 +1093,16 @@ test-exclude@^5.2.2: read-pkg-up "^4.0.0" require-main-filename "^2.0.0" +tsconfig-paths@3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + tslib@^1.8.1: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"