Tsickle - TypeScript to Closure Translator
Tsickle converts TypeScript code into a form acceptable to the Closure
Compiler. This allows using TypeScript to transpile your sources, and then
using Closure Compiler to bundle and optimize them, while taking advantage of
type information in Closure Compiler.
What conversion means
A (non-exhaustive) list of the sorts of transformations Tsickle applies:
- inserts Closure-compatible JSDoc annotations on functions/classes/etc
- converts ES6 modules into
goog.module
modules - generates externs.js from TypeScript d.ts (and
declare
, see below) - declares types for class member variables
- translates
export * from ...
into a form Closure accepts - converts TypeScript enums into a form Closure accepts
- reprocesses all jsdoc to strip Closure-invalid tags
In general the goal is that you write valid TypeScript and Tsickle handles
making it valid Closure Compiler code.
Warning: work in progress
We already use tsickle within Google to minify our apps (including those using
Angular), but we have less experience using tsickle with the various JavaScript
builds that are seen outside of Google.
We would like to make tsickle usable for everyone but right now if you'd like
to try it you should expect to spend some time debugging and reporting bugs.
Usage
Project Setup
Tsickle works by wrapping tsc
. To use it, you must set up your project such
that it builds correctly when you run tsc
from the command line, by
configuring the settings in tsconfig.json
.
If you have complicated tsc command lines and flags in a build file (like a
gulpfile etc.) Tsickle won't know about it. Another reason it's nice to put
everything in tsconfig.json
is so your editor inherits all these settings as
well.
Invocation
Run tsickle --help
for the full syntax, but basically you provide any tsickle
specific options and use it as a TypeScript compiler.
Output format
Tsickle is designed to do whatever is necessary to make the code acceptable by
Closure compiler. We view its output as a necessary intermediate form for
communicating to the Closure compiler, and not something for humans. This means
the tsickle output may be kind of ugly to read. Its only real use is to pass it
on to the compiler.
For one example, the syntax of types tsickle produces are specific to Closure.
The type {!Foo}
means "Foo, excluding null" and a type alias becomes a var
statement that is tagged with @typedef
.
Tsickle emits modules using Closure's goog.module
module system. This system
is similar to but different from ES modules, and was supported by Closure before
the ES module system was finalized.
Differences from TypeScript
Closure and TypeScript are not identical. Tsickle hides most of the
differences, but users must still be aware of some differences.
declare
Any declaration in a .d.ts
file, as well as any declaration tagged with
declare ...
, is intepreted by Tsickle as a name that should be preserved
through Closure compilation (i.e. not renamed into something shorter). Use it
any time the specific string names of your fields are significant. That would
most often happen when the object either coming from outside your program, or
being passed out of the program.
Example:
declare interface JSONResult {
username: string;
}
let r = JSON.parse(input) as JSONResult;
console.log(r.username);
By adding declare
to the interface (or if it were in a .d.ts
file), Tsickle
will inform Closure that it must use exactly the field name .username
(and not
e.g. .a
) in the output JS. This matters for this example because the input
JSON probably uses the string 'username'
and not whatever name Closure would
invent for it. (Note: declare
on an interface has no additional meaning in
pure TypeScript.)
Exporting decorators
An exporting decorator is a decorator that has @ExportDecoratedItems
in its
JSDoc.
The names of elements that have an exporting decorator are preserved through
the Closure compilation process by applying an @export
tag to them.
Example:
/** @ExportDecoratedItems */
function myDecorator() {
// ...
}
@myDecorator()
class DoNotRenameThisClass { ... }
Development
One-time setup
Run bazel run @nodejs//:yarn --script_path=yarn_install.sh && ./yarn_install.sh
to install the dependencies.
This avoids occupying the bazel
server, so that yarn
can call bazel
again.
Ideally we should just use bazel-run.sh @nodejs//:yarn
, see
https://stackoverflow.com/questions/47082298/how-can-users-get-bazel-run-sh
Test commands
ibazel test test:unit_test
executes the unit tests in watch mode (use bazel test test:unit_test
for a
single run),bazel test test:e2e_test
executes the e2e tests,bazel test test:golden_test
executes the golden tests,node check-format.js
checks the source code formatting using
clang-format
,yarn test
runs unit tests, e2e tests and checks the source code formatting.
Debugging
You can debug tests by using bazel run
and passing --node_options=--inspect
. For example, to
debug a specific golden test:
TEST_FILTER=my_golden_test ibazel run //test:golden_test -- --node_options=--inspect
Then open [about:inspect] in Chrome and choose "about:inspect". Chrome will launch a debugging
session on any node process that starts with a debugger listening on one of the listed ports. The
tsickle tests and Chrome both default to localhost:9229
, so things should work out of the box.
VS Code can also connect using the inspect protocol. It doesn't support automatically reconnecting
or any way to re-run the test suite though, so it is a less convenient. You can start the node
process passing an extra --node_options=--debug-brk
(in addition to the parameters above) to have
Node wait before program execution, so you have time to attach VS Code.
Updating Goldens
Run UPDATE_GOLDENS=y bazel run test:golden_test
to have the test suite update
the goldens in test_files/...
.
Environment variables
Pass the flag --action_env=TEST_FILTER=<REGEX>
to bazel test to limit the
end-to-end test (found in test_files/...
) run tests with a name matching the
regex.
Releasing
On a new branch, run
# tsickle releases are all minor releases for now, see npm help version.
$ npm version minor -m 'rel: %s'
This will update the version in package.json
, commit the changes, and
create a git tag.
Push the branch and get it reviewed, but do not merge. If you click
the "rebase and merge" button in the Github UI it changes the commit,
so the git tag that was created would point at the wrong commit.
Instead, push the branch to master directly via:
$ git push origin mybranch:master
$ git push origin v0.32.0
Note that Github will block non-fast-forward pushes to master, so if
there have been other intervening commits you'll need to recreate the
release. Once the versioned tag is pushed to Github the release (as
found on https://github.com/angular/tsickle/releases) will be
implicitly created.
Run bazel run :npm_package.publish
from the master branch
(you must be logged into the angular
shared npm account).