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 | 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 91x 91x 91x 91x 91x 91x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 97x 79x 79x 18x 18x 20x 19x 18x 18x 18x 19x 30x 18x 18x 18x 18x 18x 18x 18x 18x 1x 1x 1x 1x 1x 1x 1x 18x 97x 278x 278x 278x 278x 278x 278x 278x 91x 463x 91x 91x 463x 91x | /**
* @file validate.js
* @description Proves that emitted JavaScript parses before the build is
* allowed to call itself successful.
*
* ## Why this is a separate, unconditional step
*
* The compiler could previously emit a bundle containing
*
* ```js
* const { AvenxGuard } = Avenx;
* const { AvenxGuard } = Avenx;
* ```
*
* and report `Build successful`. The generated file was not JavaScript; the
* application did not start; nothing in the pipeline noticed, because nothing
* in the pipeline ever asked whether the output it had just written could be
* parsed. That specific collision cannot happen any more -- every module is its
* own scope now, so two files declaring the same name simply coexist -- but a
* code generator that is only correct because its known failure modes have been
* fixed one at a time is a generator whose next failure ships silently.
*
* So this check is not about guards. It establishes the invariant:
*
* ```text
* avenx build reports success => the emitted JavaScript parses
* ```
*
* ## How
*
* `node:vm` compiles the source without running it. That is a real parse by the
* same engine that will parse the file in production, it needs no dependency,
* and it costs a few milliseconds on a bundle of this size.
*
* Validation happens against the staging directory, before promotion, so a
* bundle that fails never reaches `dist/` at all — the previous build's output
* is left intact rather than being replaced by something broken.
* @module lib/compiler/bundle/validate
*/
import vm from 'vm';
import { AvenxErrorCodes } from '../../core/runtime/AvenxError.js';
import { BuildError } from '../errors/index.js';
/**
* Extracts the offending line from a source, for the error message.
*
* A syntax error in a 190 KB single-line minified bundle is unreadable without
* an excerpt, and the line number alone points at a file the developer did not
* write. Showing the text is what makes the diagnostic actionable.
* @param {string} source - The emitted source.
* @param {number} lineNumber - 1-based line number.
* @returns {string} A trimmed excerpt, or an empty string.
*/
function excerpt(source, lineNumber) {
if (!Number.isInteger(lineNumber) || lineNumber < 1) return '';
const line = source.split(/\r?\n/)[lineNumber - 1];
if (typeof line !== 'string') return '';
const trimmed = line.trim();
return trimmed.length > 200 ? `${trimmed.slice(0, 200)}…` : trimmed;
}
/**
* Reads the line number out of a V8 syntax error's stack.
*
* V8 reports the position as `<filename>:<line>` on the stack's first line. It
* is not exposed as a structured property, so it has to be read back out.
* @param {Error} error - The thrown SyntaxError.
* @param {string} filename - The filename passed to the compiler.
* @returns {number} The 1-based line, or 0 when it cannot be determined.
*/
function lineFromStack(error, filename) {
const stack = typeof error.stack === 'string' ? error.stack : '';
const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = stack.match(new RegExp(`${escaped}:(\\d+)`));
return match ? Number(match[1]) : 0;
}
/**
* Asserts that a string of emitted JavaScript parses as a classic script.
*
* The bundle is a classic script, not a module: it is loaded with a plain
* `<script src>` tag, and every specifier in it has been resolved into the
* graph rather than left for a module loader. Parsing it as a script is
* therefore the same parse the browser will perform.
* @param {string} code - The emitted JavaScript.
* @param {string} artifact - The artifact name, used in the diagnostic.
* @throws {BuildError} AVX_C15 when the source does not parse.
*/
export function assertValidJavaScript(code, artifact) {
try {
// Compiling is enough: the script is never run, so no application code
// executes during a build.
new vm.Script(code, { filename: artifact });
} catch (error) {
if (!(error instanceof SyntaxError)) {
throw error;
}
const line = lineFromStack(error, artifact);
throw new BuildError(
AvenxErrorCodes.COMPILER_INVALID_OUTPUT,
artifact,
error.message,
line > 0 ? String(line) : 'unknown',
excerpt(code, line) || '(source line unavailable)',
);
}
}
/**
* Asserts that the bundle carries the runtime capabilities the build linked.
*
* The compiler decides what a bundle needs from what it saw while compiling:
* a template the IR refused puts the string renderer into the entry graph, and
* the build then tells the developer, through AVX_W47, that their component
* renders through it. That sentence has to be true of the artifact, not just of
* the intent.
*
* It stopped being true once. The string renderer registers itself into a
* module-scoped registry, and a build that linked the same runtime file twice
* -- which a symlinked `avenx-core` used to cause -- filled one copy of that
* registry while the component consulted the other. The build reported success
* and the application rendered nothing.
*
* Resolution no longer produces two copies of a file, so that specific cause is
* gone. This check is here because the property is worth asserting on the
* output regardless of which defect might break it next: a capability the build
* committed to is either in the bundle or the build fails.
* @param {Map<string, string>} outputs - Artifact name to contents.
* @param {Array<{capability: string, reason: string, evidence: string}>} required
* - Capabilities the build linked. `evidence` is a pattern source that can
* only match if the capability was linked *and* executed, never merely
* defined.
* @throws {BuildError} AVX_C23 when a linked capability is absent.
*/
export function assertRuntimeCapabilities(outputs, required) {
if (!Array.isArray(required) || required.length === 0) {
return;
}
let bundle = null;
for (const [name, contents] of outputs) {
if (name.endsWith('.js') && !name.endsWith('.map') && typeof contents === 'string') {
bundle = contents;
break;
}
}
if (bundle === null) {
return;
}
for (const { capability, reason, evidence } of required) {
// A pattern rather than a substring, because the obvious substring is the
// wrong test: `installStringRenderer(` matches the function's own
// declaration, which survives in the bundle whenever anything imports the
// registry -- and `AvenxComponent` always does, to read from it. The
// evidence has to distinguish the call from the definition.
if (!new RegExp(evidence).test(bundle)) {
throw new BuildError(
AvenxErrorCodes.COMPILER_MISSING_RUNTIME_CAPABILITY,
capability,
reason,
evidence,
);
}
}
}
/**
* Validates every JavaScript artifact in a build's output set.
* @param {Map<string, string>} outputs - Artifact name to contents.
* @throws {BuildError} When any `.js` artifact does not parse.
*/
export function assertValidOutputs(outputs) {
for (const [name, contents] of outputs) {
if (name.endsWith('.js') && typeof contents === 'string') {
assertValidJavaScript(contents, name);
}
}
}
|