Flow checks codebases by processing each file separately in dependency
order. After a file has been checked, a signature is extracted and stored in
main memory, to be used for files that depend on it. Currently, the default mode
(we’ll also refer to it as classic mode) builds these signatures by using the
types inferred for the file’s exports. In the new types-first architecture,
Flow relies on annotations available at the boundaries of files to build these
signatures.
The benefit of this new architecture is dual:
-
It dramatically improves performance, in particular when it comes to
rechecks. Suppose we want Flow to check a file foo.js
, for which it hasn’t
checked its dependencies yet. Classic mode would need to check all
dependencies and generate signatures from them first, before it could check
foo.js
. In types-first, Flow extracts the dependency signatures just by
looking at the annotations around the exports. This process is mostly
syntactic, and therefore much faster than full type inference.
-
It improves error reliability. Inferred types often become complicated, and may
lead to errors being reported in downstream files, far away from their actual source.
Type annotations at file boundaries of files can help localize such errors, and
address them in the file that introduced them.
The caveat of this new version is that it requires exported parts of the code to be
annotated with types, or to be expressions whose type can be trivially inferred
(for example numbers and strings).
How to upgrade your codebase to Types-First
Upgrade Flow version
Types-first mode is officially released with version 0.125, but has been available in
experimental status as of version 0.102. If you are currently on an older
Flow version, you’d have to first upgrade Flow. Using the latest Flow version
is the best way to benefit from the performance benefits outlined above.
Prepare your codebase for Types-First
Types-first requires annotations at module boundaries in order to build type
signature for files. If these annotations are missing, then a signature-verification-failure
is raised, and the exported type for the respective part of the code will be any
.
To see what types are missing to make your codebase types-first ready, add the
following line to the [options]
section of the .flowconfig
file:
Consider for example a file foo.js
that exports a function call to foo
1
2
|
declare function foo<T>(x: T): T;
module.exports = foo(1);
|
{"value":"declare function foo<T>(x: T): T;\nmodule.exports = foo(1);\n","tokens":[{"type":"T_DECLARE","context":"normal","value":"declare","line":1,"start":0,"end":7},{"type":"T_FUNCTION","context":"normal","value":"function","line":1,"start":8,"end":16},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":1,"start":17,"end":20},{"type":"T_LESS_THAN","context":"type","value":"<","line":1,"start":20,"end":21},{"type":"T_IDENTIFIER","context":"type","value":"T","line":1,"start":21,"end":22},{"type":"T_GREATER_THAN","context":"type","value":">","line":1,"start":22,"end":23},{"type":"T_LPAREN","context":"type","value":"(","line":1,"start":23,"end":24},{"type":"T_IDENTIFIER","context":"normal","value":"x","line":1,"start":24,"end":25},{"type":"T_COLON","context":"type","value":":","line":1,"start":25,"end":26},{"type":"T_IDENTIFIER","context":"type","value":"T","line":1,"start":27,"end":28},{"type":"T_RPAREN","context":"type","value":")","line":1,"start":28,"end":29},{"type":"T_COLON","context":"normal","value":":","line":1,"start":29,"end":30},{"type":"T_IDENTIFIER","context":"type","value":"T","line":1,"start":31,"end":32},{"type":"T_SEMICOLON","context":"normal","value":";","line":1,"start":32,"end":33},{"type":"T_IDENTIFIER","context":"normal","value":"module","line":2,"start":34,"end":40},{"type":"T_PERIOD","context":"normal","value":".","line":2,"start":40,"end":41},{"type":"T_IDENTIFIER","context":"normal","value":"exports","line":2,"start":41,"end":48},{"type":"T_ASSIGN","context":"normal","value":"=","line":2,"start":49,"end":50},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":2,"start":51,"end":54},{"type":"T_LPAREN","context":"normal","value":"(","line":2,"start":54,"end":55},{"type":"T_NUMBER","context":"normal","value":"1","line":2,"start":55,"end":56},{"type":"T_RPAREN","context":"normal","value":")","line":2,"start":56,"end":57},{"type":"T_SEMICOLON","context":"normal","value":";","line":2,"start":57,"end":58}],"errors":[]}
The return type of function calls is currently not trivially inferable (due to
features like polymorphism, overloading etc.). Their result needs to be annotated
and so you’d see the following error:
Cannot build a typed interface for this module. You should annotate the exports
of this module with types. Cannot determine the type of this call expression. Please
provide an annotation, e.g., by adding a type cast around this expression.
(`signature-verification-failure`)
4│ module.exports = foo(1);
^^^^^^
To resolve this, you can add an annotation like the following:
1
2
|
declare function foo<T>(x: T): T;
module.exports = (foo(1): number);
|
{"value":"declare function foo<T>(x: T): T;\nmodule.exports = (foo(1): number);\n","tokens":[{"type":"T_DECLARE","context":"normal","value":"declare","line":1,"start":0,"end":7},{"type":"T_FUNCTION","context":"normal","value":"function","line":1,"start":8,"end":16},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":1,"start":17,"end":20},{"type":"T_LESS_THAN","context":"type","value":"<","line":1,"start":20,"end":21},{"type":"T_IDENTIFIER","context":"type","value":"T","line":1,"start":21,"end":22},{"type":"T_GREATER_THAN","context":"type","value":">","line":1,"start":22,"end":23},{"type":"T_LPAREN","context":"type","value":"(","line":1,"start":23,"end":24},{"type":"T_IDENTIFIER","context":"normal","value":"x","line":1,"start":24,"end":25},{"type":"T_COLON","context":"type","value":":","line":1,"start":25,"end":26},{"type":"T_IDENTIFIER","context":"type","value":"T","line":1,"start":27,"end":28},{"type":"T_RPAREN","context":"type","value":")","line":1,"start":28,"end":29},{"type":"T_COLON","context":"normal","value":":","line":1,"start":29,"end":30},{"type":"T_IDENTIFIER","context":"type","value":"T","line":1,"start":31,"end":32},{"type":"T_SEMICOLON","context":"normal","value":";","line":1,"start":32,"end":33},{"type":"T_IDENTIFIER","context":"normal","value":"module","line":2,"start":34,"end":40},{"type":"T_PERIOD","context":"normal","value":".","line":2,"start":40,"end":41},{"type":"T_IDENTIFIER","context":"normal","value":"exports","line":2,"start":41,"end":48},{"type":"T_ASSIGN","context":"normal","value":"=","line":2,"start":49,"end":50},{"type":"T_LPAREN","context":"normal","value":"(","line":2,"start":51,"end":52},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":2,"start":52,"end":55},{"type":"T_LPAREN","context":"normal","value":"(","line":2,"start":55,"end":56},{"type":"T_NUMBER","context":"normal","value":"1","line":2,"start":56,"end":57},{"type":"T_RPAREN","context":"normal","value":")","line":2,"start":57,"end":58},{"type":"T_COLON","context":"type","value":":","line":2,"start":58,"end":59},{"type":"T_NUMBER_TYPE","context":"type","value":"number","line":2,"start":60,"end":66},{"type":"T_RPAREN","context":"normal","value":")","line":2,"start":66,"end":67},{"type":"T_SEMICOLON","context":"normal","value":";","line":2,"start":67,"end":68}],"errors":[]}
Note: As of version 0.134, types-first is the default mode. This mode automatically
enables well_formed_exports
, so you would see these errors without explicitly
setting this flag. It is advisable to set types_first=false
during this part of
the upgrade.
As you make progress adding types to your codebase, you can include directories
so that they don’t regress as new code gets committed, and until the entire project
has well-formed exports. You can do this by adding lines like the following to your
.flowconfig:
well_formed_exports.includes=<PROJECT_ROOT>/path/to/directory
Warning: That this is a substring check, not a regular expression (for performance
reasons).
A codemod for large codebases
Adding the necessary annotations to large codebases can be quite tedious. To ease
this burden, we are providing a codemod based on Flow’s inference, that can be
used to annotate multiple files in bulk. See this tutorial for more.
Enable the types-first flag
Once you have eliminated signature verification errors, you can turn on the types-first
mode, by adding the following line to the [options]
section of the .flowconfig
file:
You can also pass --types-first
to the flow check
or flow start
commands.
The well_formed_exports
flag from before is implied by types_first
. Once
this process is completed and types-first has been enabled, you can remove
well_formed_exports
.
Unfortunately, it is not possible to enable types-first mode for part of your repo; this switch
affects all files managed by the current .flowconfig
.
Note: The above flags are available in versions of Flow >=0.102
with the experimental.
prefix (and prior to v0.128, it used whitelist
in place of includes
):
experimental.well_formed_exports=true
experimental.well_formed_exports.whitelist=<PROJECT_ROOT>/path/to/directory
experimental.types_first=true
Note: If you are using a version where types-first is enabled by default (ie. >=0.134
),
make sure you set types_first=false
in your .flowconfig while running the codemods.
Deal with newly introduced errors
Switching between classic and types-first mode may cause some new Flow errors,
besides signature-verification failures that we mentioned earlier. These errors
are due differences in the way types based on annotations are interpreted, compared
to their respective inferred types.
Below are some common error patterns and how to overcome them.
Array tuples treated as regular arrays in exports
In types-first, an array literal in an export position
1
|
module.exports = [e1, e2];
|
{"value":"module.exports = [e1, e2];\n","tokens":[{"type":"T_IDENTIFIER","context":"normal","value":"module","line":1,"start":0,"end":6},{"type":"T_PERIOD","context":"normal","value":".","line":1,"start":6,"end":7},{"type":"T_IDENTIFIER","context":"normal","value":"exports","line":1,"start":7,"end":14},{"type":"T_ASSIGN","context":"normal","value":"=","line":1,"start":15,"end":16},{"type":"T_LBRACKET","context":"normal","value":"[","line":1,"start":17,"end":18},{"type":"T_IDENTIFIER","context":"normal","value":"e1","line":1,"start":18,"end":20},{"type":"T_COMMA","context":"normal","value":",","line":1,"start":20,"end":21},{"type":"T_IDENTIFIER","context":"normal","value":"e2","line":1,"start":22,"end":24},{"type":"T_RBRACKET","context":"normal","value":"]","line":1,"start":24,"end":25},{"type":"T_SEMICOLON","context":"normal","value":";","line":1,"start":25,"end":26}],"errors":[]}
is treated as having type Array<t1 | t2>
, where e1
and e2
have types t1
and t2
, instead of the tuple type [t1, t2]
.
In classic mode, the inferred type encompassed both types at the same time. This
might cause errors in importing files that expect for example to find type t1
in the first position of the import.
Fix: If a tuple type is expected, then the annotation [t1, t2]
needs to be
explicitly added on the export side.
Indirect object assignments in exports
Flow allows the code
1
2
3
4
|
function foo(): void {}
foo.x = () => {};
foo.x.y = 2;
module.exports = foo;
|
{"value":"function foo(): void {}\nfoo.x = () => {};\nfoo.x.y = 2;\nmodule.exports = foo;\n","tokens":[{"type":"T_FUNCTION","context":"normal","value":"function","line":1,"start":0,"end":8},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":1,"start":9,"end":12},{"type":"T_LPAREN","context":"normal","value":"(","line":1,"start":12,"end":13},{"type":"T_RPAREN","context":"normal","value":")","line":1,"start":13,"end":14},{"type":"T_COLON","context":"type","value":":","line":1,"start":14,"end":15},{"type":"T_VOID_TYPE","context":"type","value":"void","line":1,"start":16,"end":20},{"type":"T_LCURLY","context":"normal","value":"{","line":1,"start":21,"end":22},{"type":"T_RCURLY","context":"normal","value":"}","line":1,"start":22,"end":23},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":2,"start":24,"end":27},{"type":"T_PERIOD","context":"normal","value":".","line":2,"start":27,"end":28},{"type":"T_IDENTIFIER","context":"normal","value":"x","line":2,"start":28,"end":29},{"type":"T_ASSIGN","context":"normal","value":"=","line":2,"start":30,"end":31},{"type":"T_LPAREN","context":"normal","value":"(","line":2,"start":32,"end":33},{"type":"T_RPAREN","context":"normal","value":")","line":2,"start":33,"end":34},{"type":"T_ARROW","context":"normal","value":"=>","line":2,"start":35,"end":37},{"type":"T_LCURLY","context":"normal","value":"{","line":2,"start":38,"end":39},{"type":"T_RCURLY","context":"normal","value":"}","line":2,"start":39,"end":40},{"type":"T_SEMICOLON","context":"normal","value":";","line":2,"start":40,"end":41},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":3,"start":42,"end":45},{"type":"T_PERIOD","context":"normal","value":".","line":3,"start":45,"end":46},{"type":"T_IDENTIFIER","context":"normal","value":"x","line":3,"start":46,"end":47},{"type":"T_PERIOD","context":"normal","value":".","line":3,"start":47,"end":48},{"type":"T_IDENTIFIER","context":"normal","value":"y","line":3,"start":48,"end":49},{"type":"T_ASSIGN","context":"normal","value":"=","line":3,"start":50,"end":51},{"type":"T_NUMBER","context":"normal","value":"2","line":3,"start":52,"end":53},{"type":"T_SEMICOLON","context":"normal","value":";","line":3,"start":53,"end":54},{"type":"T_IDENTIFIER","context":"normal","value":"module","line":4,"start":55,"end":61},{"type":"T_PERIOD","context":"normal","value":".","line":4,"start":61,"end":62},{"type":"T_IDENTIFIER","context":"normal","value":"exports","line":4,"start":62,"end":69},{"type":"T_ASSIGN","context":"normal","value":"=","line":4,"start":70,"end":71},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":4,"start":72,"end":75},{"type":"T_SEMICOLON","context":"normal","value":";","line":4,"start":75,"end":76}],"errors":[]}
but in types-first the exported type will be
{
(): void;
x: () => void;
}
In other words it won’t take into account the update on y
.
Fix: To include the update on y
in the exported type, the export will need
to be annotated with the type
{
(): void;
x: { (): void; y: number; };
};
The same holds for more complex assignment patterns like
1
2
3
|
function foo(): void {}
Object.assign(foo, { x: 1});
module.exports = foo;
|
{"value":"function foo(): void {}\nObject.assign(foo, { x: 1});\nmodule.exports = foo;\n","tokens":[{"type":"T_FUNCTION","context":"normal","value":"function","line":1,"start":0,"end":8},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":1,"start":9,"end":12},{"type":"T_LPAREN","context":"normal","value":"(","line":1,"start":12,"end":13},{"type":"T_RPAREN","context":"normal","value":")","line":1,"start":13,"end":14},{"type":"T_COLON","context":"type","value":":","line":1,"start":14,"end":15},{"type":"T_VOID_TYPE","context":"type","value":"void","line":1,"start":16,"end":20},{"type":"T_LCURLY","context":"normal","value":"{","line":1,"start":21,"end":22},{"type":"T_RCURLY","context":"normal","value":"}","line":1,"start":22,"end":23},{"type":"T_IDENTIFIER","context":"normal","value":"Object","line":2,"start":24,"end":30},{"type":"T_PERIOD","context":"normal","value":".","line":2,"start":30,"end":31},{"type":"T_IDENTIFIER","context":"normal","value":"assign","line":2,"start":31,"end":37},{"type":"T_LPAREN","context":"normal","value":"(","line":2,"start":37,"end":38},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":2,"start":38,"end":41},{"type":"T_COMMA","context":"normal","value":",","line":2,"start":41,"end":42},{"type":"T_LCURLY","context":"normal","value":"{","line":2,"start":43,"end":44},{"type":"T_IDENTIFIER","context":"normal","value":"x","line":2,"start":45,"end":46},{"type":"T_COLON","context":"normal","value":":","line":2,"start":46,"end":47},{"type":"T_NUMBER","context":"normal","value":"1","line":2,"start":48,"end":49},{"type":"T_RCURLY","context":"normal","value":"}","line":2,"start":49,"end":50},{"type":"T_RPAREN","context":"normal","value":")","line":2,"start":50,"end":51},{"type":"T_SEMICOLON","context":"normal","value":";","line":2,"start":51,"end":52},{"type":"T_IDENTIFIER","context":"normal","value":"module","line":3,"start":53,"end":59},{"type":"T_PERIOD","context":"normal","value":".","line":3,"start":59,"end":60},{"type":"T_IDENTIFIER","context":"normal","value":"exports","line":3,"start":60,"end":67},{"type":"T_ASSIGN","context":"normal","value":"=","line":3,"start":68,"end":69},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":3,"start":70,"end":73},{"type":"T_SEMICOLON","context":"normal","value":";","line":3,"start":73,"end":74}],"errors":[]}
where you’ll need to manually annotate the export with { (): void; x: number }
,
or assignments preceding the function definition
1
2
3
|
foo.x = 1;
function foo(): void {}
module.exports = foo;
|
{"value":"foo.x = 1;\nfunction foo(): void {}\nmodule.exports = foo;\n","tokens":[{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":1,"start":0,"end":3},{"type":"T_PERIOD","context":"normal","value":".","line":1,"start":3,"end":4},{"type":"T_IDENTIFIER","context":"normal","value":"x","line":1,"start":4,"end":5},{"type":"T_ASSIGN","context":"normal","value":"=","line":1,"start":6,"end":7},{"type":"T_NUMBER","context":"normal","value":"1","line":1,"start":8,"end":9},{"type":"T_SEMICOLON","context":"normal","value":";","line":1,"start":9,"end":10},{"type":"T_FUNCTION","context":"normal","value":"function","line":2,"start":11,"end":19},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":2,"start":20,"end":23},{"type":"T_LPAREN","context":"normal","value":"(","line":2,"start":23,"end":24},{"type":"T_RPAREN","context":"normal","value":")","line":2,"start":24,"end":25},{"type":"T_COLON","context":"type","value":":","line":2,"start":25,"end":26},{"type":"T_VOID_TYPE","context":"type","value":"void","line":2,"start":27,"end":31},{"type":"T_LCURLY","context":"normal","value":"{","line":2,"start":32,"end":33},{"type":"T_RCURLY","context":"normal","value":"}","line":2,"start":33,"end":34},{"type":"T_IDENTIFIER","context":"normal","value":"module","line":3,"start":35,"end":41},{"type":"T_PERIOD","context":"normal","value":".","line":3,"start":41,"end":42},{"type":"T_IDENTIFIER","context":"normal","value":"exports","line":3,"start":42,"end":49},{"type":"T_ASSIGN","context":"normal","value":"=","line":3,"start":50,"end":51},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":3,"start":52,"end":55},{"type":"T_SEMICOLON","context":"normal","value":";","line":3,"start":55,"end":56}],"errors":[]}
Note that in the last example, Flow types-first will pick up the static update if
it was after the definition:
1
2
3
|
function foo(): void {}
foo.x = 1;
module.exports = foo;
|
{"value":"function foo(): void {}\nfoo.x = 1;\nmodule.exports = foo;\n","tokens":[{"type":"T_FUNCTION","context":"normal","value":"function","line":1,"start":0,"end":8},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":1,"start":9,"end":12},{"type":"T_LPAREN","context":"normal","value":"(","line":1,"start":12,"end":13},{"type":"T_RPAREN","context":"normal","value":")","line":1,"start":13,"end":14},{"type":"T_COLON","context":"type","value":":","line":1,"start":14,"end":15},{"type":"T_VOID_TYPE","context":"type","value":"void","line":1,"start":16,"end":20},{"type":"T_LCURLY","context":"normal","value":"{","line":1,"start":21,"end":22},{"type":"T_RCURLY","context":"normal","value":"}","line":1,"start":22,"end":23},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":2,"start":24,"end":27},{"type":"T_PERIOD","context":"normal","value":".","line":2,"start":27,"end":28},{"type":"T_IDENTIFIER","context":"normal","value":"x","line":2,"start":28,"end":29},{"type":"T_ASSIGN","context":"normal","value":"=","line":2,"start":30,"end":31},{"type":"T_NUMBER","context":"normal","value":"1","line":2,"start":32,"end":33},{"type":"T_SEMICOLON","context":"normal","value":";","line":2,"start":33,"end":34},{"type":"T_IDENTIFIER","context":"normal","value":"module","line":3,"start":35,"end":41},{"type":"T_PERIOD","context":"normal","value":".","line":3,"start":41,"end":42},{"type":"T_IDENTIFIER","context":"normal","value":"exports","line":3,"start":42,"end":49},{"type":"T_ASSIGN","context":"normal","value":"=","line":3,"start":50,"end":51},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":3,"start":52,"end":55},{"type":"T_SEMICOLON","context":"normal","value":";","line":3,"start":55,"end":56}],"errors":[]}
Exported variables with updates
The types-first signature extractor will not pick up subsequent update of an exported
let-bound variables. Consider the example
1
2
3
|
let foo: number | string = 1;
foo = "blah";
module.exports = foo;
|
{"value":"let foo: number | string = 1;\nfoo = \"blah\";\nmodule.exports = foo;\n","tokens":[{"type":"T_LET","context":"normal","value":"let","line":1,"start":0,"end":3},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":1,"start":4,"end":7},{"type":"T_COLON","context":"type","value":":","line":1,"start":7,"end":8},{"type":"T_NUMBER_TYPE","context":"type","value":"number","line":1,"start":9,"end":15},{"type":"T_BIT_OR","context":"type","value":"|","line":1,"start":16,"end":17},{"type":"T_STRING_TYPE","context":"type","value":"string","line":1,"start":18,"end":24},{"type":"T_ASSIGN","context":"normal","value":"=","line":1,"start":25,"end":26},{"type":"T_NUMBER","context":"normal","value":"1","line":1,"start":27,"end":28},{"type":"T_SEMICOLON","context":"normal","value":";","line":1,"start":28,"end":29},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":2,"start":30,"end":33},{"type":"T_ASSIGN","context":"normal","value":"=","line":2,"start":34,"end":35},{"type":"T_STRING","context":"normal","value":"\"blah\"","line":2,"start":36,"end":42},{"type":"T_SEMICOLON","context":"normal","value":";","line":2,"start":42,"end":43},{"type":"T_IDENTIFIER","context":"normal","value":"module","line":3,"start":44,"end":50},{"type":"T_PERIOD","context":"normal","value":".","line":3,"start":50,"end":51},{"type":"T_IDENTIFIER","context":"normal","value":"exports","line":3,"start":51,"end":58},{"type":"T_ASSIGN","context":"normal","value":"=","line":3,"start":59,"end":60},{"type":"T_IDENTIFIER","context":"normal","value":"foo","line":3,"start":61,"end":64},{"type":"T_SEMICOLON","context":"normal","value":";","line":3,"start":64,"end":65}],"errors":[]}
In classic mode the exported type would be string
. In types-first it will be
number | string
, so if downstream typing depends on the more precise type, then
you might get some errors.
Fix: Introduce a new variable on the update and export that one. For example
1
2
3
|
const foo1: number | string = 1;
const foo2 = "blah";
module.exports = foo2;
|
{"value":"const foo1: number | string = 1;\nconst foo2 = \"blah\";\nmodule.exports = foo2;\n","tokens":[{"type":"T_CONST","context":"normal","value":"const","line":1,"start":0,"end":5},{"type":"T_IDENTIFIER","context":"normal","value":"foo1","line":1,"start":6,"end":10},{"type":"T_COLON","context":"type","value":":","line":1,"start":10,"end":11},{"type":"T_NUMBER_TYPE","context":"type","value":"number","line":1,"start":12,"end":18},{"type":"T_BIT_OR","context":"type","value":"|","line":1,"start":19,"end":20},{"type":"T_STRING_TYPE","context":"type","value":"string","line":1,"start":21,"end":27},{"type":"T_ASSIGN","context":"normal","value":"=","line":1,"start":28,"end":29},{"type":"T_NUMBER","context":"normal","value":"1","line":1,"start":30,"end":31},{"type":"T_SEMICOLON","context":"normal","value":";","line":1,"start":31,"end":32},{"type":"T_CONST","context":"normal","value":"const","line":2,"start":33,"end":38},{"type":"T_IDENTIFIER","context":"normal","value":"foo2","line":2,"start":39,"end":43},{"type":"T_ASSIGN","context":"normal","value":"=","line":2,"start":44,"end":45},{"type":"T_STRING","context":"normal","value":"\"blah\"","line":2,"start":46,"end":52},{"type":"T_SEMICOLON","context":"normal","value":";","line":2,"start":52,"end":53},{"type":"T_IDENTIFIER","context":"normal","value":"module","line":3,"start":54,"end":60},{"type":"T_PERIOD","context":"normal","value":".","line":3,"start":60,"end":61},{"type":"T_IDENTIFIER","context":"normal","value":"exports","line":3,"start":61,"end":68},{"type":"T_ASSIGN","context":"normal","value":"=","line":3,"start":69,"end":70},{"type":"T_IDENTIFIER","context":"normal","value":"foo2","line":3,"start":71,"end":75},{"type":"T_SEMICOLON","context":"normal","value":";","line":3,"start":75,"end":76}],"errors":[]}