Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate @fluent/bundle to TypeScript #436

Merged
merged 48 commits into from
Jan 23, 2020
Merged

Conversation

stasm
Copy link
Contributor

@stasm stasm commented Dec 2, 2019

See #376 for the discussion. As an experiment, I ported @fluent/bundle to TypeScript. Migrating the files in src/ was a breeze; I did run into a few troubles with tests and packaging, however.

Summary

  • Files in src/ are now written in TypeScript.
  • make compile compiles them into corresponding JS files in esm/.
    • This also produces type definitions.
    • tsc is a project-based compiler which compiles many files at once. While it's possible to run it on a file-by-file basis, it's slower to do so and some features of TS won't work (e.g. const enum). I added an empty file at esm/.compiled which is touched every time tsc runs to indicate to make that the compiled files are up to date compared to the source in src.
  • I kept all test files in JS.
    • They import from the esm/ directory, which means that the project needs to be compiled before tests are run.
    • make test takes care of that automatically.
  • The other option for running tests was to use a Babel plugin for stripping TypeScript syntax from files in src at the moment when the tests are run.
    • My own preference (at least for now) was to introduce a two-step process of first compiling the source and then running the tests. The compilation can fail early due to typing errors which shortens the feedback loop.
    • I also liked the idea of running tests on actual compiled files in the esm folder rather than using one tool to compile them for shipping (tsc) and another to run tests (@babel/preset-typescript).
  • I used typedoc to produce API docs.
  • I didn't touch other parts of the build pipeline.
    • I considered using tsc's target: "es2017" to produce the compat.js build, but in the end decided that it would be best to limit the number of moving parts in the PR.
    • The compat.js build is still produced with Rollup and Babel, as it used to be.
    • I filed Revisit Compatibility #435 to discuss a long term clean-up effort which could end up with us dropping Babel in favor of tsc's target: "es2017".

Work Remaining

  • I still need to configure the linter.

API Changes

  • I renamed the FluentType class to FluentBaseType, and then I re-introduced FluentType as a type alias in TS: type FluentType = string | FluentBaseType. I'm not sure about the naming, however, and I'm open to alternatives.
  • (Added on Dec 9) I removed FluentError. It was only used in the runtime parser. In the resolver we use native errors: RangeError, ReferenceError, and TypeError, so I changed all uses of FluentError to the native SyntaxError.

@stasm stasm mentioned this pull request Dec 2, 2019
7 tasks
fluent-bundle/src/index.ts Outdated Show resolved Hide resolved
fluent-bundle/src/builtins.ts Outdated Show resolved Hide resolved
}

