npm install js-tokens
99.6
Supply Chain Risk
70.2
Quality
78.4
Maintenance
100
Vulnerability
502 Stars
146 Commits
31 Forks
7 Watching
1 Branches
3 Contributors
Updated on 05 Sept 2024
Minified
Minified + Gzipped
JavaScript (79.79%)
CoffeeScript (15.42%)
TypeScript (4.79%)
Cumulative downloads
Total Downloads
Last day
1.4%
9,240,283
Compared to previous day
Last week
-1.2%
47,830,692
Compared to previous week
Last month
-2.4%
205,679,900
Compared to previous month
Last year
14.3%
2,310,511,640
Compared to previous year
The tiny, regex powered, lenient, almost spec-compliant JavaScript tokenizer that never fails.
const jsTokens = require("js-tokens"); const jsString = 'JSON.stringify({k:3.14**2}, null /*replacer*/, "\\t")'; Array.from(jsTokens(jsString), (token) => token.value).join("|"); // JSON|.|stringify|(|{|k|:|3.14|**|2|}|,| |null| |/*replacer*/|,| |"\t"|)
npm install js-tokens
import jsTokens from "js-tokens"; // or: const jsTokens = require("js-tokens");
jsTokens(string, options?)
Option | Type | Default | Description |
---|---|---|---|
jsx | boolean | false | Enable JSX support. |
This package exports a generator function, jsTokens
, that turns a string of JavaScript code into token objects.
For the empty string, the function yields nothing (which can be turned into an empty list). For any other input, the function always yields something, even for invalid JavaScript, and never throws. Concatenating the token values reproduces the input.
The package is very close to being fully spec compliant (it passes all but 3 of test262-parser-tests), but has taken a couple of shortcuts. See the following sections for limitations of some tokens.
// Loop over tokens: for (const token of jsTokens("hello, !world")) { console.log(token); } // Get all tokens as an array: const tokens = Array.from(jsTokens("hello, !world"));
Spec: ECMAScript Language: Lexical Grammar + Additional Syntax
export default function jsTokens(input: string): Iterable<Token>; type Token = | { type: "StringLiteral"; value: string; closed: boolean } | { type: "NoSubstitutionTemplate"; value: string; closed: boolean } | { type: "TemplateHead"; value: string } | { type: "TemplateMiddle"; value: string } | { type: "TemplateTail"; value: string; closed: boolean } | { type: "RegularExpressionLiteral"; value: string; closed: boolean } | { type: "MultiLineComment"; value: string; closed: boolean } | { type: "SingleLineComment"; value: string } | { type: "HashbangComment"; value: string } | { type: "IdentifierName"; value: string } | { type: "PrivateIdentifier"; value: string } | { type: "NumericLiteral"; value: string } | { type: "Punctuator"; value: string } | { type: "WhiteSpace"; value: string } | { type: "LineTerminatorSequence"; value: string } | { type: "Invalid"; value: string };
Spec: StringLiteral
If the ending "
or '
is missing, the token has closed: false
. JavaScript strings cannot contain (unescaped) newlines, so unclosed strings simply end at the end of the line.
Escape sequences are supported, but may be invalid. For example, "\u"
is matched as a StringLiteral even though it contains an invalid escape.
Examples:
"string" 'string' "" '' "\"" '\'' "valid: \u00a0, invalid: \u" 'valid: \u00a0, invalid: \u' "multi-\ line" 'multi-\ line' " unclosed ' unclosed
Spec: NoSubstitutionTemplate / TemplateHead / TemplateMiddle / TemplateTail
A template without interpolations is matched as is. For, example:
`abc`
: NoSubstitutionTemplate`abc
: NoSubstitutionTemplate with closed: false
A template with interpolations is matched as many tokens. For example, `head${1}middle${2}tail`
is matched as follows (apart from the two NumericLiterals):
`head${
: TemplateHead}middle${
: TemplateMiddle}tail`
: TemplateTailTemplateMiddle is optional, and TemplateTail can be unclosed. For example, `head${1}tail
(note the missing ending `
):
`head${
: TemplateHead}tail
: TemplateTail with closed: false
Templates can contain unescaped newlines, so unclosed templates go on to the end of input.
Just like for StringLiteral, templates can also contain invalid escapes. `\u`
is matched as a NoSubstitutionTemplate even though it contains an invalid escape. Also note that in tagged templates, invalid escapes are not syntax errors: x`\u`
is syntactically valid JavaScript.
Spec: RegularExpressionLiteral
Regex literals may contain invalid regex syntax. They are still matched as regex literals. The specification even says:
The productions below [...] are used by the input element scanner to find the end of the regular expression literal. The source text [...] are subsequently parsed again using the more stringent ECMAScript Regular Expression grammar.
If the ending /
is missing, the token has closed: false
. JavaScript regex literals cannot contain newlines (not even escaped ones), so unclosed regex literals simply end at the end of the line.
According to the specification, the flags of regular expressions are IdentifierParts (unknown and repeated regex flags become errors at a later stage).
Differentiating between regex and division in JavaScript is really tricky. js-tokens looks at the previous token to tell them apart. As long as the previous tokens are valid, it should do the right thing. For invalid code, js-tokens might be confused and start matching division as regex or vice versa.
Examples:
/a/ /a/gimsuy /a/Inva1id /+/ /[/]\// /[\p{Decimal_Number}--[0-9]]/v
Spec: MultiLineComment
If the ending */
is missing, the token has closed: false
. Unclosed multi-line comments go on to the end of the input.
Examples:
/* comment */ /* console.log( "commented", out + code); */ /**/ /* unclosed
Spec: SingleLineComment
Examples:
// comment // console.log("commented", out + code); //
Spec: HashbangComment
Note that a HashbangComment can only occur at the very start of the string that is being tokenized. Anywhere else you will likely get an Invalid token #
followed by a Punctuator token !
.
Examples:
#!/usr/bin/env node #! console.log("commented", out + code); #!
Spec: IdentifierName
Keywords, reserved words, null
, true
, false
, variable names and property names.
Examples:
if for var instanceof package null true false Infinity undefined NaN $variab1e_name π ℮ ಠ_ಠ \u006C\u006F\u006C\u0077\u0061\u0074
Spec: PrivateIdentifier
Any IdentifierName
preceded by a #
.
Examples:
#if #for #var #instanceof #package #null #true #false #Infinity #undefined #NaN #$variab1e_name #π #℮ #ಠ_ಠ #\u006C\u006F\u006C\u0077\u0061\u0074
Spec: NumericLiteral
Examples:
0 1.5 1 1_000 12e9 0.123e-32 0xDead_beef 0b110 12n 07 09.5
Spec: Punctuator + DivPunctuator + RightBracePunctuator
All possible values:
-- ++ . ?. < <= > >= != !== == === + - % & | ^ / * ** << >> >>> && || ?? = += -= %= &= |= ^= /= *= **= <<= >>= >>>= &&= ||= ??= ( ) [ ] { } ! ? : ; , ~ ... =>
Spec: WhiteSpace
Unlike the specification, multiple whitespace characters in a row are matched as one token, not one token per character.
Spec: LineTerminatorSequence
CR, LF and CRLF, plus \u2028
and \u2029
.
Spec: n/a
Single code points not matched in another token.
Examples:
# @ 💩
Spec: JSX Specification
export default function jsTokens( input: string, options: { jsx: true }, ): Iterable<Token | JSXToken>; export declare type JSXToken = | { type: "JSXString"; value: string; closed: boolean } | { type: "JSXText"; value: string } | { type: "JSXIdentifier"; value: string } | { type: "JSXPunctuator"; value: string } | { type: "JSXInvalid"; value: string };
Token
and runs of JSXToken
.JSXToken
can also contain WhiteSpace, LineTerminatorSequence, MultiLineComment and SingleLineComment.Spec: "
JSXDoubleStringCharacters "
+ '
JSXSingleStringCharacters '
If the ending "
or '
is missing, the token has closed: false
. JSX strings can contain unescaped newlines, so unclosed JSX strings go on to the end of input.
Note that JSX don’t support escape sequences as part of the token grammar. A "
or '
always closes the string, even with a backslash before.
Examples:
"string"
'string'
""
''
"\"
'\'
"multi-
line"
'multi-
line'
" unclosed
' unclosed
Spec: JSXText
Anything but <
, >
, {
and }
.
Spec: JSXIdentifier
Examples:
div class xml x-element x------ $htm1_element ಠ_ಠ
Spec: n/a
All possible values:
< > / . : = { }
Spec: n/a
Single code points not matched in another token.
Examples in JSX tags:
1 ` + , # @ 💩
All possible values in JSX children:
> }
The intention is to always support the latest ECMAScript version whose feature set has been finalized.
Currently, ECMAScript 2024 is supported.
Section B: Additional ECMAScript Features for Web Browsers of the spec is optional if the ECMAScript host is not a web browser, and specifies some additional syntax. Section C: The Strict Mode of ECMAScript disallows certain syntax in Strict Mode.
5<!--x
as 5 < !(--x)
rather than as 5 //x
./
and ending /
, so this is supported.Supporting TypeScript is not an explicit goal, but js-tokens and Babel both tokenize this TypeScript fixture and this TSX fixture the same way, with one edge case:
type A = Array<Array<string>> type B = Array<Array<Array<string>>>
Both lines above should end with a couple of >
tokens, but js-tokens instead matches the >>
and >>>
operators.
JSX is supported: jsTokens("<p>Hello, world!</p>", { jsx: true })
.
js-tokens should work in any JavaScript runtime that supports Unicode property escapes.
Here are a couple of tricky cases:
// Case 1: switch (x) { case x: {}/a/g; case x: {}<div>x</div>/g; } // Case 2: label: {}/a/g; label: {}<div>x</div>/g; // Case 3: (function f() {}/a/g); (function f() {}<div>x</div>/g);
This is what they mean:
// Case 1: switch (x) { case x: { } /a/g; case x: { } <div>x</div> / g; } // Case 2: label: { } /a/g; label: { } <div>x</div> / g; // Case 3: (function f() {}) / a / g; (function f() {}) < div > x < /div>/g;
But js-tokens thinks they mean:
// Case 1: switch (x) { case x: ({}) / a / g; case x: ({}) < div > x < /div>/g; } // Case 2: label: ({}) / a / g; label: ({}) < div > x < /div>/g; // Case 3: function f() {} /a/g; function f() {} <div>x</div> / g;
In other words, js-tokens:
This happens because js-tokens looks at the previous token when deciding between regex and division or JSX and comparison. In these cases, the previous token is }
, which either means “end of block” (→ regex/JSX) or “end of object literal” (→ division/comparison). How does js-tokens determine if the }
belongs to a block or an object literal? By looking at the token before the matching {
.
In case 1 and 2, that’s a :
. A :
usually means that we have an object literal or ternary:
let some = weird ? { value: {}/a/g } : {}/a/g;
But :
is also used for case
and labeled statements.
One idea is to look for case
before the :
as an exception to the rule, but it’s not so easy:
switch (x) { case weird ? true : {}/a/g: {}/a/g }
The first {}/a/g
is a division, while the second {}/a/g
is an empty block followed by a regex. Both are preceded by a colon with a case
on the same line, and it does not seem like you can distinguish between the two without implementing a parser.
Labeled statements are similarly difficult, since they are so similar to object literals:
{ label: {}/a/g } ({ key: {}/a/g })
Finally, case 3 ((function f() {}/a/g);
) is also difficult, because a )
before a {
means that the {
is part of a block, and blocks are usually statements:
if (x) { } /a/g; function f() {} /a/g;
But function expressions are of course not statements. It’s difficult to tell an function expression from a function statement without parsing.
Luckily, none of these edge cases are likely to occur in real code.
js-tokens advertises that it “never fails”. Tell you what, it can fail on extreme inputs. The regex engine of the runtime can eventually give up. js-tokens has worked around it to some extent by changing its regexes to be easier on the regex engine. To solve completely, js-tokens would have to stop using regex, but then it wouldn’t be tiny anymore which is the whole point. Luckily, only extreme inputs can fail, hopefully ones you’ll never encounter.
For example, if you try to parse the string literal "\n\n\n"
but with 10 million \n
instead of just 3, the regex engine gives up with RangeError: Maximum call stack size exceeded
(or similar). Try it out:
Array.from(require("js-tokens")(`"${"\\n".repeat(1e7)}"`));
(Yes, that is the regex engine of the runtime giving up. js-tokens has no recursive functions.)
However, if you repeat a
instead of \n
10 million times ("aaaaaa…"
), it works:
Array.from(require("js-tokens")(`"${"a".repeat(1e7)}"`));
That’s good, because it’s much more common to have lots of non-escapes in a row in a big string literal, than having mostly escapes. (Obfuscated code might have only escapes though.)
I’ve seen Safari give up instead of throwing an error.
In Safari, Chrome, Firefox and Node.js the following code successfully results in a match:
/(#)(?:a|b)+/.exec("#" + "a".repeat(1e5));
But for the following code (with 1e7
instead of 1e5
), the runtimes differ:
/(#)(?:a|b)+/.exec("#" + "a".repeat(1e7));
RangeError: Maximum call stack size exceeded
(or similar).null
(at the time of writing), silently giving up on matching the regex. It’s kind of lying that the regex did not match, while in reality it would given enough computing resources.This means that in Safari, js-tokens might not fail but instead give you unexpected tokens.
With @babel/parser for comparison. Node.js 21.6.1 on a MacBook Pro M1 (Sonoma).
Lines of code | Size | js-tokens@8.0.3 | @babel/parser@7.23.9 |
---|---|---|---|
~100 | ~4.0 KiB | ~2 ms | ~10 ms |
~1 000 | ~39 KiB | ~5 ms | ~27 ms |
~10 000 | ~353 KiB | ~44 ms | ~108 ms |
~100 000 | ~5.1 MiB | ~333 ms | ~2.0 s |
~2 400 000 | ~138 MiB | ~7 s | ~4 m 9 s (*) |
(*) Required increasing the Node.js the memory limit (I set it to 8 GiB).
See benchmark.js if you want to run benchmarks yourself.
Reason
license file detected
Details
Reason
no dangerous workflow patterns detected
Reason
no binaries found in the repo
Reason
1 existing vulnerabilities detected
Details
Reason
3 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 2
Reason
dependency not pinned by hash detected -- score normalized to 2
Details
Reason
Found 1/20 approved changesets -- score normalized to 0
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
detected GitHub workflow tokens with excessive permissions
Details
Reason
branch protection not enabled on development/release branches
Details
Reason
security policy file not detected
Details
Reason
project is not fuzzed
Details
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
Score
Last Scanned on 2024-09-02
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn Moreloose-envify
Fast (and loose) selective `process.env` replacer using js-tokens instead of an AST
opensea-js
TypeScript SDK for the OpenSea marketplace helps developers build new experiences using NFTs and our marketplace data
@axa-ch/design-tokens
AXA Design Tokens
@cobalt-ui/plugin-js
Generate JS, TS, and JSON from your design tokens schema (requires @cobalt-ui/cli)