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 666 667 668 669 670 671 672 673 674 675 676 677 678 | 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 94x 94x 94x 94x 94x 94x 94x 322x 322x 322x 322x 322x 322x 322x 79x 79x 322x 322x 322x 322x 322x 322x 322x 322x 322x 566x 566x 566x 566x 566x 566x 43496x 43496x 43496x 43496x 43496x 51x 51x 51x 51x 43496x 15x 15x 15x 15x 43430x 43430x 43496x 304x 304x 2446x 2x 2x 2x 2446x 2140x 2140x 304x 304x 304x 43126x 43126x 43496x 2x 2x 52x 52x 52x 4x 4x 4x 46x 44x 46x 46x 4x 4x 46x 46x 2x 2x 2x 43124x 43496x 1927x 43496x 1927x 1927x 566x 566x 41197x 450x 450x 42558x 42558x 42558x 566x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 565x 565x 565x 565x 565x 116x 116x 449x 565x 565x 565x 565x 85x 85x 565x 565x 106x 106x 258x 258x 258x 565x 26x 26x 232x 565x 322x 322x 322x 322x 322x 322x 322x 322x 158x 158x 158x 158x 158x 158x 158x 158x 565x 565x 565x 565x 565x 449x 449x 565x 158x 158x 158x 322x 322x 322x 322x 322x 322x 322x 322x 217x 217x 217x 217x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 26x 26x 26x 551x 551x 26x 551x 26x 26x 525x 26x 26x 26x 551x 26x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 217x 217x 26x 26x 191x 212x 191x 212x 191x 191x 217x 212x 191x 191x 212x 191x 191x 217x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 199x 199x 199x 199x 199x 199x 199x 199x 199x 199x 199x 199x 1072x 1072x 199x 1063x 199x 199x 199x 1072x 199x 322x 322x 322x 322x 322x 322x 90x 90x 90x 90x 80x 80x 80x 80x 80x 80x 80x 10x 90x 322x 322x 322x 322x 322x 322x 322x 80x 80x 80x 80x 86x 86x 80x 80x 322x 322x 322x 322x 322x 322x 322x 266x 266x 266x 266x 7x 7x 266x 266x 322x 322x 322x 322x 322x 322x 1221x 1221x 1221x 1221x 228366x 228366x 228366x 88x 88x 88x 88x 88x 88x 228278x 228366x 111x 111x 111x 111x 111x 111x 111x 228167x 228366x 5012x 5012x 5012x 49454x 49454x 49454x 5012x 5012x 5012x 5012x 5012x 223155x 223155x 223155x 223155x 1221x 1221x 322x 322x 322x 322x 322x 322x 322x 322x 1221x 1221x 1221x 1221x 1221x 1221x 1221x 1221x 1221x 810x 810x 810x 810x 810x 810x 808x 808x 303x 321x 321x 321x 321x 321x 303x 808x 808x 505x 505x 808x 810x 810x 810x 1221x 1221x 322x 322x 322x 322x 322x 322x 322x 322x 736x 215x 215x 521x 521x 521x 521x 521x 735x 303x 303x 687x 1x 1x 217x 736x 322x 322x 322x 322x 322x 322x 322x 322x 322x 1131x 1131x 725x 725x 299x 299x 725x 1131x 1131x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 90x 90x 90x 90x 90x 90x 90x 90x 90x 11x 11x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 79x 83x 296x 85x 85x 85x 85x 296x 296x 79x 79x 79x 79x 79x 153x 153x 79x 79x 79x 132x 132x 132x 26x 26x 132x 132x 296x 79x 79x 79x 83x 85x 79x 79x 6x 85x 3x 3x 85x 85x 85x 85x 85x 85x 79x 79x 90x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 84x 84x 84x 322x 322x 322x 322x 322x 322x 322x 39x 39x 322x 322x 322x 322x 322x 322x 322x 322x 2x 2x 11x 11x 11x 11x 98x 614x 614x 614x 614x 614x 614x 98x 11x 2x 2x 2x 2x 2x 2x 11x 11x 4x 4x 4x 11x 2x 2x 2x 2x 2x | /**
* @file BridgeParser.js
* @description Static analysis of Avenx bridge modules.
*
* A bridge is a normal ES module whose default export is a `bridge({...})`
* call. Because consumers reach it through an `import`, the compiler can read
* the whole picture from source alone: which bridges exist, what each one
* declares, which events it emits, and who imports it.
*
* This module answers those questions without a full JavaScript parser. It
* scans the `bridge({...})` argument with a brace/string-aware walker and reads
* only the top level of the object literal, which is all the declaration
* surface a bridge has.
*/
import fs from 'fs';
import path from 'path';
/**
* Module specifiers that resolve to the Avenx runtime.
* @type {RegExp}
*/
const RUNTIME_SPECIFIER = /^(avenx-core(\/(runtime|core))?|.*\/lib\/core(\/index\.js)?)$/;
/**
* Derives a bridge's name from its file name.
* `user-prefs.bridge.js` becomes `userPrefs`.
* @param {string} filePath - Path to the bridge module.
* @returns {string} The bridge name.
*/
export function bridgeNameFromFile(filePath) {
const base = path.basename(filePath).replace(/\.bridge\.js$/i, '');
return base
.split(/[-_.]/)
.filter(Boolean)
.map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
.join('');
}
/**
* Returns the identifier the compiled bundle uses for a bridge instance.
* @param {string} name - The bridge name.
* @returns {string} A valid JavaScript identifier.
*/
export function bridgeBindingName(name) {
return `__avx_bridge_${String(name).replace(/[^\w$]/g, '_')}`;
}
/**
* Walks source from an opening brace to its match, ignoring braces that appear
* inside strings, template literals or comments.
* @param {string} source - The source text.
* @param {number} openIndex - Index of the opening `{`.
* @returns {{ end: number, topLevelCommas: number[] }} The index of the matching
* `}` (or -1) and the offsets of commas at depth 1.
*/
function scanObjectLiteral(source, openIndex) {
let depth = 0;
let index = openIndex;
const topLevelCommas = [];
while (index < source.length) {
const char = source[index];
const next = source[index + 1];
// Comments
if (char === '/' && next === '/') {
const lineEnd = source.indexOf('\n', index);
index = lineEnd === -1 ? source.length : lineEnd;
continue;
}
if (char === '/' && next === '*') {
const blockEnd = source.indexOf('*/', index + 2);
index = blockEnd === -1 ? source.length : blockEnd + 2;
continue;
}
// Quoted strings
if (char === '"' || char === "'") {
index++;
while (index < source.length) {
if (source[index] === '\\') {
index += 2;
continue;
}
if (source[index] === char) break;
index++;
}
index++;
continue;
}
// Template literals, including ${...} substitutions
if (char === '`') {
index++;
while (index < source.length) {
if (source[index] === '\\') {
index += 2;
continue;
}
if (source[index] === '`') break;
if (source[index] === '$' && source[index + 1] === '{') {
let inner = 1;
index += 2;
while (index < source.length && inner > 0) {
if (source[index] === '{') inner++;
else if (source[index] === '}') inner--;
index++;
}
continue;
}
index++;
}
index++;
continue;
}
if (char === '{' || char === '[' || char === '(') {
depth++;
} else if (char === '}' || char === ']' || char === ')') {
depth--;
if (depth === 0) {
return { end: index, topLevelCommas };
}
} else if (char === ',' && depth === 1) {
topLevelCommas.push(index);
}
index++;
}
return { end: -1, topLevelCommas };
}
/**
* Matches the `: atomic(` that marks a bridge action as transactional.
* @type {RegExp}
*/
const ATOMIC_VALUE = /^\s*:\s*atomic\s*\(/;
/**
* Reads the member name that starts a top-level object-literal entry.
* @param {string} segment - Source of one entry, starting after `{` or `,`.
* @returns {{ name: string, kind: ('getter'|'action'|'property'), atomic: boolean= }|null}
* The parsed member, or null when the segment is not a recognisable declaration.
*/
function parseMember(segment) {
const match = segment.match(
/^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/|\s)*(?:(get|set)\s+)?(?:async\s+)?\*?\s*(?:(['"])([^'"]+)\2|([A-Za-z_$][\w$]*))\s*([:(])/,
);
if (!match) {
return null;
}
const accessor = match[1];
const name = match[3] !== undefined ? match[3] : match[4];
const delimiter = match[5];
if (accessor === 'get') {
return { name, kind: 'getter' };
}
if (accessor === 'set') {
// Setters are not part of the Bridge surface; report them as properties so
// the caller can reject them with a clear message.
return { name, kind: 'property' };
}
if (delimiter === '(') {
return { name, kind: 'action' };
}
// `addQty: atomic(function (id, n) { ... })` is an action wearing a wrapper.
// Without this it reads as a plain property, which the Bridge API rejects
// and which would hide the body from Atlas.
if (delimiter === ':' && ATOMIC_VALUE.test(segment.slice(segment.indexOf(name) + name.length))) {
return { name, kind: 'action', atomic: true };
}
return { name, kind: 'property' };
}
/**
* Splits the top level of an object literal into member declarations.
* @param {string} source - The full source text.
* @param {number} openIndex - Index of the literal's opening `{`.
* @returns {{ members: Array<{name: string, kind: string, valueStart: number}>, end: number }}
* The declared members and the index of the closing brace.
*/
function parseObjectMembers(source, openIndex) {
const { end, topLevelCommas } = scanObjectLiteral(source, openIndex);
if (end === -1) {
return { members: [], end };
}
const boundaries = [openIndex, ...topLevelCommas];
const members = [];
for (let i = 0; i < boundaries.length; i++) {
const start = boundaries[i] + 1;
const stop = i + 1 < boundaries.length ? boundaries[i + 1] : end;
const segment = source.slice(start, stop);
const member = parseMember(segment);
if (member) {
members.push({ ...member, valueStart: start + segment.indexOf(member.name) });
}
}
return { members, end };
}
/**
* Skips past an `atomic(` wrapper following a member name.
* @param {string} source - The module source.
* @param {number} valueStart - Offset of the member's name.
* @returns {number} The offset just inside the wrapper's `(`, or -1 when the
* member is not wrapped.
*/
function unwrapAtomic(source, valueStart) {
const match = /^(?:['"][^'"]*['"]|[A-Za-z_$][\w$]*)\s*:\s*atomic\s*\(/.exec(source.slice(valueStart));
return match ? valueStart + match[0].length : -1;
}
/**
* Locates the body brace of a function expression that begins at an offset.
*
* The wrapper accepts every shape a bridge action is written in — `function
* (a, b) {`, `async function (a) {`, `(a, b) => {`, `a => {` — so the brace is
* found by depth rather than by matching each form. Parentheses and brackets
* opened by the parameter list raise the depth, so the first `{` seen at depth
* zero is the body.
* @param {string} source - The module source.
* @param {number} from - Where to start scanning.
* @returns {{start: number, end: number}|null} The body's inner span, exclusive
* of the braces, or null when no body could be located.
*/
function wrappedFunctionBodySpan(source, from) {
let depth = 0;
for (let i = from; i < source.length; i++) {
const char = source[i];
if (char === '(' || char === '[') {
depth++;
} else if (char === ')' || char === ']') {
if (depth === 0) return null;
depth--;
} else if (char === '{' && depth === 0) {
const { end } = scanObjectLiteral(source, i);
return end === -1 ? null : { start: i + 1, end };
}
}
return null;
}
/**
* Locates the body of a member declared as a function.
*
* An action (`addQty(id, n) { ... }`) and a getter (`get total() { ... }`)
* both put their code between the brace that follows their parameter list and
* its match. Atlas reads those bodies to record what a bridge action writes
* and what a getter reads; without a span it would have to re-parse the module.
* @param {string} source - The module source.
* @param {number} valueStart - Offset of the member's name.
* @returns {{start: number, end: number}|null} The body's inner span, exclusive
* of the braces, or null when the member has no function body.
*/
export function memberBodySpan(source, valueStart) {
const unwrapped = unwrapAtomic(source, valueStart);
if (unwrapped !== -1) {
return wrappedFunctionBodySpan(source, unwrapped);
}
const paren = source.indexOf('(', valueStart);
if (paren === -1) return null;
const { end: parenEnd } = scanObjectLiteral(source, paren);
if (parenEnd === -1) return null;
let i = parenEnd + 1;
while (i < source.length && /\s/.test(source[i])) i++;
if (source[i] !== '{') return null;
const { end: braceEnd } = scanObjectLiteral(source, i);
if (braceEnd === -1) return null;
return { start: i + 1, end: braceEnd };
}
/**
* Reads a member's parameter list.
*
* Slicing from the member name to the body and taking everything between the
* first `(` and the last `)` works for `addQty(id, n) {`, and breaks the
* moment a wrapper puts a paren in front of it: for
* `addQty: atomic(function (id, n) {` it yields `function (id, n`. Scanning
* backwards from the body brace instead finds the same parameter list in
* every shape, wrapper or not, including an arrow's.
*
* Parameters matter more than they look. `addBridgeUnit` hands them to the
* analyser as locals; without them every parameter reads as an unknown
* identifier, and an unknown identifier blocks a diagnostic.
* @param {string} source - The module source.
* @param {number} bodyStart - Offset just inside the body's `{`.
* @returns {string} The parameter text, without its parentheses. Empty when
* the member takes no parameters.
*/
export function memberParams(source, bodyStart) {
let i = bodyStart - 2; // step back over the `{`
while (i >= 0 && /\s/.test(source[i])) i--;
// An arrow sits between the parameter list and the body.
if (i >= 1 && source[i] === '>' && source[i - 1] === '=') {
i -= 2;
while (i >= 0 && /\s/.test(source[i])) i--;
}
if (i < 0) return '';
if (source[i] !== ')') {
// `n => { ... }` — one parameter, written without parentheses.
const end = i;
while (i >= 0 && /[\w$]/.test(source[i])) i--;
const name = source.slice(i + 1, end + 1);
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : '';
}
let depth = 0;
for (let j = i; j >= 0; j--) {
const char = source[j];
if (char === ')') {
depth++;
} else if (char === '(') {
depth--;
if (depth === 0) return source.slice(j + 1, i);
}
}
return '';
}
/**
* Finds the `bridge(` call that produces the module's default export.
* @param {string} source - The module source.
* @returns {number} Index of the `{` opening the definition object, or -1.
*/
function findDefinitionBrace(source) {
const callRegex = /(^|[^\w$.])bridge\s*\(/g;
let match;
while ((match = callRegex.exec(source)) !== null) {
const parenIndex = source.indexOf('(', match.index + match[0].length - 1);
let cursor = parenIndex + 1;
while (cursor < source.length && /\s/.test(source[cursor])) cursor++;
if (source[cursor] === '{') {
return cursor;
}
}
return -1;
}
/**
* Extracts every event name emitted with a literal string, e.g. `this.emit('login')`.
* @param {string} source - The module source.
* @returns {string[]} Unique event names in source order.
*/
export function extractEmittedEvents(source) {
const events = new Set();
const regex = /\bemit\s*\(\s*(['"`])([^'"`\\]+)\1/g;
let match;
while ((match = regex.exec(source)) !== null) {
events.add(match[2]);
}
return [...events];
}
/**
* Extracts `<identifier>.on('event', ...)` subscriptions from source.
* @param {string} source - The source to scan.
* @returns {Array<{ target: string, event: string }>} The subscriptions found.
*/
export function extractSubscriptions(source) {
const found = [];
const regex = /\b([A-Za-z_$][\w$]*)\s*\.\s*on\s*\(\s*(['"`])([^'"`\\]+)\2/g;
let match;
while ((match = regex.exec(source)) !== null) {
found.push({ target: match[1], event: match[3] });
}
return found;
}
/**
* Replaces comments with spaces, preserving length and line structure.
* @param {string} source - The module source.
* @returns {string} The source with comments blanked.
*/
function blankComments(source) {
let out = '';
let i = 0;
while (i < source.length) {
const ch = source[i];
if (ch === '/' && source[i + 1] === '/') {
let j = i;
while (j < source.length && source[j] !== '\n') j++;
out += ' '.repeat(j - i);
i = j;
continue;
}
if (ch === '/' && source[i + 1] === '*') {
let j = i + 2;
while (j < source.length && !(source[j] === '*' && source[j + 1] === '/')) j++;
j = Math.min(j + 2, source.length);
out += source.slice(i, j).replace(/[^\n]/g, ' ');
i = j;
continue;
}
if (ch === '"' || ch === "'" || ch === '`') {
const quote = ch;
let j = i + 1;
while (j < source.length && source[j] !== quote) {
if (source[j] === '\\') j++;
j++;
}
j = Math.min(j + 1, source.length);
out += source.slice(i, j);
i = j;
continue;
}
out += ch;
i++;
}
return out;
}
/**
* Parses the import statements of a module.
* @param {string} source - The module source.
* @returns {Array<{ statement: string, specifier: string, defaultName: string|null, named: string[] }>}
* One entry per import statement.
*/
export function parseImports(source) {
const imports = [];
// Comments are blanked out first, keeping every offset and newline, so a
// documented example — the scaffolded bridge contains one — cannot be read
// as a real dependency. Before this, a bridge whose JSDoc showed how to
// import it declared an import of itself.
const scannable = blankComments(source);
const regex = /import\s+(?:([\s\S]*?)\s+from\s+)?['"]([^'"]+)['"];?/g;
let match;
while ((match = regex.exec(scannable)) !== null) {
const clause = (match[1] || '').trim();
const specifier = match[2];
let defaultName = null;
const named = [];
if (clause) {
const namedMatch = clause.match(/\{([\s\S]*?)\}/);
if (namedMatch) {
for (const part of namedMatch[1].split(',')) {
const cleaned = part.trim();
if (cleaned) {
named.push(cleaned.split(/\s+as\s+/)[0].trim());
}
}
}
const beforeBrace = clause.split('{')[0].replace(/,\s*$/, '').trim();
if (beforeBrace && !beforeBrace.startsWith('*')) {
defaultName = beforeBrace;
}
}
imports.push({ statement: source.slice(match.index, match.index + match[0].length), specifier, defaultName, named });
}
return imports;
}
/**
* Resolves a relative import specifier to a bridge module path, if it is one.
* @param {string} fromFile - The importing file.
* @param {string} specifier - The import specifier.
* @returns {string|null} Absolute path to the `.bridge.js` file, or null.
*/
export function resolveBridgeSpecifier(fromFile, specifier) {
if (!specifier || !specifier.startsWith('.')) {
return null;
}
const base = path.resolve(path.dirname(fromFile), specifier);
// A bridge import is explicit: the specifier names a `.bridge.js` module.
// Anything else (a child component, a helper) is not a bridge, and guessing
// otherwise would misread ordinary imports.
if (base.endsWith('.bridge.js')) {
return base;
}
if (base.endsWith('.bridge')) {
return `${base}.js`;
}
return null;
}
/**
* Collects the bridge imports of any module (component, page, bridge or main).
* @param {string} filePath - Absolute path to the importing file.
* @param {string} source - Its source text.
* @returns {Array<{ local: string, specifier: string, resolved: string }>} One entry
* per default-imported bridge.
*/
export function findBridgeImports(filePath, source) {
const results = [];
for (const entry of parseImports(source)) {
const resolved = resolveBridgeSpecifier(filePath, entry.specifier);
if (resolved && entry.defaultName) {
results.push({ local: entry.defaultName, specifier: entry.specifier, resolved });
}
}
return results;
}
/**
* Analyses a bridge module.
*
* A bridge module imports `bridge` from the Avenx runtime and exports the
* definition object it returns. Anything else in a `*.bridge.js` file is not a
* bridge, and is reported as such so the caller can raise a build error rather
* than emit a module the runtime cannot use.
* @param {string} filePath - Absolute path to the `.bridge.js` file.
* @param {string} source - The module source.
* @returns {object|null} A descriptor of the bridge, or null when the module is
* not built on the `bridge()` factory.
*/
export function analyzeBridge(filePath, source) {
const name = bridgeNameFromFile(filePath);
const imports = parseImports(source);
const importsFactory = imports.some(
(entry) => entry.named.includes('bridge') && RUNTIME_SPECIFIER.test(entry.specifier),
);
const braceIndex = findDefinitionBrace(source);
if (!importsFactory || braceIndex === -1) {
return null;
}
const descriptor = {
filePath,
name,
binding: bridgeBindingName(name),
stateKeys: [],
actions: [],
getters: [],
hasSetup: false,
events: [],
bridgeImports: [],
/**
* Actions declared with the `atomic()` wrapper, in declaration order.
* @type {string[]}
*/
atomicActions: [],
/**
* Where each member sits in the source, for consumers that need more than
* its name. Additive: nothing that only reads `stateKeys`, `getters` or
* `actions` is affected.
* @type {Array<{name: string, kind: string, valueStart: number, body: {start: number, end: number}|null}>}
*/
members: [],
};
const { members } = parseObjectMembers(source, braceIndex);
for (const member of members) {
if (member.kind === 'getter') {
descriptor.getters.push(member.name);
descriptor.members.push({ ...member, body: memberBodySpan(source, member.valueStart) });
continue;
}
if (member.name === 'setup') {
descriptor.hasSetup = true;
descriptor.members.push({ ...member, body: memberBodySpan(source, member.valueStart) });
continue;
}
if (member.name === 'state' && member.kind === 'property') {
const stateBrace = source.indexOf('{', member.valueStart + 'state'.length);
if (stateBrace !== -1) {
const { members: stateMembers } = parseObjectMembers(source, stateBrace);
descriptor.stateKeys = stateMembers.map((entry) => entry.name);
for (const entry of stateMembers) {
descriptor.members.push({ ...entry, kind: 'state', body: null });
}
}
continue;
}
if (member.kind === 'action') {
descriptor.actions.push(member.name);
if (member.atomic) {
descriptor.atomicActions.push(member.name);
}
descriptor.members.push({ ...member, body: memberBodySpan(source, member.valueStart) });
}
}
descriptor.events = extractEmittedEvents(source);
for (const entry of imports) {
if (RUNTIME_SPECIFIER.test(entry.specifier)) {
continue;
}
const resolved = resolveBridgeSpecifier(filePath, entry.specifier);
if (resolved && entry.defaultName) {
descriptor.bridgeImports.push({ local: entry.defaultName, specifier: entry.specifier, resolved });
}
// Anything else -- an npm package, a local helper -- used to be collected
// as an unsupported import and fail the build with AVX_C09, because the
// concatenator had no way to inline it. The bundler resolves it like any
// other specifier, so the only imports that fail now are the ones that
// resolve to nothing, and they fail with the reason.
}
return descriptor;
}
/**
* Reads and analyses a bridge module from disk.
* @param {string} filePath - Absolute path to the `.bridge.js` file.
* @param {function(string): string} [transform] - Optional source transform, used
* by the compiler to substitute environment variables.
* @returns {object|null} The descriptor, or null when the file cannot be read or
* is not built on the `bridge()` factory.
*/
export function analyzeBridgeFile(filePath, transform = (value) => value) {
if (!fs.existsSync(filePath)) {
return null;
}
return analyzeBridge(filePath, transform(fs.readFileSync(filePath, 'utf-8')));
}
/**
* Every member a consumer may legitimately read from a bridge instance.
* @param {object} descriptor - A bridge descriptor.
* @returns {string[]} The declared member names.
*/
export function declaredMembers(descriptor) {
return [...descriptor.stateKeys, ...descriptor.getters, ...descriptor.actions, 'on', '$dispose', '$name'];
}
/**
* Suggests the closest known name for a mistyped one, using edit distance.
* @param {string} name - The unknown name.
* @param {string[]} known - Candidate names.
* @returns {string} A " Did you mean ...?" fragment, or an empty string.
*/
export function suggestName(name, known) {
if (!name || !Array.isArray(known) || known.length === 0) {
return '';
}
const distance = (a, b) => {
const rows = [];
for (let i = 0; i <= a.length; i++) rows[i] = [i];
for (let j = 0; j <= b.length; j++) rows[0][j] = j;
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
rows[i][j] = Math.min(
rows[i - 1][j] + 1,
rows[i][j - 1] + 1,
rows[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
);
}
}
return rows[a.length][b.length];
};
let best = null;
let bestScore = Infinity;
const lower = name.toLowerCase();
for (const candidate of known) {
const score = distance(lower, String(candidate).toLowerCase());
if (score < bestScore) {
bestScore = score;
best = candidate;
}
}
// Only suggest when the names are genuinely close.
return best !== null && bestScore <= Math.max(2, Math.floor(name.length / 3))
? ` Did you mean "${best}"?`
: '';
}
|