Skip to content

Commit

Permalink
Validate ts project (bazel-contrib#1722)
Browse files Browse the repository at this point in the history
* feat(typescript): validate that ts_project configuration matches the tsconfig settings
  • Loading branch information
alexeagle authored Mar 26, 2020
1 parent 3bf8631 commit 8644816
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/typescript/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pkg_npm(
],
deps = [
"@npm_bazel_typescript//internal:BUILD",
"@npm_bazel_typescript//internal:ts_project_options_validator.js",
] + select({
# FIXME: fix stardoc on Windows; @npm_bazel_typescript//:index.md generation fails with:
# ERROR: D:/b/62unjjin/external/npm_bazel_typescript/BUILD.bazel:36:1: Couldn't build file
Expand Down
1 change: 1 addition & 0 deletions packages/typescript/replacements.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ TYPESCRIPT_REPLACEMENTS = dict(
# @build_bazel_rules_typescript//:npm_bazel_typescript_package
# use this alternate fencing
"(#|\/\/)\\s+BEGIN-DEV-ONLY[\\w\W]+?(#|\/\/)\\s+END-DEV-ONLY": "",
"//internal:local_validator": "@npm//@bazel/typescript/bin:ts_project_options_validator",
# This file gets vendored into our repo
"@build_bazel_rules_typescript//internal:common": "//internal:common",
}
Expand Down
26 changes: 24 additions & 2 deletions packages/typescript/src/internal/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,34 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# gazelle:exclude worker_protocol.proto

load("@bazel_skylib//:bzl_library.bzl", "bzl_library")

# gazelle:exclude worker_protocol.proto
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
load("@npm//typescript:index.bzl", "tsc")

package(default_visibility = ["//visibility:public"])

tsc(
name = "compile",
outs = ["ts_project_options_validator.js"],
args = [
"--outDir $(RULEDIR)",
"$(execpath ts_project_options_validator.ts)",
"--types node",
],
data = [
"ts_project_options_validator.ts",
"@npm//@types/node",
],
)

nodejs_binary(
name = "local_validator",
data = ["@npm//typescript"],
entry_point = "ts_project_options_validator.js",
)

bzl_library(
name = "bzl",
srcs = glob(
Expand Down
66 changes: 65 additions & 1 deletion packages/typescript/src/internal/ts_project.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,50 @@ ts_project = rule(
attrs = dict(_ATTRS, **_OUTPUTS),
)

def _validate_options_impl(ctx):
# Bazel won't run our action unless its output is needed, so make a marker file
# We make it a .d.ts file so we can plumb it to the deps of the ts_project compile.
marker = ctx.actions.declare_file("%s.optionsvalid.d.ts" % ctx.label.name)

arguments = ctx.actions.args()
arguments.add_all([ctx.file.tsconfig.path, marker.path, ctx.attr.target, struct(
declaration = ctx.attr.declaration,
declaration_map = ctx.attr.declaration_map,
composite = ctx.attr.composite,
emit_declaration_only = ctx.attr.emit_declaration_only,
source_map = ctx.attr.source_map,
incremental = ctx.attr.incremental,
).to_json()])

run_node(
ctx,
inputs = [ctx.file.tsconfig] + ctx.files.extends,
outputs = [marker],
arguments = [arguments],
executable = "validator",
)
return [
DeclarationInfo(
transitive_declarations = depset([marker]),
),
]

validate_options = rule(
implementation = _validate_options_impl,
attrs = {
"composite": attr.bool(),
"declaration": attr.bool(),
"declaration_map": attr.bool(),
"emit_declaration_only": attr.bool(),
"extends": attr.label_list(allow_files = [".json"]),
"incremental": attr.bool(),
"source_map": attr.bool(),
"target": attr.string(),
"tsconfig": attr.label(mandatory = True, allow_single_file = [".json"]),
"validator": attr.label(default = Label("//internal:local_validator"), executable = True, cfg = "host"),
},
)

def _out_paths(srcs, ext):
return [f[:f.rindex(".")] + ext for f in srcs if not f.endswith(".d.ts")]

Expand All @@ -149,6 +193,7 @@ def ts_project_macro(
incremental = False,
emit_declaration_only = False,
tsc = _DEFAULT_TSC,
validate = True,
**kwargs):
"""Compiles one TypeScript project using `tsc --project`
Expand Down Expand Up @@ -273,6 +318,8 @@ def ts_project_macro(
For example, `tsc = "@my_deps//typescript/bin:tsc"`
Or you can pass a custom compiler binary instead.
validate: boolean; whether to check that the tsconfig settings match the attributes.
declaration: if the `declaration` bit is set in the tsconfig.
Instructs Bazel to expect a `.d.ts` output for each `.ts` source.
source_map: if the `sourceMap` bit is set in the tsconfig.
Expand All @@ -293,11 +340,28 @@ def ts_project_macro(
if tsconfig == None:
tsconfig = name + ".json"

extra_deps = []

if validate:
validate_options(
name = "_validate_%s_options" % name,
target = "//%s:%s" % (native.package_name(), name),
declaration = declaration,
source_map = source_map,
declaration_map = declaration_map,
composite = composite,
incremental = incremental,
emit_declaration_only = emit_declaration_only,
tsconfig = tsconfig,
extends = extends,
)
extra_deps.append("_validate_%s_options" % name)

ts_project(
name = name,
srcs = srcs,
deps = deps,
args = args,
deps = deps + extra_deps,
tsconfig = tsconfig,
extends = extends,
js_outs = _out_paths(srcs, ".js") if not emit_declaration_only else [],
Expand Down
83 changes: 83 additions & 0 deletions packages/typescript/src/internal/ts_project_options_validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as ts from 'typescript';

const diagnosticsHost: ts.FormatDiagnosticsHost = {
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
getNewLine: () => ts.sys.newLine,
// Print filenames including their relativeRoot, so they can be located on
// disk
getCanonicalFileName: (f: string) => f
};

function main([tsconfigPath, output, target, attrsStr]: string[]): 0|1 {
// The Bazel ts_project attributes were json-encoded
// (on Windows the quotes seem to be quoted wrong, so replace backslash with quotes :shrug:)
const attrs = JSON.parse(attrsStr.replace(/\\/g, '"'));

// Parse your typescript settings from the tsconfig
// This will understand the "extends" semantics.
const {config, error} = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
if (error) throw new Error(tsconfigPath + ':' + ts.formatDiagnostic(error, diagnosticsHost));
const {errors, options} =
ts.parseJsonConfigFileContent(config, ts.sys, require('path').dirname(tsconfigPath));
// We don't pass the srcs to this action, so it can't know if the program has the right sources.
// Diagnostics look like
// error TS18002: The 'files' list in config file 'tsconfig.json' is empty.
// error TS18003: No inputs were found in config file 'tsconfig.json'. Specified 'include'...
const fatalErrors = errors.filter(e => e.code !== 18002 && e.code != 18003);
if (fatalErrors.length > 0)
throw new Error(tsconfigPath + ':' + ts.formatDiagnostics(fatalErrors, diagnosticsHost));

const failures: string[] = [];
const buildozerCmds: string[] = [];
function check(option: string, attr?: string) {
attr = attr || option;
// treat compilerOptions undefined as false
const optionVal = options[option] === undefined ? false : options[option];
if (optionVal !== attrs[attr]) {
failures.push(
`attribute ${attr}=${attrs[attr]} does not match compilerOptions.${option}=${optionVal}`);
buildozerCmds.push(`set ${attr} ${optionVal ? 'True' : 'False'}`);
}
}

check('declarationMap', 'declaration_map');
check('emitDeclarationOnly', 'emit_declaration_only');
check('sourceMap', 'source_map');
check('composite');
check('declaration');
check('incremental');

if (failures.length > 0) {
console.error(`ERROR: ts_project rule ${
target} was configured with attributes that don't match the tsconfig`);
failures.forEach(f => console.error(' - ' + f));
console.error('You can automatically fix this by running:');
console.error(` npx @bazel/buildozer ${buildozerCmds.map(c => `'${c}'`).join(' ')} ${target}`);
console.error('Or to suppress this error, run:');
console.error(` npx @bazel/buildozer 'set validate False' ${target}`);
return 1;
}

// We have to write an output so that Bazel needs to execute this action.
// Make the output change whenever the attributes changed.
require('fs').writeFileSync(
output, `
// ${process.argv[1]} checked attributes for ${target}
// composite: ${attrs.composite}
// declaration: ${attrs.declaration}
// declaration_map: ${attrs.declaration_map}
// incremental: ${attrs.incremental}
// source_map: ${attrs.source_map}
// emit_declaration_only: ${attrs.emit_declaration_only}
`,
'utf-8');
return 0;
}

if (require.main === module) {
try {
process.exitCode = main(process.argv.slice(2));
} catch (e) {
console.error(process.argv[1], e);
}
}
3 changes: 2 additions & 1 deletion packages/typescript/src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"main": "./internal/tsc_wrapped/index.js",
"typings": "./internal/tsc_wrapped/index.d.ts",
"bin": {
"tsc_wrapped": "./internal/tsc_wrapped/tsc_wrapped.js"
"tsc_wrapped": "./internal/tsc_wrapped/tsc_wrapped.js",
"ts_project_options_validator": "./internal/ts_project_options_validator.js"
},
"//": "note that typescript doesn't follow semver, so technically anything 3.6 or higher might break us",
"peerDependencies": {
Expand Down
4 changes: 3 additions & 1 deletion packages/typescript/test/ts_project/a/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ load("@npm_bazel_typescript//:index.bzl", "ts_project")
ts_project(
composite = True,
extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"],
source_map = True,
# Intentionally not syncing this option from tsconfig, to test validator suppression
# source_map = True,
validate = False,
visibility = ["//packages/typescript/test:__subpackages__"],
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
{
"files": ["a.ts"],
"compilerOptions": {
"declaration": true
}
"files": ["a.d.ts"]
}
9 changes: 9 additions & 0 deletions packages/typescript/test/ts_project/validation/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("@npm_bazel_typescript//:index.bzl", "ts_project")

ts_project(
composite = True,
declaration = True,
declaration_map = True,
incremental = True,
source_map = True,
)
1 change: 1 addition & 0 deletions packages/typescript/test/ts_project/validation/a.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const a: number = 0;
9 changes: 9 additions & 0 deletions packages/typescript/test/ts_project/validation/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"incremental": true,
"composite": true,
"declarationMap": true,
"declaration": true,
"sourceMap": true,
}
}

0 comments on commit 8644816

Please sign in to comment.