Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 | 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 3x 3x 3x 3x 3x 3x 3x 3x 3x 279x 279x 279x 279x 279x 279x 279x 91113x 91113x 91113x 165054474x 165054474x 91113x 91113x 279x 279x 279x 279x 279x 279x 279x 42271x 42271x 42271x 42271x 42271x 279x 279x 279x 279x 279x 279x 32223x 32223x 32223x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 6644x 6644x 4x 4x 6644x 55496x 22080x 55496x 100x 100x 100x 100x 55496x 6640x 6644x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 198x 198x 198x 2535x 2535x 2535x 198x 471x 196x 196x 2535x 2x 198x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 25440x 25440x 25440x 25440x 25440x 25440x 25440x 25440x 25440x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 31899x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 6634x 42274x 42274x 42274x 42274x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 42273x 42273x 42273x 42273x 42274x 15343x 15343x 15343x 15343x 15343x 148x 148x 148x 15343x 15195x 15195x 15195x 15195x 15195x 227x 1x 227x 226x 226x 226x 226x 226x 227x 15194x 15195x 25390x 99x 98x 98x 98x 98x 98x 1x 1x 1x 1x 1x 1x 1x 25390x 99x 25390x 25192x 25192x 25390x 15192x 15195x 14870x 14870x 14870x 14870x 14870x 15192x 15192x 42274x 22076x 22076x 22076x 99x 99x 99x 99x 99x 22076x 21977x 21977x 22076x 22076x 22076x 26930x 190x 190x 18x 18x 188x 172x 172x 172x 172x 4854x 291x 679x 679x 291x 4664x 4274x 9997x 9997x 8564x 9997x 9997x 9997x 4274x 4373x 99x 99x 98x 98x 98x 390x 390x 390x 98x 99x 99x 42270x 42270x 42270x 42270x 6631x 6631x 6634x 6380x 6380x 6631x 6634x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 100x 100x 100x 2x 2x 2x 2x 2x 2x 2x 2x 98x 98x 100x 279x 279x 279x 279x 279x 279x 279x 279x 279x 4x 4x 4x 2x 2x 2x 4x 4x 4x 4x 4x 4x 4x 4x 4x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 108x 108x 108x 108x 108x 108x 108x 108x 108x 108x 108x 108x 108x 108x 108x 108x 108x 108x 6643x 6643x 99x 99x 99x 99x 6643x 108x 108x 108x 108x 108x 108x 108x 108x 108x 108x 98x 98x 98x 98x 98x 98x 98x 98x 98x 108x 108x 108x 108x 108x 108x 108x 40464x 108x 108x 108x 108x 108x 108x 108x 99x 99x 108x 108x 108x 108x 108x 108x 6638x 6638x 6638x 6638x 6638x 6638x 6638x 6638x 6638x 108x 108x 3x 3x 3x 3x 3x 3x 3x 3x 105x 107x 97x 97x 105x 108x 6571x 6571x 6571x 6571x 6571x 6571x 6571x 6571x 6571x 6571x 6571x 97x 97x 6571x 6571x 6571x 6571x 6571x 6571x 6571x 6571x 105x 105x 105x 105x 105x 105x 105x 105x 105x 105x 105x 105x 105x 105x 108x 105x 105x 105x 108x 108x 108x 108x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 279x 9x 9x 9x 9x 9x 9x 634x 634x 634x 634x 634x 634x 634x 634x 9x 9x 9x 9x 790948x 790948x 790948x 791680x 791680x 791680x 791680x 790948x 790948x 9x 9x 9x 9x 9x 9x 9x 9x 634x 634x 197737x 197737x 200958x 200958x 197737x 197737x 197737x 197737x 197737x 634x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x | /**
* @file emit.js
* @description Renders a linked module graph into one classic script.
*
* ## The output shape, and why
*
* One outer IIFE. Inside it, each module is an inner IIFE assigned to a `var`,
* emitted in dependency order:
*
* ```js
* (function () {
* 'use strict';
* var __avx3 = (function () { var __x = {}; ...body...; return __x; })();
* var __avx7 = (function () { var { thing } = __avx3; ...body...; })();
* })();
* ```
*
* `var` rather than `const` is load-bearing: `var` hoists to the top of the
* outer function, so a module emitted *before* its dependency — which happens
* exactly once per cycle — can still name it. The binding is `undefined` at
* that moment and holds the exports object by the time anything dereferences
* it, which is the same shape the language itself has across a cycle.
*
* ## Three problems this file has to get right
*
* **Live bindings.** `reactive/watcher.js` has `export let activeWatcher`, it
* reassigns it, and `runtime/AvenxComponent.js` reads it across the module
* boundary to decide whether it is inside its own render. Copying the value at
* import time would capture `null` forever and break that check silently. So a
* mutable exported binding is *hoisted out of its module* into bundle scope
* under its own name: the exporter's assignments and every importer's reads
* then resolve to one variable through the ordinary scope chain, with no
* identifier rewriting anywhere. Exports are exposed as getters for the same
* reason, so a namespace or a re-export barrel sees the current value.
*
* **Cycles.** Across the one edge that closes a cycle, the dependency's
* exports object does not exist yet. ES modules make exactly one guarantee
* there — function declarations hoist — so this emitter reproduces exactly
* that: a cyclic import of a function becomes a forwarder that dereferences on
* call, and a cyclic import of anything else is a build error, because in a
* browser it would be a temporal dead zone.
*
* **Line fidelity.** Every rewritten declaration is emitted on a single line
* and padded back to the line count it replaced, so a module's body keeps a
* 1:1 line correspondence with its source. That is what makes the source map
* exact and a production stack trace point at a line the developer wrote.
* @module lib/bundler/emit
*/
import path from 'path';
/**
* Raised when a graph cannot be rendered into a correct bundle.
*/
export class EmitError extends Error {
/**
* @param {string} message - What cannot be emitted, and why.
* @param {string} [file] - The module responsible.
* @param {'cycle'|'live-binding-alias'|'live-binding-collision'} [kind] - Which
* condition was hit, so a caller can map it onto its own diagnostics
* without matching on the message text.
*/
constructor(message, file = '', kind = 'cycle') {
super(message);
this.name = 'EmitError';
/** @type {string} */
this.file = file;
/** @type {string} */
this.kind = kind;
/** @type {Array<string[]>} */
this.cycles = [];
}
}
/**
* Counts the lines a chunk of text occupies.
* @param {string} text - The text.
* @returns {number} Line count, minimum one.
*/
function lineCount(text) {
let lines = 1;
for (let i = 0; i < text.length; i += 1) {
if (text[i] === '\n') lines += 1;
}
return lines;
}
/**
* Pads generated text so it occupies the same number of lines it replaced.
* @param {string} generated - Replacement text, expected to be one line.
* @param {string} original - The text being replaced.
* @returns {string} The replacement, newline-padded.
*/
function padToLines(generated, original) {
const want = lineCount(original);
const have = lineCount(generated);
return have >= want ? generated : generated + '\n'.repeat(want - have);
}
/**
* A valid JavaScript identifier for a module's exports object.
* @param {number} index - The module's index in emission order.
* @returns {string} The binding name.
*/
function moduleBinding(index) {
return `__avx${index}`;
}
/**
* Finds the exported bindings a module can reassign.
*
* Only `let` and `var` declarations qualify. A `const`, a `function` and a
* `class` binding cannot be reassigned, so copying their value at import time
* is indistinguishable from a live binding and costs nothing.
* @param {object} module - A graph module.
* @returns {Set<string>} Names that must live at bundle scope.
*/
export function mutableExports(module) {
const names = new Set();
if (module.format !== 'esm') {
return names;
}
for (const statement of module.record.statements) {
if (statement.kind !== 'export-declaration') continue;
const text = module.source.slice(statement.bodyStart, statement.end).trimStart();
if (/^(let|var)\b/.test(text)) {
for (const name of statement.declares) {
names.add(name);
}
}
}
return names;
}
/**
* The local name under which a module declares an exported function.
*
* This is the question that decides whether a cyclic import is legal, so it is
* answered from the declaration rather than guessed from usage: only a function
* declaration is initialised before any module body runs, which is the one
* guarantee ES modules make across a cycle.
* @param {object} module - The exporting graph module.
* @param {string} exported - The exported name.
* @returns {string|null} The local declaration name, or null when the export is
* not a function declaration.
*/
function functionExportLocal(module, exported) {
if (!module || module.format !== 'esm') {
return null;
}
for (const statement of module.record.statements) {
const isDefault = statement.kind === 'export-default';
if (statement.kind !== 'export-declaration' && !isDefault) continue;
if (isDefault ? exported !== 'default' : !statement.declares.includes(exported)) continue;
const text = module.source.slice(statement.bodyStart, statement.end).trimStart();
if (/^(async\s+)?function\b/.test(text)) {
return isDefault ? statement.declares[0] || null : exported;
}
}
return null;
}
/**
* Renders one module's body with its declarations rewritten.
* @param {object} context - Emission context.
* @param {object} context.module - The graph module.
* @param {object} context.graph - The module graph.
* @param {Map<string, number>} context.indexOf - Module id to emission index.
* @param {Set<string>} context.hoisted - Names living at bundle scope.
* @param {Set<string>} context.included - Module ids that reached the bundle.
* @param {function(string, string): string} context.slotFor - Allocates a
* bundle-scope slot for a function crossing a cycle.
* @returns {string} The transformed module body.
* @throws {EmitError} When a construct cannot be rendered correctly.
*/
function renderEsmBody({ module, graph, indexOf, hoisted, included, slotFor }) {
const source = module.source;
const pieces = [];
const trailing = [];
let cursor = 0;
/**
* The bundle-scope binding for a specifier this module imports.
* @param {string} specifier - The specifier as written.
* @returns {{name: string, target: object, cyclic: boolean}} Binding details.
*/
const targetOf = (specifier) => {
const id = module.resolved.get(specifier);
const target = graph.modules.get(id);
return {
name: moduleBinding(indexOf.get(id)),
target,
id,
cyclic: graph.isCyclicEdge(module.id, id),
included: included.has(id),
};
};
/**
* Declares one export on the module's exports object, live.
* @param {string} exported - The exported name.
* @param {string} expression - An expression yielding the current value.
*/
const declareExport = (exported, expression) => {
trailing.push(`__avx_def(__x, ${JSON.stringify(exported)}, function () { return ${expression}; });`);
};
// Statement rewrites and dynamic-import rewrites are one ordered list. A
// dynamic import lives inside an ordinary statement, which is copied
// verbatim, so the two never overlap -- but they have to be applied in
// source order for the cursor to stay monotonic.
const edits = [
...module.record.statements
.filter((statement) => statement.kind !== 'statement')
.map((statement) => ({ start: statement.start, end: statement.end, statement })),
...module.record.dynamicImports.map((entry) => ({ start: entry.start, end: entry.end, dynamic: entry })),
].sort((a, b) => a.start - b.start);
for (const edit of edits) {
pieces.push(source.slice(cursor, edit.start));
const original = source.slice(edit.start, edit.end);
if (edit.dynamic) {
const { name, target, included: present } = targetOf(edit.dynamic.specifier);
// Everything is in one chunk, so the module is already evaluated by the
// time anything can await it. `import()` therefore resolves immediately
// with the namespace, which is the correct observable behaviour for an
// unsplit build -- what is missing is a separate chunk, not the semantics.
const namespace = target && target.format === 'cjs' ? `{ default: ${name} }` : name;
pieces.push(padToLines(present ? `Promise.resolve(${namespace})` : 'Promise.resolve({})', original));
cursor = edit.end;
continue;
}
const statement = edit.statement;
let replacement;
if (statement.kind === 'import') {
const entry = module.record.imports.find((item) => item.start === statement.start);
const { name, target, cyclic, included: present } = targetOf(entry.specifier);
if (!present) {
// The dependency was shaken out because nothing this module keeps uses
// it. Its bindings are unreachable by construction, so there is
// nothing to declare.
replacement = '';
} else if (entry.sideEffectOnly) {
// Ordering already guarantees the module ran; naming it keeps the
// dependency visible in the output.
replacement = `/* side-effect import: ${entry.specifier} */ void ${name};`;
} else {
const parts = [];
const plain = [];
if (entry.namespace) {
parts.push(`var ${entry.namespace} = ${name};`);
}
if (entry.defaultLocal) {
if (cyclic) {
parts.push(cyclicBinding(entry.defaultLocal, 'default', target, module, slotFor));
} else {
// A CommonJS module's default export is its `module.exports`
// object itself, which is what `__avxN` already holds.
const value = target && target.format === 'cjs' ? name : `${name}.default`;
parts.push(`var ${entry.defaultLocal} = ${value};`);
}
}
for (const binding of entry.bindings) {
if (hoisted.has(binding.imported)) {
if (binding.imported === binding.local) {
// A live binding lives at bundle scope under this exact name, so
// the reference resolves through the scope chain and stays live.
// Declaring anything here would shadow it with a stale copy.
continue;
}
throw new EmitError(
`"${binding.imported}" is a live binding and cannot be imported under the alias "${binding.local}". ` +
'Import it under its own name so it keeps resolving to the one variable that holds it.',
module.id,
'live-binding-alias',
);
}
if (cyclic) {
parts.push(cyclicBinding(binding.local, binding.imported, target, module, slotFor));
} else {
plain.push(binding);
}
}
if (plain.length > 0) {
const pattern = plain
.map((binding) => (binding.imported === binding.local ? binding.local : `${binding.imported}: ${binding.local}`))
.join(', ');
parts.push(`var { ${pattern} } = ${name};`);
}
replacement = parts.join(' ');
}
} else if (statement.kind === 'export-declaration') {
const body = source.slice(statement.bodyStart, statement.end);
const isMutable = /^\s*(let|var)\b/.test(body);
if (isMutable) {
// Hoisted to bundle scope: drop the declaration keyword so the
// assignment targets the outer variable rather than a module-local
// shadow of it.
replacement = body.replace(/^(\s*)(let|var)\s+/, '$1');
if (!/=/.test(replacement)) {
replacement = '';
}
} else {
replacement = body;
}
for (const name of statement.declares) {
declareExport(name, name);
}
} else if (statement.kind === 'export-default') {
const body = source.slice(statement.bodyStart, statement.end);
if (statement.declares.length > 0) {
replacement = body;
declareExport('default', statement.declares[0]);
} else {
const expression = body.replace(/;\s*$/, '');
replacement = `var __avx_default = ${expression};`;
declareExport('default', '__avx_default');
}
} else if (statement.kind === 'export-list') {
for (const entry of statement.entries || []) {
declareExport(entry.exported, entry.local);
}
replacement = '';
} else if (statement.kind === 'reexport') {
for (const entry of statement.reExportEntries || []) {
const { name, included: present } = targetOf(entry.specifier);
if (!present) continue;
// `export * as ns from 'm'` re-exports the namespace object itself.
const value = entry.imported === '*' ? name : `${name}[${JSON.stringify(entry.imported)}]`;
declareExport(entry.exported, value);
}
replacement = '';
} else if (statement.kind === 'star-reexport') {
const { name, target, included: present } = targetOf(statement.starSpecifier);
if (present && target) {
// Expanded to explicit getters rather than a runtime copy loop: the
// names are known here, and a copy would freeze a live binding.
for (const exported of target.exportNames) {
if (exported === 'default' || exported === '*') continue;
declareExport(exported, `${name}[${JSON.stringify(exported)}]`);
}
}
replacement = '';
} else {
replacement = original;
}
pieces.push(padToLines(replacement, original));
cursor = statement.end;
}
pieces.push(source.slice(cursor));
if (trailing.length > 0) {
pieces.push(`\n${trailing.join('\n')}\n`);
}
return pieces.join('');
}
/**
* Builds a binding for an import that crosses a cycle.
*
* The dependency's *exports object* does not exist yet at this point — the
* importing module is running inside the call that will produce it — so the
* forwarder cannot go through it. It goes through a bundle-scope slot instead,
* which the exporting module fills from its hoisted function declaration before
* its own body runs. That is exactly the order ES modules use, so a cyclic call
* that works in a browser works here and one that does not, does not.
* @param {string} local - The local name to declare.
* @param {string} imported - The name being imported.
* @param {object} target - The exporting graph module.
* @param {object} importer - The importing graph module.
* @param {function(string, string): string} slotFor - Allocates the shared slot.
* @returns {string} The declaration.
* @throws {EmitError} When the binding cannot legally cross a cycle.
*/
function cyclicBinding(local, imported, target, importer, slotFor) {
const declared = functionExportLocal(target, imported);
if (!declared) {
throw new EmitError(
`"${imported}" is imported across a module cycle but is not a function declaration.\n` +
'Only function declarations are initialised before a module body runs, so reading anything ' +
'else across a cycle is a temporal dead zone in a browser too. Break the cycle, or move the ' +
'value behind a function.',
importer.id,
);
}
const slot = slotFor(target.id, imported);
return `var ${local} = function () { return ${slot}.apply(this, arguments); };`;
}
/**
* Wraps a CommonJS module so the bundle can evaluate it.
* @param {object} context - Emission context.
* @param {object} context.module - The graph module.
* @param {Map<string, number>} context.indexOf - Module id to emission index.
* @param {Set<string>} context.included - Module ids in the bundle.
* @returns {string} The wrapped body.
*/
function renderCjsBody({ module, indexOf, included }) {
const cases = [];
for (const [specifier, id] of module.resolved) {
if (!included.has(id)) continue;
cases.push(`if (id === ${JSON.stringify(specifier)}) return ${moduleBinding(indexOf.get(id))};`);
}
return [
'var module = { exports: {} }, exports = module.exports;',
`function require(id) { ${cases.join(' ')} throw new Error('Cannot find module ' + id); }`,
'void require;',
module.source,
'return module.exports;',
].join('\n');
}
/**
* The prelude every bundle carries.
*
* One helper, five lines. A module system that needs more machinery than this
* at runtime has moved work out of the build that belonged in it.
* @type {string}
*/
const PRELUDE = `var __avx_def = function (target, name, get) {
Object.defineProperty(target, name, { enumerable: true, configurable: true, get: get });
};`;
/**
* Renders a linked graph into a single classic script.
* @param {object} options - Emission options.
* @param {object} options.graph - The module graph.
* @param {string[]} options.order - Module ids in emission order.
* @param {Set<string>} [options.included] - Modules that survived tree shaking.
* @param {string} [options.banner] - Text placed before the outer IIFE.
* @param {string} [options.footer] - Text placed inside the outer IIFE, after
* every module, where it can read `__avx_entry`.
* @param {string} [options.rootDir] - Root for source-map paths.
* @param {boolean} [options.sourceMap] - Whether to build a source map.
* @param {string} [options.file] - Output file name, for the map.
* @returns {{code: string, map: object|null, modules: number}} The bundle.
* @throws {EmitError} When the graph cannot be rendered correctly.
*/
export function emitBundle({
graph,
order,
included = null,
banner = '',
footer = '',
rootDir = process.cwd(),
sourceMap = false,
file = 'bundle.js',
}) {
const present = included || new Set(order);
const emitted = order.filter((id) => present.has(id));
const indexOf = new Map(emitted.map((id, index) => [id, index]));
// Every mutable exported binding in the bundle lives at bundle scope, so the
// exporter's writes and the importers' reads are the same variable.
const hoisted = new Set();
const hoistedOwner = new Map();
for (const id of emitted) {
const module = graph.modules.get(id);
for (const name of mutableExports(module)) {
if (hoistedOwner.has(name) && hoistedOwner.get(name) !== id) {
throw new EmitError(
`two modules export a mutable binding named "${name}":\n ${hoistedOwner.get(name)}\n ${id}\n` +
'A live binding is hoisted to bundle scope under its own name, so the two would collide. ' +
'Rename one of them.',
id,
'live-binding-collision',
);
}
hoisted.add(name);
hoistedOwner.set(name, id);
}
}
// Functions reached across a cycle are published into a bundle-scope slot by
// the module that declares them, before its body runs. `slots` is keyed by
// module id and export name so one function is published once however many
// cyclic importers it has.
/** @type {Map<string, string>} */
const slots = new Map();
/** @type {Map<string, Array<{slot: string, local: string}>>} */
const slotsByModule = new Map();
const slotFor = (moduleId, exported) => {
const key = `${moduleId}\u0000${exported}`;
const existing = slots.get(key);
if (existing) return existing;
const slot = `__avx_fn${slots.size}`;
slots.set(key, slot);
const declared = functionExportLocal(graph.modules.get(moduleId), exported);
if (!slotsByModule.has(moduleId)) slotsByModule.set(moduleId, []);
slotsByModule.get(moduleId).push({ slot, local: declared });
return slot;
};
const lines = [];
/** @type {Array<{module: string, outputLine: number, lines: number}>} */
const segments = [];
const push = (text) => {
lines.push(text);
};
if (banner) push(banner);
push('(function () {');
push("'use strict';");
push(PRELUDE);
if (hoisted.size > 0) {
push(`var ${[...hoisted].join(', ')};`);
}
const currentLine = () => lines.join('\n').split('\n').length;
let bodies;
try {
bodies = emitted.map((id) => {
const module = graph.modules.get(id);
return {
id,
module,
body:
module.format === 'cjs'
? renderCjsBody({ module, indexOf, included: present })
: renderEsmBody({ module, graph, indexOf, hoisted, included: present, slotFor }),
};
});
} catch (error) {
// The cycles travel with the error so a caller can describe the failure in
// its own vocabulary -- Avenx reports a cycle between two bridges as a
// bridge problem, which is what the developer was actually writing.
if (error instanceof EmitError) {
error.cycles = graph.cycles;
}
throw error;
}
if (slots.size > 0) {
push(`var ${[...slots.values()].join(', ')};`);
}
for (const { id, module, body } of bodies) {
const binding = moduleBinding(indexOf.get(id));
const label = module.virtual ? id : path.relative(rootDir, id);
push(`// ${label}`);
push(`var ${binding} = (function () {`);
push('var __x = {};');
// Published before the body, mirroring the point at which ES modules
// initialise a hoisted function declaration.
const published = slotsByModule.get(id);
if (published && published.length > 0) {
push(published.map((entry) => `${entry.slot} = ${entry.local};`).join(' '));
}
const bodyStartLine = currentLine() + 1;
segments.push({ module: id, outputLine: bodyStartLine, lines: lineCount(body) });
push(body);
push('return __x;');
push('})();');
}
// The last entry's exports are published as `__avx_entry` inside the bundle
// scope. A footer -- the one that installs `globalThis.Avenx`, for instance
// -- needs a name for what the entry produced, and inventing one per caller
// would make the contract implicit.
const liveEntries = graph.entries.filter((entry) => present.has(entry));
for (const entry of liveEntries) {
push(`void ${moduleBinding(indexOf.get(entry))};`);
}
if (liveEntries.length > 0) {
push(`var __avx_entry = ${moduleBinding(indexOf.get(liveEntries[liveEntries.length - 1]))};`);
push('void __avx_entry;');
}
if (footer) push(footer);
push('})();');
const code = lines.join('\n') + '\n';
const map = sourceMap ? buildSourceMap({ graph, segments, rootDir, file }) : null;
return { code, map, modules: emitted.length };
}
/**
* Builds a line-level source map for an emitted bundle.
*
* Line-level rather than column-level, and exact rather than approximate: every
* rewritten declaration was padded back to the line count it replaced, so
* output line N of a module's body is source line N of that module. A stack
* trace from a production bundle therefore names a file and a line the
* developer wrote.
* @param {object} options - Map options.
* @param {object} options.graph - The module graph.
* @param {Array<object>} options.segments - Where each module's body landed.
* @param {string} options.rootDir - Root for relative source paths.
* @param {string} options.file - The generated file name.
* @returns {object} A source map, version 3.
*/
function buildSourceMap({ graph, segments, rootDir, file }) {
const sources = [];
const sourcesContent = [];
const indexOfSource = new Map();
const sourceIndex = (id) => {
if (indexOfSource.has(id)) return indexOfSource.get(id);
const module = graph.modules.get(id);
const name = module.virtual ? id.replace(/^\s*/, '') : path.relative(rootDir, id).split(path.sep).join('/');
const index = sources.length;
sources.push(name);
sourcesContent.push(module.source);
indexOfSource.set(id, index);
return index;
};
const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const encode = (value) => {
let vlq = value < 0 ? ((-value) << 1) | 1 : value << 1;
let out = '';
do {
let digit = vlq & 31;
vlq >>>= 5;
if (vlq > 0) digit |= 32;
out += BASE64[digit];
} while (vlq > 0);
return out;
};
/** @type {Array<string>} */
const mappingLines = [];
let previousSource = 0;
let previousSourceLine = 0;
for (const segment of segments) {
const index = sourceIndex(segment.module);
for (let line = 0; line < segment.lines; line += 1) {
const outputLine = segment.outputLine + line - 1;
while (mappingLines.length <= outputLine) {
mappingLines.push('');
}
mappingLines[outputLine] =
encode(0) + encode(index - previousSource) + encode(line - previousSourceLine) + encode(0);
previousSource = index;
previousSourceLine = line;
}
}
return {
version: 3,
file,
sources,
sourcesContent,
names: [],
mappings: mappingLines.join(';'),
};
}
|