diff --git a/WORKSPACE b/WORKSPACE index b05e7156..cee64284 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -58,3 +58,10 @@ npm_translate_lock( load("@npm//:repositories.bzl", "npm_repositories") npm_repositories() + +load("@aspect_rules_jasmine//jasmine:repositories.bzl", "LATEST_VERSION", "rules_jasmine_repositories") + +rules_jasmine_repositories( + name = "jasmine", + jasmine_version = LATEST_VERSION, +) diff --git a/examples/package.json b/examples/package.json index ce8b597e..d044f17c 100644 --- a/examples/package.json +++ b/examples/package.json @@ -3,7 +3,10 @@ "dependencies": { "@babel/cli": "7.18.9", "@babel/core": "7.18.9", + "@babel/parser": "7.18.9", "@babel/preset-typescript": "7.18.6", + "@babel/types": "7.18.6", + "@types/jasmine": "*", "@types/node": "18.6.1", "date-fns": "2.29.1", "typescript": "4.8.2" diff --git a/examples/pnpm-lock.yaml b/examples/pnpm-lock.yaml index 442cd95c..96f02128 100644 --- a/examples/pnpm-lock.yaml +++ b/examples/pnpm-lock.yaml @@ -3,7 +3,10 @@ lockfileVersion: 5.4 specifiers: '@babel/cli': 7.18.9 '@babel/core': 7.18.9 + '@babel/parser': 7.18.9 '@babel/preset-typescript': 7.18.6 + '@babel/types': 7.18.6 + '@types/jasmine': '*' '@types/node': 18.6.1 date-fns: 2.29.1 typescript: 4.8.2 @@ -11,7 +14,10 @@ specifiers: dependencies: '@babel/cli': 7.18.9_@babel+core@7.18.9 '@babel/core': 7.18.9 + '@babel/parser': 7.18.9 '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9 + '@babel/types': 7.18.6 + '@types/jasmine': 4.3.0 '@types/node': 18.6.1 date-fns: 2.29.1 typescript: 4.8.2 @@ -320,6 +326,14 @@ packages: - supports-color dev: false + /@babel/types/7.18.6: + resolution: {integrity: sha512-NdBNzPDwed30fZdDQtVR7ZgaO4UKjuaQFH9VArS+HMnurlOY0JWN+4ROlu/iapMFwjRQU4pOG4StZfDmulEwGA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.18.6 + to-fast-properties: 2.0.0 + dev: false + /@babel/types/7.18.9: resolution: {integrity: sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==} engines: {node: '>=6.9.0'} @@ -372,6 +386,10 @@ packages: dev: false optional: true + /@types/jasmine/4.3.0: + resolution: {integrity: sha512-u1jWakf8CWvLfSEZyxmzkgBzOEvXH/szpT0e6G8BTkx5Eu0BhDn7sbc5dz0JBN/6Wwm9rBe+JAsk9tJRyH9ZkA==} + dev: false + /@types/node/18.6.1: resolution: {integrity: sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg==} dev: false diff --git a/examples/project_references/BUILD.bazel b/examples/project_references/BUILD.bazel new file mode 100644 index 00000000..cb26dee5 --- /dev/null +++ b/examples/project_references/BUILD.bazel @@ -0,0 +1,14 @@ +load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin") +load("@aspect_rules_ts//ts:defs.bzl", "ts_config") + +package(default_visibility = ["//examples/project_references:__subpackages__"]) + +ts_config( + name = "tsconfig", + src = "tsconfig-base.json", +) + +copy_to_bin( + name = "tsconfig-base", + srcs = ["tsconfig-base.json"], +) diff --git a/examples/project_references/README.md b/examples/project_references/README.md new file mode 100644 index 00000000..f05fd689 --- /dev/null +++ b/examples/project_references/README.md @@ -0,0 +1,29 @@ +# TypeScript Project References + +TypeScript has its own feature for breaking up a large TS program into multiple compilation units: + +> Project references are a new feature in TypeScript 3.0 that allow you to structure your TypeScript programs into smaller pieces. +> By doing this, you can greatly improve build times, enforce logical separation between components, and organize your code in new and better ways. +> We’re also introducing a new mode for tsc, the --build flag, that works hand in hand with project references to enable faster TypeScript builds. + +See documentation: https://www.typescriptlang.org/docs/handbook/project-references.html + +This works with rules_ts, as this example demonstrates. +However, Project References don't provide any benefit in Bazel, since we already knew how to compile +projects independently and have them reference each other. +The reason you'd set it up this way is to allow `tsc --build` to work outside of Bazel, allowing +both legacy and Bazel workflows to coexist in the same codebase. + +## Known issues + +TypeScript writes a .tsbuildinfo output file for composite projects. This is intended to make a +subsequent compilation of that project faster, by loading that file and reusing information from +a previous execution of `tsc` to enable incremental builds. +Under Bazel, the `.tsbuildinfo` file is produced, but the result is ignored, because Bazel does not +permit the output of an action to be reused as an input to a subsequent spawn of the same action, +as that would be non-hermetic. + +## Dependency graph + +`app` depends on `lib_b` which depends on `lib_a`. Each one is marked with `composite = True` so +TypeScript knows to look for the referenced project by resolving to the other `tsconfig.json` file. diff --git a/examples/project_references/app/BUILD.bazel b/examples/project_references/app/BUILD.bazel new file mode 100644 index 00000000..d04cd206 --- /dev/null +++ b/examples/project_references/app/BUILD.bazel @@ -0,0 +1,10 @@ +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") + +ts_project( + name = "compile", + composite = True, + declaration = True, + extends = "//examples/project_references:tsconfig-base", + tsconfig = "tsconfig.json", + deps = ["//examples/project_references/lib_b"], +) diff --git a/examples/project_references/app/index.ts b/examples/project_references/app/index.ts new file mode 100644 index 00000000..8f5509ef --- /dev/null +++ b/examples/project_references/app/index.ts @@ -0,0 +1,11 @@ +// This import ought to be disallowed by a "strict dependencies" feature. +// We didn't tell TypeScript that app depends on lib_a, so the import should not resolve. +// In theory, we could make a custom compiler just for Bazel (like ts_library from google3) +// to implement a strict dependencies feature, but the rules_ts philosophy is to be a thin starlark +// wrapper around TypeScript's tsc. +// See https://github.com/microsoft/TypeScript/issues/36743 +import { a } from '../lib_a' +import { sayHello } from '../lib_b' + +sayHello('world') +console.error(a) diff --git a/examples/project_references/app/tsconfig.json b/examples/project_references/app/tsconfig.json new file mode 100644 index 00000000..bbc117a5 --- /dev/null +++ b/examples/project_references/app/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig-base.json", + "references": [{ "path": "../lib_b/tsconfig.json" }] +} diff --git a/examples/project_references/lib_a/BUILD.bazel b/examples/project_references/lib_a/BUILD.bazel new file mode 100644 index 00000000..fdc248b4 --- /dev/null +++ b/examples/project_references/lib_a/BUILD.bazel @@ -0,0 +1,27 @@ +load("@aspect_rules_ts//ts:defs.bzl", "ts_config", "ts_project") + +package(default_visibility = ["//examples/project_references:__subpackages__"]) + +ts_config( + name = "config", + src = "tsconfig.json", + deps = [ + "tsconfig-extended.json", + "//examples/project_references:tsconfig", + ], +) + +ts_project( + name = "lib_a", + composite = True, + tsconfig = "config", + # Intentionally not syncing this option from tsconfig, to test validator suppression + # source_map = True, + validate = False, + # Use @babel/parser since the package.json is required to resolve "typings" field + # Repro of #2044 + deps = [ + "//examples:node_modules/@babel/parser", + "//examples:node_modules/@babel/types", + ], +) diff --git a/examples/project_references/lib_a/index.d.ts b/examples/project_references/lib_a/index.d.ts new file mode 100644 index 00000000..bd5ef5b0 --- /dev/null +++ b/examples/project_references/lib_a/index.d.ts @@ -0,0 +1,3 @@ +export declare const a: string +import { DecoratorsPluginOptions } from '@babel/parser' +export declare const o: DecoratorsPluginOptions diff --git a/examples/project_references/lib_a/index.ts b/examples/project_references/lib_a/index.ts new file mode 100644 index 00000000..84121f31 --- /dev/null +++ b/examples/project_references/lib_a/index.ts @@ -0,0 +1,5 @@ +export const a: string = 'hello' + +// Repro rules_nodejs#2044 +import { DecoratorsPluginOptions } from '@babel/parser' +export const o: DecoratorsPluginOptions = {} diff --git a/examples/project_references/lib_a/tsconfig-extended.json b/examples/project_references/lib_a/tsconfig-extended.json new file mode 100644 index 00000000..307dfb35 --- /dev/null +++ b/examples/project_references/lib_a/tsconfig-extended.json @@ -0,0 +1,4 @@ +// This file is an extra hop of indirection, regression test for rules_nodejs#1754 +{ + "extends": "../tsconfig-base" +} diff --git a/examples/project_references/lib_a/tsconfig.json b/examples/project_references/lib_a/tsconfig.json new file mode 100644 index 00000000..4a6235ae --- /dev/null +++ b/examples/project_references/lib_a/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig-extended.json", + "compilerOptions": { + "sourceMap": true + } +} diff --git a/examples/project_references/lib_b/BUILD.bazel b/examples/project_references/lib_b/BUILD.bazel new file mode 100644 index 00000000..7dc98371 --- /dev/null +++ b/examples/project_references/lib_b/BUILD.bazel @@ -0,0 +1,40 @@ +load("@aspect_rules_jasmine//jasmine:defs.bzl", "jasmine_test") +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") + +package(default_visibility = ["//examples/project_references:__subpackages__"]) + +ts_project( + name = "lib_b", + srcs = ["index.ts"], + # just a test for the pass-through args attribute + args = ["--emitBOM"], + composite = True, + declaration = True, + extends = "//examples/project_references:tsconfig-base", + deps = ["//examples/project_references/lib_a"], +) + +ts_project( + name = "transpile_test", + testonly = True, + srcs = [":index.spec.ts"], + composite = True, + declaration = True, + extends = "//examples/project_references:tsconfig-base", + tsconfig = "tsconfig-test.json", + deps = [ + ":lib_b", + "//examples:node_modules/@types/jasmine", + "//examples:node_modules/@types/node", + ], +) + +jasmine_test( + name = "test", + args = ["*.spec.js"], + chdir = package_name(), + data = [ + "index.spec.js", + ":lib_b", + ], +) diff --git a/examples/project_references/lib_b/index.spec.ts b/examples/project_references/lib_b/index.spec.ts new file mode 100644 index 00000000..dd2a37a4 --- /dev/null +++ b/examples/project_references/lib_b/index.spec.ts @@ -0,0 +1,15 @@ +import { sayHello } from './' + +describe('b', () => { + it('should say hello', () => { + let captured: string = '' + console.log = (s: string) => (captured = s) + sayHello(' world') + expect(captured).toBe('hello world') + }) + it('should include byte-order mark since that was passed in args attr', () => { + expect( + require('fs').readFileSync(require.resolve('./index'), 'utf-8')[0] + ).toBe('\ufeff') + }) +}) diff --git a/examples/project_references/lib_b/index.ts b/examples/project_references/lib_b/index.ts new file mode 100644 index 00000000..62d9e216 --- /dev/null +++ b/examples/project_references/lib_b/index.ts @@ -0,0 +1,5 @@ +import { a } from '../lib_a' + +export function sayHello(f: string) { + console.log(a + f) +} diff --git a/examples/project_references/lib_b/tsconfig-test.json b/examples/project_references/lib_b/tsconfig-test.json new file mode 100644 index 00000000..cfee39d3 --- /dev/null +++ b/examples/project_references/lib_b/tsconfig-test.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "types": ["jasmine", "node"] + }, + "references": [{ "path": "./" }], + "include": ["*.spec.ts"] +} diff --git a/examples/project_references/lib_b/tsconfig.json b/examples/project_references/lib_b/tsconfig.json new file mode 100644 index 00000000..2b7459bb --- /dev/null +++ b/examples/project_references/lib_b/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig-base.json", + "references": [{ "path": "../lib_a" }], + "exclude": ["*.spec.ts"] +} diff --git a/examples/project_references/tsconfig-base.json b/examples/project_references/tsconfig-base.json new file mode 100644 index 00000000..143fb491 --- /dev/null +++ b/examples/project_references/tsconfig-base.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "declaration": true, + "lib": ["ES2015", "ES2016.Array.Include", "DOM"], + "module": "commonjs", + "target": "ES2015", + "composite": true + } +} diff --git a/internal_deps.bzl b/internal_deps.bzl index 722ed56c..08565ea6 100644 --- a/internal_deps.bzl +++ b/internal_deps.bzl @@ -52,3 +52,11 @@ def rules_ts_internal_deps(): "https://github.com/bazelbuild/stardoc/releases/download/0.5.2/stardoc-0.5.2.tar.gz", ], ) + + maybe( + http_archive, + name = "aspect_rules_jasmine", + sha256 = "938a2818100fd89e7600a45b7ba4fcd4114c11c5b5741db30ff7c6e0dcb2ea4b", + strip_prefix = "rules_jasmine-0.1.0", + url = "https://github.com/aspect-build/rules_jasmine/archive/refs/tags/v0.1.0.tar.gz", + ) diff --git a/ts/private/ts_project.bzl b/ts/private/ts_project.bzl index ae7f6c98..a39dc5ba 100644 --- a/ts/private/ts_project.bzl +++ b/ts/private/ts_project.bzl @@ -89,6 +89,13 @@ def _ts_project_impl(ctx): inputs = srcs_inputs[:] for dep in ctx.attr.deps: + # When TypeScript builds a composite project, our compilation will want to read the tsconfig.json of + # composite projects we reference. + # Otherwise we'd get an error like + # examples/project_references/lib_b/tsconfig.json(5,9): error TS6053: + # File 'execroot/aspect_rules_ts/bazel-out/k8-fastbuild/bin/examples/project_references/lib_a/tsconfig.json' not found. + if ctx.attr.composite and TsConfigInfo in dep: + inputs.extend(dep[TsConfigInfo].deps.to_list()) if ValidOptionsInfo in dep: inputs.append(dep[ValidOptionsInfo].marker)