if (arg instanceof FluentNumber) {
return new FluentDateTime(arg.valueOf(), { ...arg.opts, ...values(opts) });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FluentDateTime extends FluentNumber now, which made this possible. I needed a way to type-guard the access to .opts, which is only guaranteed to exist on FluentNumber and FluentDateTime. Initially I just made two instanceof checks here, but then decided that FluentNumber and FluentDateTime were close enough to make them related. FluentDateTime keeps its value as a number, too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having second thoughts about this.

In TypeScript, this works because Intl.DateTimeFormatOptions happens to be assignable to Intl.NumberFormatOptions. I'm not sure if we should rely on this.

On a higher level, this is a spec question: what should be the outcome of NUMBER($date) and DATETIME($number)? I think it will be safer to not make assumptions about the answer right now and wait for the formatting spec to be finished.

I'll revert the FluentDateTime extends FluentNumber change.

fluent-bundle/src/bundle.ts Outdated Show resolved Hide resolved
@stasm
Copy link
Contributor Author

stasm commented Dec 3, 2019

@blushingpenguin, @huy-nguyen, @Seally -- would you be interested in taking a look at this PR and giving me hints about how to improve it? Thanks!

@stasm stasm marked this pull request as ready for review December 9, 2019 17:50
@stasm
Copy link
Contributor Author

stasm commented Dec 9, 2019

I got to a point where this PR is now ready for review. I'd like to flag both @Pike for a general review, and @zbraniecki, who has typing experience from working on fluent-rs.

I added explicit return types to all functions and methods and I like how it improves the reading experience when browsing the code, in particular outside the IDE. I have a few naming questions; I asked them in inline comments.

@stasm stasm requested review from zbraniecki and Pike December 9, 2019 17:59
@Pike
Copy link
Contributor

Pike commented Dec 10, 2019

Some ad-hoc comments from the first phase of my review:

I tried to map the syntax ebnf to ast.ts, and struggled for two reasons:

  • the ordering of types and variants is different
  • the Runtime prefix dominates my ability to discover concepts I know.

The latter might have been exaggerated by the fact that I was looking at expressions, which means everything is called RuntimeFooExpression, which doesn't blend well with how humans read.

Or at least I read that they read by "first few characters and last few characters and sum of characters inbetween".

Copy link
Contributor

@Pike Pike left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stas and I talked about this a bit, keeping track of this in the PR:

  • revert the behavior changes of NUMBER and friends on unused positional arguments
  • use plain Node names for classes in ast.js, as there's no ambiguity with @fluent/syntax

stas also suggested to rename the resolver functions from MessageReference to something that actually resembles what the function does. I'm not sure if that makes this patch easier or harder to review, but the general idea makes sense to me.

Copy link
Contributor

@Gregoor Gregoor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only had time for a quick check, will look at it more later this week. Looks great so far, excited for a typed future! 🎉

fluent-bundle/src/resolver.ts Show resolved Hide resolved
if (!func) {
scope.reportError(new ReferenceError(`Unknown function: ${name}()`));
return new FluentNone(`${name}()`);
switch (name) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I liked the old way better, but I guess the way TS does exports it's not as easy to achieve?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was getting an error about typeof import(...) not having an index signature. A workaround which I found was to use let func = scope.bundle._functions[name] || (<any>builtins)[name];, which is shorter and much closer to the old way. Do you think it's a better choice here than the explicit switch?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... or add NUMBER and DATE to _functions in the bundle constructor?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was getting an error about typeof import(...) not having an index signature. A workaround which I found was to use let func = scope.bundle._functions[name] || (<any>builtins)[name];, which is shorter and much closer to the old way. Do you think it's a better choice here than the explicit switch?

Yeah no I guess I agree that the switch is better than that, because we don't lose type info that way. Another option which imo beats switch would be sth like:

let func = scope.bundle._functions[name] || (
    name == "DATETIME" || name == "NUMBER" ? builtins[name] : null
);

There should also be a more generic version to achieve the type refinement necessary to use name as an index, but I'm not coming up with it rn 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like @Pike's suggestion to deal with this when the bundle is constructed rather than later on, when translations are resolved. I implemented it in 4d48ced.

@stasm
Copy link
Contributor Author

stasm commented Jan 13, 2020

I updated this PR to merge cleanly into the current master.

Copy link
Contributor

@Pike Pike left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ast is much more readable now, which has the side effect that I found a few more things.

General things I still need to learn is what TS types actually do, in terms of instanceof. I wonder if we could get rid of the pseudo type type.

Also, I'd like to learn more about the use of public.

fluent-bundle/src/ast.ts Outdated Show resolved Hide resolved
fluent-bundle/src/ast.ts Outdated Show resolved Hide resolved
fluent-bundle/src/ast.ts Outdated Show resolved Hide resolved
if (!func) {
scope.reportError(new ReferenceError(`Unknown function: ${name}()`));
return new FluentNone(`${name}()`);
switch (name) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... or add NUMBER and DATE to _functions in the bundle constructor?

fluent-bundle/src/resource.ts Outdated Show resolved Hide resolved
@stasm stasm requested a review from Pike January 15, 2020 16:49
Copy link
Contributor

@Pike Pike left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One nit question, one question more about understanding TS, but this looks good to me either way.

fluent-bundle/src/bundle.ts Outdated Show resolved Hide resolved
fluent-bundle/src/bundle.ts Show resolved Hide resolved
@stasm
Copy link
Contributor Author

stasm commented Jan 23, 2020

I have verified that @fluent/bundle built from TypeScript works correctly in a React app (The Fluent Playground) and in Firefox (as Fluent.jsm).

@stasm stasm merged commit e992e23 into projectfluent:master Jan 23, 2020
@stasm stasm deleted the ts-bundle branch January 23, 2020 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants