Read this before touching the CSS, HTML or JavaScript parser, printer or minifier.
lib/css/, lib/html/ and lib/javascript/ each hold the same five pieces:
syntax.js— a facade reachingsyntax-parser.js(reads) andsyntax-printer.js(writes) through getters, so a walk that never prints never loads the printer. It is neither parser nor printer.syntax-parser.js/syntax-printer.js— algorithm only; every lookup table they read is indata.js— except generator-written regions such as the HTML entity table (// #region html entitiesinlib/html/syntax-parser.js) — generated bytooling/generate-css-data.js,generate-html-data.jsorgenerate-js-data.js(see "Do not touch" inAGENTS.md).- A minify entry the default minimizer dispatches to:
cssMinify.js,htmlMinify.js,jsMinify.js— andxmlMinify.jsfor.svgand.xml, underexperiments.futureDefaultsor anoptimization.minimizeOptions.xmlobject. - A spec-conformance suite (
yarn test:css-parsing,test:html5lib,test:xmlconf,test:test262) and the invariants and ecosystem comparison in tooling/COMPARE_TOOLS.md.
Every visitor and node printer receives one argument, the language's path: webpack.css.syntax, webpack.html.syntax and webpack.javascript.syntax hand over an object with the same members under the same names, named after ESTree where ESTree has the concept. NodePath<TNode> in lib/util/SourceProcessor.js is that contract, and SourceProcessor takes only a path satisfying it, so a missing or misnamed member fails lint:types.
const links = [];
new SourceProcessor()
.use([NodeType.Element], (path) => {
const href = path.fieldNamed(Field.attributes, "href");
if (href !== 0) links.push([path.value(href), path.loc(href)]);
})
.process(source);| Member | Returns | CSS | HTML | JavaScript (ESTree) |
|---|---|---|---|---|
node, parent, index |
the current node, its parent (null at a root), its position |
✓ | ✓ | ✓ |
skipChildren(), stop() |
don't descend into this node; end the walk | ✓ | ✓ | ✓ |
state |
what the run was given as process(src, { state }), typed by the processor's state type |
✓ | ✓ | ✓ |
within(T) |
whether an ancestor of the current node has NodeType T |
a hash within a Declaration is a color |
a parent link walk | inside a function, a loop, … |
type(n) |
the language's NodeType member |
CSS Syntax names | DOM names, plus Attribute |
type |
range(n) |
[start, end] offsets into the parsed input, as acorn's range |
✓ | ✓ (an attribute's end past its quote) | range |
start(n), end(n) |
the same offsets as numbers, allocating nothing | ✓ | ✓ | start, end |
loc(n) |
[line, column, endLine, endColumn], lines from 1 and columns from 0 |
✓ | ✓ | loc |
source(n), source(s, e) |
the node as written; the input between two offsets | ✓ | ✓ | ✓ |
name(n) |
its name, decoded; "" without one |
at-rule, declaration, function; a dimension's unit | element, attribute, doctype, PI target | Identifier.name |
value(n) |
its value, decoded; "" without one |
token value: a number for a number, percentage or dimension; a simple block's bracket; text with escapes resolved otherwise | text, comment, PI data, attribute value | Literal.value |
childCount(n), child(i, n) |
its children, one by one, 0 past the end |
a function's or block's value, a rule's prelude | child nodes | children in source order |
children(n) |
every child as a new array, for a reader that keeps or searches them | ✓ | ✓ | ✓ |
fieldCount(F, n), field(i, F, n) |
a Field's items, one at a time, 0 past the end; a single-node field has at most one |
prelude, value, declarations, rules |
attributes, content (single) |
arguments, callee, … |
fieldNamed(F, name, n) |
the first item of a list Field whose name is name, 0 if none |
✓ | ✓ | ✓ |
flag(F, n) |
whether a Flag is set |
important; integer (a number's type flag), id (a hash's) |
mathml, svg (an element's namespace), selfClosing |
computed, optional, … |
rangeOf(P, n) |
where a Part is written, [start, end], null without one |
name, value (a url()'s contents), block |
name, value (inside the quotes), content, startTag, endTag, publicId, systemId |
— |
startOf(P, n), endOf(P, n) |
the same as numbers, -1 without one |
✓ | ✓ | — |
textOf(P, n) |
a Part's text beyond the name and value, decoded; null without one |
none | publicId, systemId |
raw, operator, kind, … |
replace(text), remove() |
while printing, the current node prints as text, or as nothing |
✓ (a declaration's ; optional) |
— | — |
Field, Flag and Part are exported beside NodeType and named with the spec's word: CSS Syntax's for CSS (a rule's declarations and rules read as empty on a block big enough to stream, so read those from the walk), the DOM's for HTML. A Flag is a bit of the node's flags column (important, an element's namespace and selfClosing) or read off a token's text (CSS's type flags).
path.fieldNamed(Field.declarations, "color"); // CSS: the declaration, 0 if none
path.flag(Flag.important); // CSS
path.field(0, Field.content); // HTML: a <template>'s fragment
for (let i = 0, n = path.fieldCount(Field.attributes); i < n; i++) {
names.push(path.name(path.field(i, Field.attributes))); // HTML: nodes the shared members read
}
path.rangeOf(Part.name); // where the name is written, null without oneThe types follow the same tables: a visitor registered for some node types gets a path whose field, fieldCount, fieldNamed, flag, rangeOf, textOf and within accept only the parts those types have, and an item read from a field is typed as its node type. So use([NodeType.Comment], (path) => path.fieldCount(Field.attributes)) fails to type-check, as does reading attributes off an attribute; a node of unknown type (parent, child(i)) takes any part.
No language has a member of its own, the writes below aside: what one grammar has is a row in its NodeType, Field, Flag or Part table, read through the members above, so a new language adds tables, not methods.
HTML's Part.content runs to where the content ends when the end tag was left out, and an element the parser inserted has no name, tag or content range.
skipChildren() still fires the node's exit, since the printer prints a node there. After stop() neither the current node's remaining visitors nor any pending exit fire. A print still writes the whole output, and a walk-only parse ends there: CSS once the top-level rule it was called in closes, HTML at once (HTML visits nothing before about 49k nodes have parsed, so stopping early in a big document still pays for that much parsing).
A run's state reaches every visitor as path.state, so visitors keep no closure of their own. Its type is the processor's: new SourceProcessor<{ count: number }>() in TypeScript, or a @type annotation in JSDoc, types path.state in every visitor and makes process take only that shape:
/** @type {import("webpack").css.syntax.SourceProcessor<{ count: number }>} */
const processor = new SourceProcessor();
processor
.use([NodeType.Ident], (path) => path.state.count++)
.process(source, { state: { count: 0 } });One visitor serves several node types through an array, resolved once in use() so the walk sees the same per-type slots: processor.use([NodeType.Text, NodeType.Comment], visitor).
Conventions every language keeps, and a new member follows:
- A node is an integer id, valid until the next parse;
0is no node, and an HTML attribute's id is negative. The path is one object rebound before each callback; read it during the callback, never keep it. - A parse entry point returns ids too, read through the exported path
Auntil the next parse (parseHtml, CSS'sparseA*, JavaScript'sparseProgram), and takes aparseError(input, code, start, end, severity)option reporting each parse error of the tokenizer and of the parser or tree construction:"error"where end of input cuts a token or rule short, or where a JavaScript one stops the parse, and"warning"otherwise; without the option, a"warning"goes unreported andparseProgramreturnsundefinedfor an"error". No spec names them all, so HTML's tree-construction errors take html5lib-python's names, JavaScript's take Babel'sreasonCodekebab-cased (read off probes bytooling/generate-js-data.js), and CSS's are webpack's own. - The node is the last argument and optional, defaulting to the current one:
path.range(),path.range(other),path.child(0). - A node has at most one
nameand onevalue; every other part is aField, aFlagor aPart(its position, and its text when it has more than the name and value), so the generic members cover any grammar instead of one method per part. nameandvalueare what the language's spec defines, decoded as ESTree'sIdentifier.nameandLiteral.valueare: escapes and character references resolved, a number as a number, a comment's text without its delimiters.sourceis exactly the input, so the spelling as written issource(path.startOf(Part.name), path.endOf(Part.name)). A node without a name or value answers"", a missing partnull.- Positions come as numbers and as ranges, like acorn's
start/endandrange:start,end,startOfandendOfallocate nothing, soCssParserandHtmlParserread every position through them;range,rangeOfandloc([line, column, endLine, endColumn]) are a fresh array per call. - Walk children one way everywhere:
for (let i = 0, c = path.child(0); c !== 0; c = path.child(++i)). HTML keeps where the lastchildread stopped per parent, so this takes one step per child even with nested loops. - Nothing is stored for a read. A member reads the columns the parser already fills or derives its answer when called: HTML counts
indexalong the sibling links and makesloc's converter on its first call in a parse. The walk writes no more than its position, so a read nobody makes costs nothing. - Writes act on the current node and only while printing.
replace(text)andremove()take effect when the node prints after its visitors, which still run its printer for what it records; on a walk that prints nothing, or on a comment, which prints from the token stream, they do nothing, and a rule whose block is large enough to stream prints as written. In CSS, what holds a replaced node prints the text its children printed as, without the minifier's rewrites, which read fields the replacement no longer matches; the rest of the sheet minifies as before. HTML streams most elements to the output before theirexitand JavaScript decides parentheses and spacing from the original node, so neither has writes yet. - Reads go through the path, and webpack's own plugins read through it.
CssParserandHtmlParseruse the members above and nothing else from a node, so what they do a plugin can; only the printers keep raw readers of their own (_rawName,_attributeList, …).
In the generator, derive — don't type out. Read tables from published datasets (mdn-data, color-name, @webref/idl) whenever derivable, including by analyzing a grammar rather than listing names: the value-definition syntax says which properties take an <integer>, so that set is computed. An existing SUPPLEMENT table counts as a source too — cosine at each eighth turn is sine two eighths along, and each inverse trig table is its forward one read back.
Per-construct behavior is a table too. Where the minifier differs per name (each math function; next, properties or at-rules), the per-name part is a descriptor in the generator and the shared part an engine in syntax-printer.js keyed by it. MATH_FUNCTION_FOLD is the example: it says how each function's arguments are read, which arithmetic runs and what unit results carry, so the printer implements none and names no function. The arithmetic is emitted alongside and bound by reference, not name, so an undefined name fails generation instead of folding nothing. A new function is one line; one whose arithmetic exists needs nothing else. A test must drive every descriptor (one input per entry) so a wrong-but-existing binding fails instead of silently declining. Hand-listing names into SUPPLEMENT is the last resort, each entry stating why it can't be derived (spec prose, an equivalence between spellings, a judgement no dataset states) — hand lists go stale unnoticed when specs move; derived ones turn a spec change into a reviewable diff.
lib/css/data.js— every table the CSS minifier looks names up in, plus the arithmetic its math-function descriptors bind to: frommdn-data+color-name(box shorthands, color-argument and math functions, named colors) and the generator'sSUPPLEMENTof spec-prose tables and math primitives, bytooling/generate-css-data.js— which also holds the value-definition-syntax parser those grammars are read with, and runs generation only as the entry point so its tests can require it.lib/html/data.js— every table the HTML parser and minifier look names up in: reflected-attribute tables from webref's HTML IDL (@webref/idl,@webref/elements) plus the generator'sSUPPLEMENTandPARSER_TABLESof §13.2 tree-construction vocabulary, bytooling/generate-html-data.js— which also emits the// #region html entitiesblock inlib/html/syntax-parser.jsfrom the vendoredtooling/html-entities.json(WHATWG's named character references).lib/javascript/data.js— the JS parser's and printer's Unicode tables: run-length identifier ranges the tokenizer decodes at its first non-ASCII code point, and per-edition\p{...}property names (reached only for a pattern the engine rejected), whichtooling/generate-js-data.jsreads from the pinned acorn devDependency — bumping it moves them; plus the Unicode 8.0 identifier ranges the printer writes unescaped (so older engines read its output), read from terser's own frozen tables; andNodeType,Field,Flag,PartandNODE_FIELDS, each ESTree node type's number, fields and their slots in aSyntaxTree, typed out in the generator.lib/javascript/parse-error-codes.js— each parse error message with its code, Babel'sreasonCodekebab-cased, read off probes both parsers read; in its own module, loaded by the first errorparseProgramreports, so a parse reporting none never loads it.lib/javascript/syntax-printer-data.js— the tables only the printer reads, in their own module so a build that parses without minifying through the printer never loads them: the native-object tables of pure globals, calls, methods and static properties the compressor reads (NATIVE_*, built only whennativeObjectTables()is called, so loading the module allocates none), each name with itsunsafe/builtins_ecma >= Nconditions in terser's order, parsed with acorn out of terser'slib/compress/native-objects.js. Plus the keyword, reserved-word and operator-precedence tables the printer names tokens by (parserTables()), parsed out of terser'slib/parse.js, and the DOM property names property mangling keeps (domProperties()), out of itstools/domprops.js. Plus the built-insimproveevaluates on literals (foldedBuiltIns()):tooling/generate-js-data.jsplaces every member@mdn/browser-compat-datalists ofMath,Number,String,Arrayand the global functions inFOLDED_BUILT_INS,FOLDED_CONSTANTSor, with the reason,UNFOLDED_BUILT_INS, and fails when a bump lists one in neither, or a folded one is missing, misplaced, or newer in Node thanengines.node(folding it would tie the output to the build's Node). Plus the constructorsimprovecalls withoutnew,CONSTRUCTED_WHEN_CALLED, each one the spec has construct the same when called. Plus the built-insimprovedrops (pureBuiltIns()): every global and standard static member BCD dates toengines.node, less the onesIMPURE_BUILT_INSandUNREAD_BUILT_INSname with the reason (eval, a promise combinator that may reject,Intl, …); a call is dropped only where running it at build time on its literal arguments does not throw.
syntax.jspublishes the parser, the printer andSourceProcessoronly — read a former re-export offparserorprinter.
syntax.jsmirrorscss, publishing the parser, the printer andSourceProcessoronly.syntax-parser.jsholds the tokenizer, §13.2 tree construction and the entity table.
parseHtml(…, { xml: true })reads XML 1.0 throughtokenizeXmland its own token handlers (XML_PARSE_CALLBACKS), so the HTML handlers carry no XML branch on their per-token path. Names are case-sensitive, there are no insertion modes, and the internal DTD subset's entities and attribute defaults are applied — an entity holding markup is tokenized in place and printed as its reference, under a nesting limit and an expansion budget.- The printer then writes XML, never HTML, and offers
renderEmbeddedSourceonly an SVG or XHTML element's<style>(of no type ortext/css) andstyle=""— the namespaces where they are CSS.xmlMinify.jshands those tobuiltinEmbeddedRendererwith the CSS options and browsers a.cssasset gets. - An SVG
data:URL a stylesheet or a document holds is minified the same way while the XML minimizer is on (svgincssMinifyandhtmlMinify):builtinEmbeddedRendereranswers it only where that is shorter, quoting a tie with'(xmlQuote) since the URL usually sits in". An HTML<svg>subtree is offered withas: "foreign-element"and declined: it is HTML's to print. One in astyle=""whose payload holds the quote the declaration list switches to is not offered yet. yarn test:xmlconfholds both halves to the W3C XML Conformance Test Suite: each document's canonical form, and each printed output reading back to it.
syntax.jsreachesparserandprinterthrough getters: parsing never loads the printer, and the printer loads the parser for itsparsephase, which reads an asset with it into aSyntaxTreethe printer adopts. It is the only parser: a source it refuses is refused, and terser's parse options for what is not a program (parse.expression,parse.strict,parse.experimental_typescript) are refused too.toEstreeObject(tree, node, fields?)builds aSyntaxTreenode's ESTree object, each call returning the same object: every field, or only those listed and the rest on a later call. It bridges hooks and plugins while the parser moves to that struct-of-arrays tree. The tree already reads as the node path does, a node passed last:type,start,end,range,name,value,field,fieldCount,flagandtextOfwith theNodeType,Field,FlagandPartthatsyntax.parserexports.SourceProcessorwalks the tree a parse writes as the CSS and HTML ones walk theirs, each visitor handed that path and the children taken in source order (a template's quasis and expressions in turn). Asked formode: "minify", the same walk prints:syntax-printer.js'sprinterreturns each node's text from its children's, asminifyprints a source it neither compresses nor mangles, comments left out and terser's format options read fromformat.minifyprints such a source with it too where no source map is asked for and not every comment is kept (whichprintTreeprints with fewer instructions), walking the tree its own parse wrote (walkSyntaxTree) with the comments it keeps, a name printed inside its parent's text where none of its own is read: each is taken as terser's printer reaches it, by the node holding its token, and printed once the text before it is, which a text opening with it leaves to the node printing that text. What terser's stream reads off the text before a node —has_parensbefore a function or object starting a statement,!--,</script— it reads off the tree instead, through two members only the JavaScript path has:tree, theSyntaxTreewalked, andancestor(level), a node enclosing the current one; and each child's text is read once (PrintContext#consume), so a long one is made flat and its pieces let go as it is printed.syntax-parser.js— tokenizer, acorn-derived core and every production in one file (any parse reaches the productions). Given asyntaxTree, the lazy path writes each node into it as it finishes, an id in the tree's columns rather than an object; the few the parser reads back by name (a key, a module name, an exported declaration, a function before its body ends) stay objects until their parent is written, and an expression read as a pattern after all is retyped in place. Only the latest edition without parser plugins takes a tree, since acorn's own productions read their nodes back.regexp.js(pattern validator) is the one on-demand piece, reached only for a pattern the host engine rejected — neverrequireit from a build path.sourceTypenames which production a source is, forparseProgram,processand the printer'sminifyalike:"module"(the default),"script", or aFunctionBodyread as acorn'scommonjsgoal reads one (returnandnew.targetat its top, noimport,exportorimport.meta):"commonjs", a CommonJS module in the function Node wraps it in, or"event-handler", an event handler attribute's value in the one HTML compiles it into, which a#!line cannot open. HTML offers only"event-handler"for an attribute, never"module".minifycompresses a function's body inside a function built around its tree, so every rule a function's body reads by applies and a name it reads undeclared stays global (for a handler, the element's, form's or document's); it prints the body without that function, mapped where it was read, its#!line and comments kept, and a handler without the last semicolon the closing brace would leave off.parseProgramandprocessread import phases (import defer,import source) too.syntax-printer.js— webpack's JavaScript minifier, terser's design ported to write byte for byte what terser writes;jsMinify.jsis theminifythe default minimizer dispatches JS to underexperiments.futureDefaults, and goes through it alone; without that flag the minimizer dispatches toterserMinifyasminimizer-webpack-pluginpublishes it, running the user's own terser since webpack lists it only as a devDependency. Neither needs an option to say which, and the printer itself never loads terser.createModulesbuilds what terser's modules exported — the node factories (createAst), the parser's tables (createParseTables), the compressor and the output stream — and each entry ofPHASESinstalls one part onto them: add phases there and nowhere else. A node is a plain object without methods, made by a factorycreateAstcompiles fromNODE_KINDSwith its fields; its prototype is its kind's, holding its ESTreetype, the values ESTree fixes for the kind (prefix, a name'srole, a declaration'skind), the kind itself, whichast.kindOfreads, and its kind bits. A kind's prototype sits below a frozen prototype per terser class, chained asNODE_KIND_ANCESTRYgives each kind's classes, andcreateAstnames a predicate per class,is<Class>(isLambdaNode), a boundisPrototypeOfof that class's prototype: the phases ask a node what it is through these, neverinstanceof. There are no node methods: every operation terser defined per class (optimize,evaluate,has_side_effects,drop_side_effect_free,size, scope analysis, …) is a function onastdispatching onnode.typethrough a table, a type absent reading the default, and walkers, transformers and the compressor are plain objects (createWalker,createTransformer,createCompressor), withast.walkNodeandast.transformNodevisiting children by type. Apure_funcsfunction is still handed a call under terser's field names (args,expression,print_to_string) through a read-only view. A parsed source is adopted from itsSyntaxTree, reading the columns rather than calling a method per field (ast.adoptTree); an ESTree, which aparse.spidermonkeyinput is, throughast.adoptEstree, andast.toEstreewrites the SpiderMonkey JSONformat.spidermonkeyhands back, as terser'sto_mozilla_astdid.correctwraps the methods with which terser writes a wrong program and changes only that output;corrections.enabledswitches it off.improvewrites less where terser's longer program provably runs the same — after compressing, a statement calling a function in place with nothing passed runs its body there, at the top level too wheretoplevelowns the names of thevars it moves there, a number, string or bigint passed for a parameter nothing writes is read in place where shorter, a small function called from several places is copied into each call whose arguments it reads in order, a function called in place with only literals passed reads them in place of its parameters, in strict code an arrow called in place to runevalis the call it makes,(a, 0) && bis(a, 0)and(a, 1) && bis(a, b)(||alike),typeofof a function or class declaration nothing writes is"function", a constant read in a template is written into its text where shorter,!of an array or object literal is!1after its effects, a binding of a function, not seen by other scripts, whose every read reads a value the writes its body runs before it leave known has each read written as that value and goes (void 0and a value much longer than the name left alone), as does a primitive read off an object declared as a literal and read only through its properties, or straight off a literal, a function called straight off an object literal is called bare where it reads nothis, aconstobject read only through its properties is split withoutreduce_vars, and a call of an exactly specified built-in on literals ("abc".charAt(1),Math.max(1, 2),parseInt("ff", 16),100 .toString(16),[1].concat([2])) is replaced by its value, an array without holes as an array literal, and aNumberconstant by its number where shorter, and an expression nobody reads that only reads a built-in global (JSON,Math.PI,Object.prototype.toString), calls one on literals without throwing (new Set,Object.keys([]),new RegExp("a", "i")) or reads a global atypeofproved declared (typeof x != "undefined" && x) is dropped, as is what runs nothing: a statement of asm.js made only of a constant or a function, a label abreakorcontinuereaching its loop orswitchanyway names, an emptyfinallywith nocatch,({} = value)of a value never nullish (leaving the value), aswitchof constant tests whose cases only declare functions Annex B copies nowhere, and the constant a folded test leaves after its effects; and a shorter spelling of the same is written:ArrayandObjectconstructed as literals (new Array(1, 2)as[1,2]),newdropped where the call constructs the same (Error,Function,RegExppassed no regular expression, …, byCONSTRUCTED_WHEN_CALLED), a spread array literal passed as its elements,typeof x == "undefined"astypeof x > "u", a+beside a number literal in arithmetic dropped,!(a == b)asa != b,!!of a boolean dropped andBoolean(x)as!!x,??after a value nevernullorundefineddropped, an integer tested against zero as the integer,===between numbers as==, an array literal or a bigint concatenated as the string it makes, a string literal's character ("abc"[1]),+[],Number,StringandBigIntof a literal andtoStringof a boolean or a string as their value,c ? a = x : a = yasa = c ? x : y(a name nowithreaches, an undeclared one in sloppy code only, a property ofthiswhere it is bound or of a name nothing reassigns),(a = b) === null || a === void 0asnull == (a = b), from ECMAScript 2020a == null ? void 0 : a.basa?.b(andnull == (a = b) ? void 0 : a.cas(a = b)?.c), and from ECMAScript 2021a || (a = b)asa ||= b(a name or a property of a name assigned,&&and??alike) anda = a || basa ||= b(avar,letor parameter, which writing back to itself shows nothing), a name in a scope awithreaches is mangled where nowithbody spells it, andconstasletwhere nothing writes the name, below the top level and where noevalorwithreaches (once names are mangled, so the letters mangling counts stay terser's);collapse_varsalso folds a value into a plain=write of a name another function declares, into a store to a property of aletorconstdeclared before it, and into what afor…inorfor…ofwhose head declares noletorconstloops over, and a barevarafter aforwith an empty head is declared in that head; a function's lastreturn c ? void a : void brunsc ? a : band falls off, a lastif (c) ethere is writtenc && e, and a statement before afor…ofstays out of its head, where the sequence would need parens; a module's imports of one module merge where they fit (import{default as a}asimport a), as do itsexport{…}lists, a quoted name another module knows written bare where it is one (import{"a"as b}), andfunction f(){}…export default f(or a class the export follows) is declared as that export; avarwithout values is declared in a latervarof its list past statements declaring nothing, or in the head of afor…inorfor…ofof the next statement assigning it, in the same function, that holds its every read; a number multiplied or masked (*,&,|,^) is written last, as an equality'snullor number is;drop_unusedalso drops from a declared or parameter pattern the names nothing reads (an array's as holes but for the last, which goes as one only where a default makes it longer, an object's where no rest follows and no getter could run: underpure_getters, or reading a literal), a declaration whose object pattern then names nothing and reads a literal (an array pattern still looks up its value's iterator), and from ECMAScript 2019 acatchbinding nothing reads — taking built-ins as present and as the language defines them, as oxc and swc do —, and from ECMAScript 2015 a compressed program whose every string reads the same as a template of its length (none a directive, a quoted property name, a module's name or attribute, a tag, afterawaitoryield, passed torequireorObject.defineProperty, which Node'scjs-module-lexerreads quoted, or holding a backtick,${or both quotes) writes them all with backticks, which deflate codes shorter than a double quote, underquote_style: 0with semicolons, a mix measuring bigger (improvements.backticks.enabledswitches that one off); andimprovements.enabledswitches it off. Theparsephase builds the tree fromsyntax-parser.js;javascript/printer.unittest.jsholds the adopted tree to terser's own conversion, andPARSED_DIFFERENTLYinjavascript/printer.unittest.jslists where the tree it builds prints otherwise than terser's.test/specCases/minify-corpora.spectest.jsholds the whole printer to terser's output, a refusal matching any refusal, over each corpus in itsCORPORAlist (add a new one there) under the option sets inOPTION_SETS, and runs each output whose test states what its input prints (terser'sexpect_stdout, swc'sexpected.stdoutandexec.rs) in terser's sandbox. A byte difference is accepted only where switching thecorrectandimprovephases off gives terser's bytes back, and fails whereimprovewrites more than without it, unless the program is shorter and onlyconstwritten asletmakes it gzip bigger (improvements.lets.enabledswitches that one off to tell); what it fixes is listed inCORRECTED(printing right with the phase, as terser without), and an output misprinting as terser's by an option's own contract inINHERITED, each with the reason — an entry that stops matching fails until retired. Where the reference instead refuses a source the spec allows, the parse error it answers with is listed inREFUSED_BY_REFERENCEwith what makes the source legal; where webpack refuses a source the reference reads, the source is listed inREFUSED_BY_WEBPACKwith the rule it breaks (a test262 test expecting a parse error needs no entry); where it reads or prints one differently, the case is listed inREFERENCE_MISPRINTSwith what it gets wrong — a listed one webpack must still run correctly, and one the reference stops differing on fails until retired. Each output is also sized against swc's recorded one, a fixture'soutput.jsor, for one of terser's cases, what swc's port of it records (gzip decides, raw breaks a tie): one bigger fails unlesstest/helpers/swcSmaller.jslists it under what swc does that webpack does not yet, and a listed one no longer bigger fails until retired. Under the default minimizer's options each source of every corpus but test262 is also minified byoxc-minify, held the same way totest/helpers/oxcSmaller.js; an oxc output that misprints is no lead.JS_MINIFY_REPORT=<file>tallies where webpack's is worse, the same and better. terser's corpus submodule is pinned to theterserdevDependency — bump both together, and regeneratedata.js.- Printing and mangling read the compressed tree as it is held, by its ESTree types: no conversion step stands between the compressor and the printer, so a tree a caller passes in or
format.asthands back is printed and mangled as it is. Where the compressor holds a shape of its own the printer reads it directly — a template'ssegments, two side by side printing as one text; an import'simported_nameandimported_names, an export'sexported_definition,exported_valueandexported_names, attributes as the object holding them;import()'sargs; a key's or a property read's name as a symbol node; a literal'srolefor a regular expression or a bigint's digits — and a name prints as itsdefinitionnames it (mangled_name, else the definition's own name), terser's or the mangler's alike, never renamed in place. Where nobody keeps the tree,analyzeManglingScopesandprintProgramdrop what only the compressor read (COMPRESSOR_ONLY_FIELDS) off each node they read; every field written is one its kind's factory made. TheestreeManglephase mangles that tree as terser's mangler did:analyzeManglingScopesresolves its scopes, writing each name'sdefinition,computeEstreeCharFrequencyorders the identifiers,mangleManglingScopesgives the definitions theirmangled_nameandrenameMangledEstreerenames#privatenames and properties in one walk, skipped where there are neither.printEstree(theestreePrintphase) dispatches onnode.typethrough three tables —codegen(the text),needsParensandsourceMapping— and asks about a parent through thekindOfpredicates, neverinstanceof; the stream (MinifiedOutput) reads comments throughNodeAccess. Everything that prints goes through it:minify, the mangler's character frequency and the compressor'sprintToString. yarn test:minify-corpora— webpack's JS minifier vs the published one it replaces, byte for byte (a refusal matching any refusal), over every JS corpus: terser's owntest/compressandtest/input, test262, swc's minifier tests and oxc's; where a test states what its input prints, the output is also run and must print it. Each output must also be no bigger than swc's recorded one unlessSWC_SMALLERlists why, and every output outside test262 no bigger thanoxc-minify's unlessOXC_SMALLERdoes;JS_MINIFY_REPORT=<file>tallies worse, the same and better.