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 | 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 16x 16x 16x 16x 16x 16x 16x 16x 16x 282x 282x 282x 282x 282x 282x 282x 282x 25548x 25548x 282x 282x 282x 282x 282x 282x 282x 38x 38x 9x 9x 29x 29x 29x 38x 29x 29x 38x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 50x 50x 26x 26x 50x 50x 47x 93x 24x 24x 24x 93x 50x 282x 282x 282x 282x 282x 282x 282x 33x 33x 33x 7x 7x 26x 26x 26x 26x 33x 33x 33x 26x 29x 25x 25x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 33x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 38x 38x 38x 36x 36x 2x 2x 2x 1x 1x 2x 1x 38x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 25107x 25107x 19081x 19081x 6026x 6026x 25107x 25107x 6026x 6026x 25107x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 24976x 25013x 25013x 25013x 41x 41x 24976x 24976x 24976x 24976x 24969x 24969x 7x 15x 6x 36x 36x 36x 1x 1x 36x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 24976x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 136x 136x 136x 136x 136x 136x 136x 136x 136x 136x 136x 136x 136x 136x 136x 282x 282x 282x 282x 282x 282x 282x 17313x 17313x 282x 282x 282x 282x 282x 282x 282x 282x 282x 25771x 25771x 25771x 25771x 25771x 25771x 25771x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 25771x 227x 227x 25544x 25771x 409x 409x 25135x 25771x 25771x 5x 5x 5x 5x 5x 5x 5x 25130x 25130x 25771x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 25129x 25771x 25091x 25091x 38x 38x 25771x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 25091x 25091x 25091x 25091x 25091x 25091x 25091x 99922x 99922x 148x 148x 99922x 24943x 24943x 25091x 5x 5x 5x 5x 5x 5x 24938x 25091x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 24938x 24938x 24933x 24933x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 24938x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 38x 38x 38x 38x 38x 38x 38x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 24x 38x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 5x 5x 5x 5x 5x 11x 38x 38x 38x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 33x 33x 33x 33x 26x 30x 7x 7x 7x 7x 7x 33x 33x 33x 33x 33x 282x 282x 282x 282x 282x 282x 282x 282x 282x 38x 38x 38x 38x 66x 66x 274x 274x 14x 14x 274x 274x 208x 208x 66x 38x 38x 38x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 25134x 25134x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 282x 1x 1x 1x 1x 1x 1x 282x | /**
* @file resolve.js
* @description Turns an import specifier into a module the bundler can read.
*
* ## What this replaces
*
* There was no module resolution. `rewriteRuntimeImports` matched the runtime
* entry and deleted everything else, so `import { format } from 'date-fns'` in
* a component became nothing at all — a green build and a `ReferenceError` the
* first time the action ran. Resolution is the difference between a build that
* knows what an application depends on and one that guesses.
*
* ## The rules, in order
*
* 1. **Virtual modules.** The compiler owns component, page and entry
* generation, and hands those in as sources rather than files. They resolve
* by id and win over anything on disk.
* 2. **Relative and absolute paths.** Probed for an exact file, then for the
* Avenx and JavaScript extensions, then for a directory index. A specifier
* that names a `.component.js` or `.page.js` resolves to the *compiled*
* virtual module, never to the raw template file — the raw file is not
* JavaScript and would not parse.
* 3. **Node builtins.** Rejected, by name, with the reason. A browser bundle
* that quietly contains `fs` is a bundle that fails at load; saying so at
* build time is the whole point.
* 4. **Bare specifiers.** The Node algorithm, walking `node_modules` upward
* from the importing file, honouring `exports`, `browser`, `module` and
* `main` in that order of preference for a browser target.
*
* Nothing here falls back to "skip it". A specifier that cannot be resolved is
* a {@link ResolveError}, and the build fails with the importer and the
* specifier — because the alternative is what this file exists to end.
* @module lib/bundler/resolve
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* The root of the installed `avenx-core` package, derived from this file.
* @type {string}
*/
export const AVENX_PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
/**
* Specifiers that mean "the Avenx browser runtime".
*
* The compiler has always accepted all three spellings, and a deep path into
* `lib/core` besides, so resolution accepts exactly what the old rewriter did.
* @type {RegExp}
*/
const RUNTIME_SPECIFIER = /^(avenx-core(\/(runtime|core))?)$/;
/**
* Node builtin modules, with and without the `node:` prefix.
*
* Listed rather than probed, because the answer must not depend on which Node
* version is running the build: an application importing `fs` is broken in a
* browser on every version, and the diagnostic should say so identically.
* @type {Set<string>}
*/
const NODE_BUILTINS = new Set([
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants',
'crypto', 'dgram', 'diagnostics_channel', 'dns', 'domain', 'events', 'fs', 'http',
'http2', 'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks', 'process',
'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'sys',
'timers', 'tls', 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib',
]);
/**
* Extensions probed for a path specifier that names no file directly.
* @type {string[]}
*/
const EXTENSIONS = ['.js', '.mjs', '.component.js', '.page.js', '.bridge.js', '.guard.js', '.json'];
/**
* Extensions an Avenx bundle has no way to represent as a module.
*
* The old pipeline deleted these imports silently, so `import './theme.css'`
* in `main.app.js` looked like it did something and did nothing at all. Saying
* so is strictly better than either silence or a resolution error that reads
* as a missing file.
* @type {Set<string>}
*/
const ASSET_EXTENSIONS = new Set([
'.css', '.scss', '.sass', '.less', '.styl',
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif', '.ico',
'.woff', '.woff2', '.ttf', '.otf', '.eot',
'.mp3', '.mp4', '.webm', '.wav',
]);
/**
* Raised when a specifier cannot be resolved to a module.
*/
export class ResolveError extends Error {
/**
* @param {string} specifier - The unresolvable specifier.
* @param {string} importer - The module that asked for it.
* @param {string} reason - Why resolution failed.
*/
constructor(specifier, importer, reason) {
super(reason);
this.name = 'ResolveError';
/** @type {string} */
this.specifier = specifier;
/** @type {string} */
this.importer = importer;
/** @type {string} */
this.reason = reason;
}
}
/**
* Whether a specifier names the Avenx runtime entry.
* @param {string} specifier - The import specifier.
* @returns {boolean} True for `avenx-core`, `avenx-core/runtime` or `avenx-core/core`.
*/
export function isRuntimeSpecifier(specifier) {
return RUNTIME_SPECIFIER.test(specifier);
}
/**
* Reads and caches a `package.json`.
* @param {string} file - Absolute path to the manifest.
* @param {Map<string, object|null>} cache - Shared manifest cache.
* @returns {object|null} The parsed manifest, or null when absent or invalid.
*/
function readManifest(file, cache) {
if (cache.has(file)) {
return cache.get(file);
}
let manifest;
try {
manifest = JSON.parse(fs.readFileSync(file, 'utf-8'));
} catch {
manifest = null;
}
cache.set(file, manifest);
return manifest;
}
/**
* Picks a target out of an `exports` value for a browser build.
*
* Conditions are tried in the order a browser bundler should prefer them:
* `browser` before `import` before `module` before `default`. `require` is
* accepted last, because a package that offers only CommonJS is still better
* bundled than reported missing — {@link module:lib/bundler/interop} decides
* what to do with the format once the file is read.
* @param {any} value - An `exports` entry: string, conditions object, or array.
* @returns {string|null} A relative target, or null when nothing applies.
*/
function selectCondition(value) {
if (typeof value === 'string') {
return value;
}
if (Array.isArray(value)) {
for (const entry of value) {
const picked = selectCondition(entry);
if (picked) return picked;
}
return null;
}
if (!value || typeof value !== 'object') {
return null;
}
for (const condition of ['browser', 'import', 'module', 'default', 'require']) {
if (condition in value) {
const picked = selectCondition(value[condition]);
if (picked) return picked;
}
}
return null;
}
/**
* Resolves a subpath against a manifest's `exports` field.
* @param {object} manifest - The parsed package manifest.
* @param {string} subpath - `.` for the package root, otherwise `./name`.
* @returns {string|null} A relative target, or null when `exports` does not cover it.
*/
function resolveExports(manifest, subpath) {
const table = manifest.exports;
if (table === undefined || table === null) {
return null;
}
// A bare string or a conditions object with no subpath keys means the whole
// package resolves to one target, and only `.` is exported.
const hasSubpathKeys =
typeof table === 'object' && !Array.isArray(table) && Object.keys(table).some((key) => key.startsWith('.'));
if (!hasSubpathKeys) {
return subpath === '.' ? selectCondition(table) : null;
}
if (table[subpath] !== undefined) {
return selectCondition(table[subpath]);
}
// Pattern entries: `"./*": "./dist/*.js"`.
for (const [pattern, value] of Object.entries(table)) {
const star = pattern.indexOf('*');
if (star === -1) continue;
const prefix = pattern.slice(0, star);
const suffix = pattern.slice(star + 1);
if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) continue;
const middle = subpath.slice(prefix.length, subpath.length - suffix.length || undefined);
const target = selectCondition(value);
if (target) return target.replace('*', middle);
}
return null;
}
/**
* Applies a package's `browser` field remapping to a resolved file.
*
* The string form replaces the entry point. The object form maps individual
* paths, and a `false` value means "this module is empty in a browser", which
* is how packages ship Node-only branches — honouring it is what keeps a
* `fs` shim from reaching the bundle.
* @param {object} manifest - The parsed manifest.
* @param {string} packageDir - The package root.
* @param {string} file - The file resolution produced.
* @returns {string|false} The remapped file, or false when it is stubbed out.
*/
function applyBrowserField(manifest, packageDir, file) {
const field = manifest.browser;
if (!field || typeof field !== 'object') {
return file;
}
for (const [from, to] of Object.entries(field)) {
if (!from.startsWith('.')) continue;
if (path.resolve(packageDir, from) === file) {
return to === false ? false : path.resolve(packageDir, to);
}
}
return file;
}
/**
* Real paths, keyed by the path that was walked to reach them.
*
* `fs.realpathSync` is a syscall per segment, and a large graph probes the same
* package directory thousands of times. The cache is module-scoped rather than
* per-Resolver because the answer is a property of the filesystem, not of a
* build.
* @type {Map<string, string>}
*/
const realPaths = new Map();
/**
* Canonicalises a path so that one file has one identity.
*
* A module's identity in the graph is the path that resolved to it. Without
* this, a file reachable by two paths becomes two modules -- and that is not a
* corner case: `npm link`, a `file:` dependency, a pnpm store and every
* workspace layout put a symlink between an application and its dependencies.
*
* Duplicated source is the cheap half of the damage. The expensive half is
* module state. `lib/core/renderer/stringRenderer.js` holds the registry the
* fallback renderer installs itself into; emitted twice, the install fills one
* copy and `AvenxComponent` reads the other, so a component whose template the
* IR refused throws at its first render.
*
* Node's own ESM loader canonicalises this way unless `--preserve-symlinks` is
* passed, and Rollup, webpack and esbuild all do the same. Avenx now agrees
* with them.
*
* Falls back to the path as given when it cannot be resolved: a path that does
* not exist is not this function's error to raise, and the caller is already
* about to say so with a specifier and an importer.
* @param {string} target - An absolute path.
* @returns {string} The canonical path, or `target` when it cannot be read.
*/
export function realPath(target) {
const cached = realPaths.get(target);
if (cached !== undefined) {
return cached;
}
let resolved;
try {
resolved = fs.realpathSync.native ? fs.realpathSync.native(target) : fs.realpathSync(target);
} catch {
resolved = target;
}
realPaths.set(target, resolved);
return resolved;
}
/**
* Probes a path for a real file, trying Avenx and JavaScript extensions and a
* directory index.
*
* Every return goes through {@link realPath}, so the id the graph keys a module
* by is the file's own path rather than whichever route reached it.
* @param {string} candidate - An absolute path with or without an extension.
* @returns {string|null} The file that exists, or null.
*/
export function probeFile(candidate) {
const stat = (target) => {
try {
return fs.statSync(target);
} catch {
return null;
}
};
const direct = stat(candidate);
if (direct && direct.isFile()) {
return realPath(candidate);
}
if (!direct) {
for (const extension of EXTENSIONS) {
const withExtension = `${candidate}${extension}`;
const found = stat(withExtension);
if (found && found.isFile()) {
return realPath(withExtension);
}
}
return null;
}
if (direct.isDirectory()) {
for (const extension of ['.js', '.mjs']) {
const index = path.join(candidate, `index${extension}`);
const found = stat(index);
if (found && found.isFile()) {
return realPath(index);
}
}
}
return null;
}
/**
* Resolves module specifiers for one build.
*
* Holds the virtual-module table the compiler populated and a manifest cache,
* so repeated resolution across a large graph reads each `package.json` once.
*/
export class Resolver {
/**
* @param {object} options - Resolver options.
* @param {Map<string, string>} [options.virtualModules] - Module id to source.
* @param {string} [options.runtimeEntry] - Absolute path of the runtime barrel.
* @param {string[]} [options.roots] - Extra directories to search for packages.
*/
constructor({ virtualModules = new Map(), runtimeEntry = null, roots = [] } = {}) {
/** @type {Map<string, string>} */
this.virtualModules = virtualModules;
// Canonicalised for the same reason every probed path is: the runtime entry
// is returned directly for `avenx-core` and `avenx-core/runtime` without
// going through probeFile, and it has to name the same module that a
// relative import of lib/core/index.js names.
/** @type {string} */
this.runtimeEntry = realPath(runtimeEntry || path.join(AVENX_PACKAGE_ROOT, 'lib', 'core', 'index.js'));
/** @type {string[]} */
this.roots = roots;
/** @type {Map<string, object|null>} */
this.manifests = new Map();
/** @type {Map<string, string>} */
this.cache = new Map();
}
/**
* Whether an id names a module the compiler generated rather than a file.
* @param {string} id - The module id.
* @returns {boolean} True for a virtual module.
*/
isVirtual(id) {
return this.virtualModules.has(id);
}
/**
* Resolves a specifier to a module id.
* @param {string} specifier - The import specifier as written.
* @param {string} importer - Absolute path or virtual id of the importing module.
* @returns {string} The resolved module id.
* @throws {ResolveError} When the specifier names nothing resolvable.
*/
resolve(specifier, importer) {
const key = `${importer}\u0000${specifier}`;
const cached = this.cache.get(key);
if (cached) {
return cached;
}
const resolved = this.resolveUncached(specifier, importer);
this.cache.set(key, resolved);
return resolved;
}
/**
* Resolution without the memo.
* @param {string} specifier - The import specifier.
* @param {string} importer - The importing module.
* @returns {string} The resolved module id.
* @throws {ResolveError} When the specifier names nothing resolvable.
* @private
*/
resolveUncached(specifier, importer) {
if (this.virtualModules.has(specifier)) {
return specifier;
}
if (isRuntimeSpecifier(specifier)) {
return this.runtimeEntry;
}
const bareBuiltin = specifier.startsWith('node:') ? specifier.slice(5) : specifier;
if (NODE_BUILTINS.has(bareBuiltin)) {
throw new ResolveError(
specifier,
importer,
`"${specifier}" is a Node.js builtin and has no browser implementation. ` +
'An Avenx application bundle runs in a browser, so it cannot contain it.',
);
}
const extension = path.extname(specifier).toLowerCase();
if (ASSET_EXTENSIONS.has(extension)) {
throw new ResolveError(
specifier,
importer,
`Avenx does not bundle ${extension} files, so this import cannot be honoured.\n` +
(extension === '.css' || extension === '.scss' || extension === '.sass' || extension === '.less'
? 'Component styles belong in a matching .component.css or .page.css file, and application-wide ' +
'styles in a <@global> block. A stylesheet that is genuinely external belongs in a <link> tag ' +
'in index.html.'
: 'Reference the asset by URL from your template or stylesheet instead.'),
);
}
if (specifier.startsWith('.') || path.isAbsolute(specifier)) {
return this.resolvePath(specifier, importer);
}
return this.resolveBare(specifier, importer);
}
/**
* Resolves a relative or absolute specifier.
* @param {string} specifier - The specifier.
* @param {string} importer - The importing module.
* @returns {string} The resolved module id.
* @throws {ResolveError} When nothing exists at that path.
* @private
*/
resolvePath(specifier, importer) {
const base = this.directoryOf(importer);
const candidate = path.resolve(base, specifier);
// A generated module wins over the file it was generated from. A
// `.component.js` on disk is Avenx template source, not JavaScript, and
// handing it to the module reader would fail on markup.
for (const suffix of ['', '.component.js', '.page.js', '.js']) {
const virtualId = `${candidate}${suffix}`;
if (this.virtualModules.has(virtualId)) {
return virtualId;
}
}
const file = probeFile(candidate);
if (!file) {
throw new ResolveError(
specifier,
importer,
`no file exists at ${candidate} (tried it directly, with ${EXTENSIONS.join(', ')}, and as a directory index)`,
);
}
return this.applyOwningBrowserField(file, specifier);
}
/**
* Applies the `browser` field of the package a resolved file belongs to.
*
* A package stubs its Node-only modules by mapping them to `false`, and those
* modules are usually reached by a *relative* import from inside the package
* rather than by a bare specifier. Honouring the field only at the package
* entry would therefore miss exactly the case it exists for.
*
* Restricted to files under `node_modules`: this is how third-party packages
* describe themselves, and silently emptying a module in an application's own
* source would be a surprise rather than a service.
* @param {string} file - The resolved file.
* @param {string} specifier - The specifier that produced it, for the stub id.
* @returns {string} The file, or the id of an empty module standing in for it.
* @private
*/
applyOwningBrowserField(file, specifier) {
const marker = `${path.sep}node_modules${path.sep}`;
if (!file.includes(marker)) {
return file;
}
let dir = path.dirname(file);
for (;;) {
const manifestPath = path.join(dir, 'package.json');
if (fs.existsSync(manifestPath)) {
const manifest = readManifest(manifestPath, this.manifests) || {};
const mapped = applyBrowserField(manifest, dir, file);
return mapped === false ? this.emptyModuleId(specifier) : mapped;
}
const parent = path.dirname(dir);
if (parent === dir || !dir.includes(marker)) {
return file;
}
dir = parent;
}
}
/**
* Resolves a bare package specifier through `node_modules`.
* @param {string} specifier - The specifier, e.g. `lodash` or `lodash/fp`.
* @param {string} importer - The importing module.
* @returns {string} The resolved module id.
* @throws {ResolveError} When the package or its subpath cannot be found.
* @private
*/
resolveBare(specifier, importer) {
const scoped = specifier.startsWith('@');
const parts = specifier.split('/');
const name = scoped ? parts.slice(0, 2).join('/') : parts[0];
const subpath = specifier.slice(name.length).replace(/^\//, '');
const searched = [];
for (const dir of this.packageDirectories(importer, name)) {
searched.push(dir);
const manifestPath = path.join(dir, 'package.json');
const manifest = readManifest(manifestPath, this.manifests) || {};
const resolved = this.resolveInPackage(dir, manifest, subpath);
if (resolved === false) {
// `"browser": { "./node-only.js": false }` — the package itself says
// this module is empty in a browser. Honour that rather than bundling
// a Node implementation.
return this.emptyModuleId(specifier);
}
if (resolved) {
return resolved;
}
}
if (specifier === 'avenx-core' || specifier.startsWith('avenx-core/')) {
// Building inside the Avenx repository itself, or against a checkout
// rather than an install. The package that owns this file is the one the
// application means.
const local = this.resolveInPackage(
AVENX_PACKAGE_ROOT,
readManifest(path.join(AVENX_PACKAGE_ROOT, 'package.json'), this.manifests) || {},
specifier.slice('avenx-core'.length).replace(/^\//, ''),
);
if (local) {
return local;
}
}
throw new ResolveError(
specifier,
importer,
searched.length > 0
? `no package named "${name}" was found. Looked in:\n ${searched.join('\n ')}`
: `no package named "${name}" was found in any node_modules directory above ${this.directoryOf(importer)}`,
);
}
/**
* Resolves a subpath inside a package directory.
* @param {string} dir - The package root.
* @param {object} manifest - Its parsed manifest.
* @param {string} subpath - The subpath, `''` for the package root.
* @returns {string|false|null} A file, `false` when browser-stubbed, or null.
* @private
*/
resolveInPackage(dir, manifest, subpath) {
const exportsTarget = resolveExports(manifest, subpath === '' ? '.' : `./${subpath}`);
let candidate;
if (exportsTarget) {
candidate = path.resolve(dir, exportsTarget);
} else if (subpath !== '') {
candidate = path.resolve(dir, subpath);
} else {
const browserMain = typeof manifest.browser === 'string' ? manifest.browser : null;
const main = browserMain || manifest.module || manifest.main || 'index.js';
candidate = path.resolve(dir, main);
}
const file = probeFile(candidate);
if (!file) {
return null;
}
return applyBrowserField(manifest, dir, file);
}
/**
* Yields every `node_modules/<name>` directory that exists above an importer.
* @param {string} importer - The importing module.
* @param {string} name - The package name.
* @returns {string[]} Existing package directories, nearest first.
* @private
*/
packageDirectories(importer, name) {
const found = [];
const bases = [this.directoryOf(importer), ...this.roots];
for (const base of bases) {
let dir = base;
for (;;) {
const candidate = path.join(dir, 'node_modules', name);
if (fs.existsSync(path.join(candidate, 'package.json')) && !found.includes(candidate)) {
found.push(candidate);
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
}
return found;
}
/**
* The directory a module's relative imports resolve against.
*
* A virtual module is generated *for* a source file and keeps that file's id,
* so its relative imports resolve exactly as the developer wrote them.
* @param {string} importer - Module id.
* @returns {string} An absolute directory.
* @private
*/
directoryOf(importer) {
return path.dirname(importer);
}
/**
* Registers and returns an empty module standing in for a browser stub.
*
* The id begins with a NUL so it can never collide with a real path, which is
* the same convention other bundlers use for a module that exists only in
* memory.
* @param {string} specifier - The specifier being stubbed.
* @returns {string} The virtual module id.
* @private
*/
emptyModuleId(specifier) {
const id = `\u0000avenx:empty:${specifier}`;
if (!this.virtualModules.has(id)) {
this.virtualModules.set(id, `// "${specifier}" is browser-stubbed by its own package.\nexport default {};\n`);
}
return id;
}
}
|