@entrorove/skill 0.1.70 → 0.1.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/SKILL.md +2 -2
- package/local-budget-reads.mjs +7 -2
- package/local-budget.mjs +3 -2
- package/local-scope-build-data.mjs +73 -0
- package/local-scope-build-facts.mjs +33 -0
- package/local-scope-build-io.mjs +57 -0
- package/local-scope-build-read.mjs +22 -0
- package/local-scope-build-readset.mjs +49 -0
- package/local-scope-files.mjs +20 -7
- package/local-scope-repository.mjs +47 -37
- package/local-scope-signature.mjs +6 -4
- package/local-scope-state.mjs +7 -5
- package/local-scope-uni-build.mjs +69 -119
- package/local-scope-uni-conditional.mjs +8 -6
- package/local-scope-uni-directives.mjs +5 -5
- package/local-scope-uni-expressions.mjs +10 -5
- package/local-scope-workspaces.mjs +3 -3
- package/manifest.json +1 -1
- package/package.json +8 -2
- package/release-source-groups.mjs +39 -0
- package/release-state.json +23 -17
- package/release.mjs +4 -39
- package/releases.json +6 -0
- package/scope-context-observer.mjs +3 -2
|
@@ -5,7 +5,7 @@ import { promisify } from 'node:util';
|
|
|
5
5
|
import { builtinModules } from 'node:module';
|
|
6
6
|
import ts from 'typescript';
|
|
7
7
|
import { parse as parseVue, parseCache as vueParseCache } from '@vue/compiler-sfc';
|
|
8
|
-
import { lineCount, observedScopeRisk, packageBoundary, scopeFail, scopeFiles, scopeProbeTarget, scopeRead, scopeTarget, stableScope } from './local-scope-files.mjs';
|
|
8
|
+
import { lineCount, observedScopeRisk, packageBoundary, scopeFail, scopeFiles, scopeProbeTarget, scopeRead, scopeBuildSourceRead, scopeTarget, stableScope } from './local-scope-files.mjs';
|
|
9
9
|
import { sha256 } from './local-integrity.mjs';
|
|
10
10
|
import { workspaceImport, workspaceIndex } from './local-scope-workspaces.mjs';
|
|
11
11
|
import { assertBudgetRunnerAllowed } from './local-budget-reads.mjs';
|
|
@@ -19,6 +19,8 @@ import { createScopeRequireContexts } from './local-scope-create-require.mjs';
|
|
|
19
19
|
import { resolveCreatedRequire } from './local-scope-require-resolution.mjs';
|
|
20
20
|
import { uniConditionalVariants } from './local-scope-uni-conditional.mjs';
|
|
21
21
|
import { createUniBuildInspection } from './local-scope-uni-build.mjs';
|
|
22
|
+
import { createScopeBuildIO } from './local-scope-build-io.mjs';
|
|
23
|
+
import { observeBuildFacts } from './local-scope-build-facts.mjs';
|
|
22
24
|
|
|
23
25
|
const execute = promisify(execFile);
|
|
24
26
|
const inspectionFreshness = new WeakMap();
|
|
@@ -244,6 +246,9 @@ function declarationModules(content, from) {
|
|
|
244
246
|
&& node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DeclareKeyword)).map(node => node.name.text));
|
|
245
247
|
}
|
|
246
248
|
async function resolveImport(root, from, specifier, workspace, input, aliases, configPath, declarations) {
|
|
249
|
+
if (specifier.startsWith('/') || /^[A-Za-z]:/.test(specifier) || specifier.includes('\\') || /[\u0000-\u001f\u007f]/.test(specifier)) {
|
|
250
|
+
scopeFail('ENTROROVE_PATH_UNSAFE', 'Source module addresses require safe repository-relative or declared package selectors');
|
|
251
|
+
}
|
|
247
252
|
if (/^(?:https?:|file:|data:)/i.test(specifier)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'remote source imports cannot be inspected as local facts');
|
|
248
253
|
if (declarations.has(specifier)) return null;
|
|
249
254
|
const label = /^[A-Za-z0-9@._/~+-]{1,256}$/.test(specifier) ? specifier : '[unsupported specifier]';
|
|
@@ -258,9 +263,9 @@ async function resolveImport(root, from, specifier, workspace, input, aliases, c
|
|
|
258
263
|
const localPackage = !specifier.startsWith('.') ? workspaceImport(workspace, specifier) : null;
|
|
259
264
|
if (!specifier.startsWith('.') && !localPackage) {
|
|
260
265
|
if (builtinModules.includes(specifier) || (specifier.startsWith('node:') && builtinModules.includes(specifier.slice(5)))) return null;
|
|
261
|
-
const boundary = await packageBoundary(root, configPath ?? from, input);
|
|
266
|
+
const boundary = await packageBoundary(root, configPath ?? from, input, scopeBuildSourceRead);
|
|
262
267
|
if (boundary.manifestPath) {
|
|
263
|
-
const manifest = JSON.parse((await
|
|
268
|
+
const manifest = JSON.parse((await scopeBuildSourceRead(root, boundary.manifestPath, input)).content.toString('utf8'));
|
|
264
269
|
const name = specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : specifier.split('/')[0];
|
|
265
270
|
if (['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].some(field => Object.hasOwn(manifest[field] ?? {}, name))) return null;
|
|
266
271
|
}
|
|
@@ -284,25 +289,29 @@ async function externalConfigReference(root, relative, resolved) {
|
|
|
284
289
|
async function configMetadata(root, relative, input) {
|
|
285
290
|
const target = await scopeProbeTarget(root, relative, dependencyReference(relative));
|
|
286
291
|
if (!target.exists || !target.stat.isFile()) throw Object.assign(new Error(`Missing configuration file: ${relative}`), { code: 'ENOENT' });
|
|
287
|
-
const read = await
|
|
292
|
+
const read = await scopeBuildSourceRead(root, relative, input), resolved = await fs.realpath(target.target);
|
|
288
293
|
const physical = path.relative(root, resolved).split(path.sep).join('/');
|
|
289
294
|
const outside = physical === '..' || physical.startsWith('../') || path.isAbsolute(physical);
|
|
290
295
|
const realPath = outside ? relative : physical;
|
|
291
296
|
const referenceRoot = outside ? await externalConfigReference(root, relative, resolved) : undefined;
|
|
292
|
-
return { path: relative, sha256: read.sha256, contentBase64: read.content.toString('base64'), realPath, referenceRoot };
|
|
293
|
-
}
|
|
294
|
-
async function conditionalBuildDirectory(root, relative, maximumEntries) {
|
|
295
|
-
const target = relative === '.' ? root : (await scopeTarget(root, relative, false, dependencyReference(relative))).target;
|
|
296
|
-
const directory = await fs.opendir(target); const environmentFiles = []; let entries = 0;
|
|
297
|
-
for await (const entry of directory) {
|
|
298
|
-
if (++entries > maximumEntries) scopeFail('ENTROROVE_SCOPE_LIMIT', 'Conditional build directory exceeds its bounded entry inspection');
|
|
299
|
-
if (entry.name === '.env' || entry.name.startsWith('.env.')) environmentFiles.push(entry.name);
|
|
300
|
-
}
|
|
301
|
-
return { environmentFiles, entries };
|
|
297
|
+
return { path: relative, sha256: read.sha256, mode: read.mode, contentBase64: read.content.toString('base64'), realPath, referenceRoot };
|
|
302
298
|
}
|
|
303
299
|
function conditionalBuildInspection(root, input) {
|
|
304
|
-
|
|
305
|
-
|
|
300
|
+
const io = createScopeBuildIO(root, relative => configMetadata(root, relative, input));
|
|
301
|
+
return createUniBuildInspection(io.read, io.inspectDirectory, { probe: io.probe, repositoryRoot: root });
|
|
302
|
+
}
|
|
303
|
+
function sourceProjection(content, relative, root) {
|
|
304
|
+
// Only these pure parsers run here. Filesystem, privacy and budget checks occur
|
|
305
|
+
// outside this boundary and cannot become a semantic uncertainty.
|
|
306
|
+
try {
|
|
307
|
+
const conditional = /\.vue$/i.test(relative) ? uniConditionalVariants(content.toString('utf8'), relative) : undefined;
|
|
308
|
+
return { conditional, imports: importSpecifiers(content, relative, root, conditional) };
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error.code !== 'ENTROROVE_IMPORT_UNSUPPORTED') throw error;
|
|
311
|
+
return { conditional: undefined, imports: { specifiers: [], requires: [], dynamic: true,
|
|
312
|
+
unresolvedImports: [{ line: error.line ?? 1, column: error.column ?? 1, reason: 'source-projection-unsupported',
|
|
313
|
+
message: 'Source syntax has no supported static projection; the original hashed source remains read-only' }] } };
|
|
314
|
+
}
|
|
306
315
|
}
|
|
307
316
|
async function resolveLocalImport(root, relative, required = true) {
|
|
308
317
|
if (relative.startsWith('../') || relative === '..') scopeFail('ENTROROVE_PATH_UNSAFE', 'local import escapes the repository');
|
|
@@ -318,6 +327,8 @@ async function resolveLocalImport(root, relative, required = true) {
|
|
|
318
327
|
}
|
|
319
328
|
export async function importGraph(root, originals, input = { repositoryRoot: root }, limits = {}) {
|
|
320
329
|
const pending = originals.filter(file => file.exists !== false).map(file => file.path); const visited = new Map(); const edges = []; let dynamic = false;
|
|
330
|
+
const explicitSources = new Set(pending);
|
|
331
|
+
const graphRead = (actualRoot, relative, binding) => (explicitSources.has(relative) ? scopeRead : scopeBuildSourceRead)(actualRoot, relative, binding);
|
|
321
332
|
let sourceBytes = 0;
|
|
322
333
|
const dynamicSources = [], unresolvedImports = [];
|
|
323
334
|
const pendingCreates = new Set(originals.filter(file => file.exists === false && input.scopeCreatePaths?.includes(file.path)).map(file => file.path));
|
|
@@ -334,7 +345,7 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
334
345
|
}
|
|
335
346
|
sourceBytes += target.stat.size;
|
|
336
347
|
}
|
|
337
|
-
const read = await
|
|
348
|
+
const read = await graphRead(root, relative, input); const boundary = await packageBoundary(root, relative, input, scopeBuildSourceRead);
|
|
338
349
|
visited.set(relative, { path: relative, sha256: read.sha256, module: boundary.path, boundary });
|
|
339
350
|
if (!sourceExtension.test(relative)) continue;
|
|
340
351
|
const contexts = (input.importContexts ?? []).filter(context => relative === context.sourcePrefix || relative.startsWith(`${context.sourcePrefix}/`))
|
|
@@ -342,20 +353,23 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
342
353
|
const configPath = contexts[0]?.configPath;
|
|
343
354
|
const aliases = await scopeTsconfigImports(target => configMetadata(root, target, input), relative, configPath);
|
|
344
355
|
for (const observed of aliases.observations) metadata.set(observed.path, observed);
|
|
345
|
-
const
|
|
346
|
-
|
|
356
|
+
const projection = sourceProjection(read.content, relative, root), conditional = projection.conditional;
|
|
357
|
+
const build = await inspectBuild(relative, configPath, { hasConditionals: Boolean(conditional?.directives.length) });
|
|
358
|
+
const opaque = await observeBuildFacts(root, build, input, { visited, metadata, dynamicSources, unresolvedImports });
|
|
359
|
+
dynamic ||= opaque;
|
|
347
360
|
const declarations = declarationModules(read.content, relative);
|
|
348
|
-
const imports =
|
|
361
|
+
const imports = projection.imports; dynamic ||= imports.dynamic;
|
|
349
362
|
unresolvedImports.push(...imports.unresolvedImports.map(site => ({ path: relative, ...site })));
|
|
350
|
-
if (imports.dynamic) dynamicSources.push({ path: relative, sha256: read.sha256 });
|
|
363
|
+
if (imports.dynamic && !dynamicSources.some(item => item.path === relative)) dynamicSources.push({ path: relative, sha256: read.sha256 });
|
|
351
364
|
for (const specifier of imports.specifiers.sort()) {
|
|
352
365
|
let target;
|
|
353
366
|
try { target = await resolveImport(root, relative, specifier, workspace, input, aliases, configPath, declarations); }
|
|
354
367
|
catch (error) {
|
|
355
368
|
const missing = error.code === 'ENTROROVE_IMPORT_UNRESOLVED' && specifier.startsWith('.')
|
|
356
369
|
? error.missingCandidates?.filter(candidate => pendingCreates.has(candidate)) : null;
|
|
357
|
-
if (
|
|
358
|
-
unresolvedImports.push({ path: relative, specifier, reason: 'approved-create-not-present'
|
|
370
|
+
if (error.code !== 'ENTROROVE_IMPORT_UNRESOLVED') throw error;
|
|
371
|
+
unresolvedImports.push({ path: relative, specifier, reason: missing?.length ? 'approved-create-not-present' : 'source-import-unresolved',
|
|
372
|
+
candidatePaths: missing?.length ? missing : error.missingCandidates ?? [] });
|
|
359
373
|
dynamic = true;
|
|
360
374
|
if (!dynamicSources.some(source => source.path === relative)) dynamicSources.push({ path: relative, sha256: read.sha256 });
|
|
361
375
|
continue;
|
|
@@ -372,15 +386,16 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
372
386
|
}
|
|
373
387
|
}
|
|
374
388
|
const nodes = [...visited.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
375
|
-
await stableScope(root, nodes, input);
|
|
389
|
+
await stableScope(root, nodes, input, graphRead);
|
|
376
390
|
await inspectBuild.verify();
|
|
377
391
|
const workspaceMetadata = [...metadata.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
378
|
-
await stableScope(root, workspaceMetadata, input);
|
|
392
|
+
await stableScope(root, workspaceMetadata, input, scopeBuildSourceRead);
|
|
379
393
|
const graph = { nodes, edges: edges.sort((a, b) => `${a.from}:${a.specifier}`.localeCompare(`${b.from}:${b.specifier}`)),
|
|
380
394
|
dynamic, complete: !dynamic, dynamicSources: dynamicSources.sort((a, b) => a.path.localeCompare(b.path)), workspaceMetadata,
|
|
381
395
|
...(unresolvedImports.length ? { unresolvedImports } : {}) };
|
|
382
396
|
const actualFiles = [...nodes, ...workspaceMetadata].map(file => ({ path: file.path, sha256: file.sha256 }));
|
|
383
|
-
inspectionFreshness.set(graph, async () => { await inspectBuild.verify(); await stableScope(root, actualFiles, input
|
|
397
|
+
inspectionFreshness.set(graph, async () => { await inspectBuild.verify(); await stableScope(root, actualFiles, input,
|
|
398
|
+
(actualRoot, relative, binding) => metadata.has(relative) ? scopeBuildSourceRead(actualRoot, relative, binding) : graphRead(actualRoot, relative, binding)); });
|
|
384
399
|
return graph;
|
|
385
400
|
}
|
|
386
401
|
export async function scopeCandidateImports(root, file, input = { repositoryRoot: root }) {
|
|
@@ -389,10 +404,6 @@ export async function scopeCandidateImports(root, file, input = { repositoryRoot
|
|
|
389
404
|
.sort((a, b) => b.sourcePrefix.length - a.sourcePrefix.length)[0];
|
|
390
405
|
const aliases = await scopeTsconfigImports(relative => configMetadata(root, relative, input), file.path, context?.configPath);
|
|
391
406
|
const conditional = /\.vue$/i.test(file.path) ? uniConditionalVariants(file.content, file.path) : undefined;
|
|
392
|
-
let inspectBuild;
|
|
393
|
-
if (conditional?.directives.length) {
|
|
394
|
-
inspectBuild = conditionalBuildInspection(root, input); await inspectBuild(file.path, context?.configPath);
|
|
395
|
-
}
|
|
396
407
|
const parsed = importSpecifiers(Buffer.from(file.content), file.path, root, conditional);
|
|
397
408
|
const declarations = declarationModules(Buffer.from(file.content), file.path), edges = [];
|
|
398
409
|
for (const specifier of parsed.specifiers) {
|
|
@@ -401,10 +412,9 @@ export async function scopeCandidateImports(root, file, input = { repositoryRoot
|
|
|
401
412
|
}
|
|
402
413
|
for (const request of parsed.requires) {
|
|
403
414
|
const resolved = await resolveCreatedRequire(file.path, request.anchor, request.specifier, target => configMetadata(root, target, input));
|
|
404
|
-
await stableScope(root, resolved.metadata, input);
|
|
415
|
+
await stableScope(root, resolved.metadata, input, scopeBuildSourceRead);
|
|
405
416
|
edges.push({ from: file.path, to: resolved.external ? null : resolved.target, external: resolved.external, specifier: request.specifier });
|
|
406
417
|
}
|
|
407
|
-
await inspectBuild?.verify();
|
|
408
418
|
return { edges: edges.sort((a, b) => `${a.from}:${a.specifier}`.localeCompare(`${b.from}:${b.specifier}`)), dynamic: parsed.dynamic,
|
|
409
419
|
...(parsed.unresolvedImports.length ? { unresolvedImports: parsed.unresolvedImports.map(site => ({ path: file.path, ...site })) } : {}) };
|
|
410
420
|
}
|
|
@@ -417,17 +427,17 @@ export async function collectScopeFacts(root, requested, baselineRef, knownFiles
|
|
|
417
427
|
files.push({ path: relative, exists: false, sha256: sha256('entrorove.missing-file/1.0'), bytes: 0, lines: 0, module: previous.module });
|
|
418
428
|
continue;
|
|
419
429
|
}
|
|
420
|
-
const read = await scopeRead(root, relative, input); const boundary = await packageBoundary(root, relative, input);
|
|
430
|
+
const read = await scopeRead(root, relative, input); const boundary = await packageBoundary(root, relative, input, scopeBuildSourceRead);
|
|
421
431
|
files.push({ path: relative, exists: true, sha256: read.sha256, bytes: read.bytes, lines: lineCount(read.content), module: boundary.path });
|
|
422
432
|
}
|
|
423
433
|
const graph = await importGraph(root, files, input);
|
|
424
434
|
await contextEvidence(root, graph, input);
|
|
425
435
|
const verifyGraph = inspectionFreshness.get(graph), observedMetadata = graph.workspaceMetadata.map(file => ({ path: file.path, sha256: file.sha256 }));
|
|
426
|
-
inspectionFreshness.set(graph, async () => { await verifyGraph(); await stableScope(root, observedMetadata, input); });
|
|
436
|
+
inspectionFreshness.set(graph, async () => { await verifyGraph(); await stableScope(root, observedMetadata, input, scopeBuildSourceRead); });
|
|
427
437
|
const observedGit = await observeGitFacts(root, files, baselineRef, input); const history = observedGit.facts;
|
|
428
438
|
const modules = [...new Map(graph.nodes.map(node => [node.boundary.path, node.boundary])).values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
429
439
|
await stableScope(root, files, input);
|
|
430
|
-
await stableScope(root, modules.filter(module => module.manifestPath).map(module => ({ path: module.manifestPath, sha256: module.manifestDigest })), input);
|
|
440
|
+
await stableScope(root, modules.filter(module => module.manifestPath).map(module => ({ path: module.manifestPath, sha256: module.manifestDigest })), input, scopeBuildSourceRead);
|
|
431
441
|
if (history.available && (await scopeGitMetadata(root, ['rev-parse', '--verify', 'HEAD'], input, [128]))?.trim() !== history.head) scopeFail('ENTROROVE_SCOPE_CHANGED', 'Git HEAD changed during scope inspection');
|
|
432
442
|
const sourceLines = files.reduce((sum, file) => sum + file.lines, 0); const bytes = files.reduce((sum, file) => sum + file.bytes, 0);
|
|
433
443
|
// An actual current diff (including empty untracked files) is authoritative; clean Git uses history, unknown Git uses source lines.
|
|
@@ -452,12 +462,12 @@ async function contextEvidence(root, graph, input) {
|
|
|
452
462
|
if (!target.stat.isFile() || target.stat.size > 1_048_576 || (bytes += target.stat.size) > 8_388_608) {
|
|
453
463
|
scopeFail('ENTROROVE_SCOPE_LIMIT', 'Context evidence exceeds its bounded actual file inspection');
|
|
454
464
|
}
|
|
455
|
-
const read = await
|
|
465
|
+
const read = await scopeBuildSourceRead(root, file.path, input);
|
|
456
466
|
if (read.sha256 !== file.sha256 || metadata.has(file.path) && metadata.get(file.path).sha256 !== read.sha256) {
|
|
457
467
|
scopeFail('ENTROROVE_SCOPE_CHANGED', 'Actual context build evidence changed since its exact review');
|
|
458
468
|
}
|
|
459
469
|
if (!metadata.has(file.path)) metadata.set(file.path, { path: file.path, sha256: read.sha256 });
|
|
460
470
|
}
|
|
461
471
|
graph.workspaceMetadata = [...metadata.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
462
|
-
await stableScope(root, graph.workspaceMetadata, input);
|
|
472
|
+
await stableScope(root, graph.workspaceMetadata, input, scopeBuildSourceRead);
|
|
463
473
|
}
|
|
@@ -2,7 +2,7 @@ import { createPrivateKey, createPublicKey, randomUUID, sign } from 'node:crypto
|
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { canonical, sha256 } from './local-integrity.mjs';
|
|
5
|
-
import { scopeFail, scopeRead, scopeControlRead, scopeRoot, stableScope } from './local-scope-files.mjs';
|
|
5
|
+
import { scopeFail, scopeRead, scopeBuildSourceRead, scopeControlRead, scopeRoot, stableScope } from './local-scope-files.mjs';
|
|
6
6
|
import { collectScopeFacts, verifyScopeInspectionFreshness } from './local-scope-repository.mjs';
|
|
7
7
|
import { readScopeSnapshot, verifyFreshScopeSnapshot } from './local-scope-snapshot.mjs';
|
|
8
8
|
import { scopeGitMetadata } from './local-scope-git-reads.mjs';
|
|
@@ -51,10 +51,12 @@ async function signingChallenge(input, beforeSign, maximumExpiresAt) {
|
|
|
51
51
|
async function verifySigningFreshness(input, root, actual) {
|
|
52
52
|
await verifyScopeInspectionFreshness(actual);
|
|
53
53
|
await verifyFreshScopeSnapshot(input, root, actual);
|
|
54
|
-
|
|
55
|
-
await stableScope(root, actual.importGraph.
|
|
54
|
+
const explicit = new Set(actual.files.map(file => file.path));
|
|
55
|
+
await stableScope(root, actual.importGraph.nodes, input,
|
|
56
|
+
(actualRoot, relative, binding) => (explicit.has(relative) ? scopeRead : scopeBuildSourceRead)(actualRoot, relative, binding));
|
|
57
|
+
await stableScope(root, actual.importGraph.workspaceMetadata, input, scopeBuildSourceRead);
|
|
56
58
|
await stableScope(root, actual.modules.filter(module => module.manifestPath)
|
|
57
|
-
.map(module => ({ path: module.manifestPath, sha256: module.manifestDigest })), input);
|
|
59
|
+
.map(module => ({ path: module.manifestPath, sha256: module.manifestDigest })), input, scopeBuildSourceRead);
|
|
58
60
|
if (actual.git.available && (await scopeGitMetadata(root, ['rev-parse', '--verify', 'HEAD'], input, [128]))?.trim() !== actual.git.head) {
|
|
59
61
|
scopeFail('ENTROROVE_SCOPE_CHANGED', 'Git HEAD changed before the original context signature');
|
|
60
62
|
}
|
package/local-scope-state.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { canonical, sha256, verifySnapshotManifest } from './local-integrity.mjs';
|
|
7
7
|
import { withStateLock } from './local-state-lock.mjs';
|
|
8
|
-
import { scopeFail, scopeRead, scopeControlRead, scopeTarget, stableScope } from './local-scope-files.mjs';
|
|
8
|
+
import { scopeFail, scopeRead, scopeBuildSourceRead, scopeControlRead, scopeTarget, stableScope } from './local-scope-files.mjs';
|
|
9
9
|
|
|
10
10
|
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
11
11
|
const order = ['lock', 'probe', 'swarm'];
|
|
@@ -63,13 +63,13 @@ async function originalSnapshot(input, root, base) {
|
|
|
63
63
|
}
|
|
64
64
|
return { manifest, directory, digest: manifest.snapshotDigest };
|
|
65
65
|
}
|
|
66
|
-
async function capture(root, files, directory, input) {
|
|
66
|
+
async function capture(root, files, directory, input, readSource = scopeRead) {
|
|
67
67
|
for (const file of files) {
|
|
68
68
|
if (file.exists === false) {
|
|
69
69
|
if ((await scopeTarget(root, file.path, true)).exists) scopeFail('ENTROROVE_SCOPE_CHANGED', 'a deleted source reappeared before preservation');
|
|
70
70
|
continue;
|
|
71
71
|
}
|
|
72
|
-
const read = await
|
|
72
|
+
const read = await readSource(root, file.path, input);
|
|
73
73
|
if (read.sha256 !== file.sha256) scopeFail('ENTROROVE_SCOPE_CHANGED', 'scope source changed before snapshot preservation');
|
|
74
74
|
const destination = path.join(directory, file.path);
|
|
75
75
|
await fs.mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
@@ -107,16 +107,18 @@ function preservedFiles(facts) {
|
|
|
107
107
|
}
|
|
108
108
|
async function saveRevision(location, root, state, facts, mode, initial, input) {
|
|
109
109
|
const staged = await fs.mkdtemp(path.join(location.directory, '.revision-'));
|
|
110
|
+
const explicit = new Set(facts.files.map(file => file.path));
|
|
111
|
+
const readSource = (actualRoot, relative, binding) => (explicit.has(relative) ? scopeRead : scopeBuildSourceRead)(actualRoot, relative, binding);
|
|
110
112
|
const final = path.join(location.directory, `revision-${state.revision + 1}-${randomUUID()}`); let committed = false;
|
|
111
113
|
try {
|
|
112
114
|
const currentDirectory = path.join(staged, 'current'); await fs.mkdir(currentDirectory, { mode: 0o700 });
|
|
113
|
-
await capture(root, preservedFiles(facts), currentDirectory, input);
|
|
115
|
+
await capture(root, preservedFiles(facts), currentDirectory, input, readSource);
|
|
114
116
|
if (initial) {
|
|
115
117
|
const original = path.join(staged, 'original'); await fs.mkdir(original, { mode: 0o700 });
|
|
116
118
|
await preserveSnapshotCopies14(input, initial.directory, initial.manifest, original);
|
|
117
119
|
await fs.writeFile(path.join(original, 'manifest.json'), `${canonical(initial.manifest)}\n`, { flag: 'wx', mode: 0o600 });
|
|
118
120
|
}
|
|
119
|
-
await stableScope(root, preservedFiles(facts), input); await fs.rename(staged, final);
|
|
121
|
+
await stableScope(root, preservedFiles(facts), input, readSource); await fs.rename(staged, final);
|
|
120
122
|
const record = historyRecord(state, facts, path.join(final, 'current'), mode);
|
|
121
123
|
const next = { ...state, revision: record.revision, mode, history: [...state.history, record] };
|
|
122
124
|
if (initial) next.originalSnapshot = { ...next.originalSnapshot, directory: path.join(final, 'original') };
|
|
@@ -1,135 +1,85 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import ts from 'typescript';
|
|
3
2
|
import { uniConditionalError } from './local-scope-uni-expressions.mjs';
|
|
3
|
+
import { createBuildDataReadset } from './local-scope-build-readset.mjs';
|
|
4
|
+
import { buildConfigurationNames, buildDataPath, buildModuleData, buildScriptReferences } from './local-scope-build-data.mjs';
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
const tree = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS), imports = new Map();
|
|
12
|
-
if (tree.parseDiagnostics.length) throw fail('Conditional build configuration has invalid syntax');
|
|
13
|
-
let exported = false, nodes = 0;
|
|
14
|
-
const value = (node, depth = 0) => {
|
|
15
|
-
if (++nodes > 2048 || depth > 24) throw fail('Conditional build configuration exceeds its static inspection bound');
|
|
16
|
-
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) return value(node.expression, depth + 1);
|
|
17
|
-
if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)
|
|
18
|
-
|| [ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.NullKeyword].includes(node.kind)) return;
|
|
19
|
-
if (ts.isArrayLiteralExpression(node)) { for (const item of node.elements) value(item, depth + 1); return; }
|
|
20
|
-
if (ts.isObjectLiteralExpression(node)) {
|
|
21
|
-
for (const property of node.properties) {
|
|
22
|
-
if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name)) throw fail('Dynamic build properties cannot establish default conditional macros');
|
|
23
|
-
value(property.initializer, depth + 1);
|
|
24
|
-
}
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
28
|
-
const operation = imports.get(node.expression.text);
|
|
29
|
-
if (operation === 'uni' && node.arguments.length === 0) return;
|
|
30
|
-
if (operation === 'defineConfig' && node.arguments.length === 1) return value(node.arguments[0], depth + 1);
|
|
31
|
-
}
|
|
32
|
-
throw fail('Dynamic build configuration cannot establish default conditional macros without execution');
|
|
33
|
-
};
|
|
34
|
-
for (const statement of tree.statements) {
|
|
35
|
-
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
36
|
-
const module = statement.moduleSpecifier.text, clause = statement.importClause;
|
|
37
|
-
if (module === '@dcloudio/vite-plugin-uni' && clause?.name && !clause.namedBindings) imports.set(clause.name.text, 'uni');
|
|
38
|
-
else if (module === 'vite' && !clause?.name && clause?.namedBindings && ts.isNamedImports(clause.namedBindings)
|
|
39
|
-
&& clause.namedBindings.elements.every(element => (element.propertyName ?? element.name).text === 'defineConfig')) {
|
|
40
|
-
for (const element of clause.namedBindings.elements) imports.set(element.name.text, 'defineConfig');
|
|
41
|
-
} else throw fail('Custom build imports cannot establish default conditional macros without execution');
|
|
42
|
-
} else if (ts.isExportAssignment(statement) && !exported && !statement.isExportEquals) { exported = true; value(statement.expression); }
|
|
43
|
-
else if (ts.isExpressionStatement(statement) && !exported && ts.isBinaryExpression(statement.expression)
|
|
44
|
-
&& statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
45
|
-
&& statement.expression.left.getText(tree) === 'module.exports') { exported = true; value(statement.expression.right); }
|
|
46
|
-
else if (!ts.isEmptyStatement(statement)) throw fail('Custom build statements cannot establish default conditional macros without execution');
|
|
47
|
-
}
|
|
48
|
-
if (!exported) throw fail('Conditional build configuration requires a statically inspectable export');
|
|
6
|
+
const controlKeys = ['UNI_CUSTOM_DEFINE', 'UNI_CUSTOM_CONTEXT', 'UNI_SCRIPT_DEFINE', 'UNI_SCRIPT_ENV', 'UNI_COMPILER_VERSION',
|
|
7
|
+
'UNI_APP_X', 'UNI_APP_X_DOM2', 'UNI_UTS_PLATFORM', 'UNI_INPUT_DIR', 'VITE_ROOT_DIR', 'UNI_CLI_CONTEXT',
|
|
8
|
+
'UNI_HBUILDERX', 'UNI_HBUILDERX_PLUGINS', 'NODE_PATH', 'NODE_OPTIONS'];
|
|
9
|
+
function assertProcessControls(filename) {
|
|
10
|
+
const present = controlKeys.filter(key => Object.hasOwn(process.env, key) || Object.hasOwn(process, key));
|
|
11
|
+
if (present.length) throw uniConditionalError(filename, 0, `Build process override controls are unsupported: ${present.join(', ')}`);
|
|
49
12
|
}
|
|
50
|
-
function
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
for (const configuration of Object.values(scripts)) {
|
|
57
|
-
if (!configuration || typeof configuration !== 'object' || Array.isArray(configuration)) throw fail('Custom uni-app build entry is not statically inspectable');
|
|
58
|
-
if (configuration.define !== undefined && (!configuration.define || typeof configuration.define !== 'object'
|
|
59
|
-
|| Array.isArray(configuration.define) || Object.keys(configuration.define).length)) {
|
|
60
|
-
throw fail('Custom uni-app defines, including builtin macro overrides, require explicit scanner support');
|
|
61
|
-
}
|
|
62
|
-
if (controls.test(JSON.stringify(configuration))) throw fail('Custom conditional build environment requires explicit scanner support');
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
for (const [name, command] of Object.entries(manifest?.scripts ?? {})) {
|
|
66
|
-
if (typeof command !== 'string' || controls.test(command)) throw fail('Custom conditional build command requires explicit scanner support');
|
|
67
|
-
if (/^(?:dev|build)(?::|$)/.test(name) && (!/^(?:cross-env\s+NODE_ENV=(?:development|production|test)\s+)?(?:uni|vite|vue-cli-service\s+uni-(?:build|serve))(?:\s+[A-Za-z0-9_./:@=-]+)*$/.test(command)
|
|
68
|
-
|| /(?:^|\s)(?:--config(?:[=\s]|$)|-c(?:\s|$)|--define(?:[=\s]|$))/.test(command))) {
|
|
69
|
-
throw fail('Custom conditional build command requires explicit scanner support');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
13
|
+
function jsonData(item) {
|
|
14
|
+
let value; try { value = JSON.parse(Buffer.from(item.contentBase64, 'base64').toString('utf8')); }
|
|
15
|
+
catch { throw uniConditionalError(item.path, 0, 'Build package data is invalid JSON'); }
|
|
16
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || value.scripts !== undefined
|
|
17
|
+
&& (!value.scripts || typeof value.scripts !== 'object' || Array.isArray(value.scripts))) throw uniConditionalError(item.path, 0, 'Build package data requires an object and string script map');
|
|
18
|
+
return value;
|
|
72
19
|
}
|
|
73
|
-
function
|
|
74
|
-
let directory = path.posix.dirname(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
if (directory === '.') return;
|
|
20
|
+
async function hostData(sourcePath, configPath, state) {
|
|
21
|
+
let directory = path.posix.dirname(configPath ?? sourcePath);
|
|
22
|
+
for (let count = 0; count < 128; count++) {
|
|
23
|
+
const relative = path.posix.join(directory, 'package.json'), item = await state.observe(relative);
|
|
24
|
+
if (item) return { root: directory, item, manifest: jsonData(item) };
|
|
25
|
+
if (directory === '.') return { root: path.posix.dirname(configPath ?? sourcePath), item: null, manifest: {} };
|
|
79
26
|
directory = path.posix.dirname(directory);
|
|
80
27
|
}
|
|
28
|
+
throw uniConditionalError(sourcePath, 0, 'Build host data exceeds its bounded package ancestry');
|
|
81
29
|
}
|
|
82
|
-
function
|
|
83
|
-
const present = controlKeys.filter(key => Object.hasOwn(process.env, key) || Object.hasOwn(process, key));
|
|
84
|
-
if (present.length) throw uniConditionalError(filename, 0, `Conditional compilation override controls are unsupported: ${present.join(', ')}`);
|
|
85
|
-
}
|
|
30
|
+
function sourceText(item) { return Buffer.from(item.contentBase64, 'base64').toString('utf8'); }
|
|
86
31
|
export function createUniBuildInspection(read, inspectDirectory) {
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
32
|
+
const state = createBuildDataReadset(read, inspectDirectory), contexts = new Map(), modules = new Map(); let controlsRequired = false;
|
|
33
|
+
const inspectSource = async (sourcePath, configPath, context = { hasConditionals: true }) => {
|
|
34
|
+
const key = `${sourcePath}:${configPath ?? ''}:${Boolean(context.hasConditionals)}`;
|
|
35
|
+
if (contexts.has(key)) return contexts.get(key);
|
|
36
|
+
const host = await hostData(sourcePath, configPath, state), unresolved = new Map(), queue = [], visited = new Set();
|
|
37
|
+
const mark = (item, reason) => { if (item) unresolved.set(`${item.path}:${reason}`, { path: item.path, sha256: item.sha256, reason }); };
|
|
38
|
+
const enqueue = async (relative, reason) => {
|
|
39
|
+
const item = await state.observe(relative); if (item) { mark(item, reason); queue.push(item); } return item;
|
|
40
|
+
};
|
|
41
|
+
if (configPath) await state.observe(buildDataPath('.', configPath));
|
|
42
|
+
for (const name of buildConfigurationNames) await enqueue(buildDataPath(host.root, name), 'opaque-build-config');
|
|
43
|
+
const scripts = buildScriptReferences(host.manifest, host.item?.path);
|
|
44
|
+
if (scripts.opaque) mark(host.item, 'opaque-build-command');
|
|
45
|
+
for (const reference of scripts.references) await enqueue(buildDataPath(host.root, reference), 'opaque-build-loader');
|
|
46
|
+
if (host.manifest['uni-app']?.scripts !== undefined) mark(host.item, 'custom-build-definitions');
|
|
47
|
+
const roots = new Set([host.root]);
|
|
48
|
+
while (queue.length) {
|
|
49
|
+
const item = queue.shift(); if (visited.has(item.path)) continue; visited.add(item.path);
|
|
50
|
+
if (visited.size > 128) throw Object.assign(uniConditionalError(item.path, 0, 'Build source data exceeds its 128-module bound'), { code: 'ENTROROVE_SCOPE_LIMIT' });
|
|
51
|
+
if (/\.json$/.test(item.path) || /(?:^|\/)\.babelrc$/.test(item.path)) continue;
|
|
52
|
+
if (item.path.split('/').includes('node_modules')) continue;
|
|
53
|
+
if (!modules.has(item.path)) modules.set(item.path, buildModuleData(sourceText(item), item.path));
|
|
54
|
+
const parsed = modules.get(item.path);
|
|
55
|
+
for (const literal of parsed.literalRoots) {
|
|
56
|
+
const root = literal.root !== undefined ? buildDataPath(host.root, literal.root)
|
|
57
|
+
: literal.context !== undefined ? buildDataPath(host.root, literal.context) : host.root;
|
|
58
|
+
roots.add(literal.envDir === undefined ? root : buildDataPath(root, literal.envDir));
|
|
111
59
|
}
|
|
112
|
-
for (const
|
|
113
|
-
const
|
|
114
|
-
|
|
60
|
+
for (const reference of parsed.references) {
|
|
61
|
+
const target = buildDataPath(path.posix.dirname(item.path), reference);
|
|
62
|
+
const candidates = /\.[A-Za-z0-9]+$/.test(target) ? [target]
|
|
63
|
+
: ['.js', '.cjs', '.mjs', '.ts', '.mts', '.cts', '.json', '/index.js', '/index.cjs', '/index.ts'].map(extension => `${target}${extension}`);
|
|
64
|
+
for (const candidate of candidates) await enqueue(candidate, 'opaque-build-loader');
|
|
115
65
|
}
|
|
116
66
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for (const [relative, expected] of inspected) {
|
|
128
|
-
let actual;
|
|
129
|
-
try { actual = await read(relative); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
130
|
-
if ((actual?.sha256 ?? null) !== (expected?.sha256 ?? null)) throw Object.assign(
|
|
131
|
-
uniConditionalError(relative, 0, 'Conditional build metadata changed during inspection'), { code: 'ENTROROVE_SCOPE_CHANGED' });
|
|
67
|
+
if (unresolved.size || context.hasConditionals) {
|
|
68
|
+
controlsRequired = true; assertProcessControls(sourcePath);
|
|
69
|
+
for (const root of roots) {
|
|
70
|
+
const listing = await state.directory(root);
|
|
71
|
+
if (listing?.environmentFiles.length) {
|
|
72
|
+
const origin = [...visited].map(path => state.observations.find(item => item.path === path)).find(Boolean) ?? host.item;
|
|
73
|
+
if (origin) mark(origin, 'build-environment-present');
|
|
74
|
+
else mark(await state.observe(sourcePath), 'build-environment-present');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
132
77
|
}
|
|
78
|
+
const proof = { sourceProjection: 'raw-source', buildObservation: unresolved.size ? 'opaque' : 'not-observed',
|
|
79
|
+
get observations() { return state.observations; },
|
|
80
|
+
unresolvedBuildSources: [...unresolved.values()].sort((a, b) => `${a.path}:${a.reason}`.localeCompare(`${b.path}:${b.reason}`)) };
|
|
81
|
+
contexts.set(key, proof); return proof;
|
|
133
82
|
};
|
|
83
|
+
inspectSource.verify = async () => { if (controlsRequired) assertProcessControls('build-data'); await state.verify(); };
|
|
134
84
|
return inspectSource;
|
|
135
85
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { uniDirectives } from './local-scope-uni-directives.mjs';
|
|
2
|
-
import { uniCondition, uniConditionContexts, uniConditionalError } from './local-scope-uni-expressions.mjs';
|
|
2
|
+
import { uniCondition, uniConditionContexts, uniConditionalError, uniConditionalLimitError } from './local-scope-uni-expressions.mjs';
|
|
3
3
|
|
|
4
4
|
const blank = source => source.replace(/[^\r\n]/g, ' ');
|
|
5
5
|
function conditionalSegments(source, directives, filename) {
|
|
@@ -8,10 +8,11 @@ function conditionalSegments(source, directives, filename) {
|
|
|
8
8
|
segments.push({ start: cursor, end: item.start, guards: stack.map(guard => ({ ...guard })) });
|
|
9
9
|
segments.push({ start: item.start, end: item.end, directive: true }); cursor = item.end;
|
|
10
10
|
const fail = reason => uniConditionalError(filename, item.start, reason, source);
|
|
11
|
+
const limit = reason => uniConditionalLimitError(filename, item.start, reason, source);
|
|
11
12
|
if (['ifdef', 'ifndef', 'if'].includes(item.kind)) {
|
|
12
|
-
if (stack.length >= 32) throw
|
|
13
|
+
if (stack.length >= 32) throw limit('Conditional compilation exceeds its 32-level nesting bound');
|
|
13
14
|
const condition = uniCondition(item.expression, fail), index = conditions.length;
|
|
14
|
-
if ((conditionTokens += condition.tokens) > 4096) throw
|
|
15
|
+
if ((conditionTokens += condition.tokens) > 4096) throw limit('Conditional expressions exceed their 4096-token aggregate inspection bound');
|
|
15
16
|
conditions.push(condition); stack.push({ index, negated: item.kind === 'ifndef', syntax: item.syntax, alternative: false });
|
|
16
17
|
} else {
|
|
17
18
|
const active = stack.at(-1);
|
|
@@ -29,15 +30,16 @@ function conditionalSegments(source, directives, filename) {
|
|
|
29
30
|
const activeSegment = (segment, key) => !segment.directive && segment.guards.every(guard => (key[guard.index] === '1') !== guard.negated);
|
|
30
31
|
export function uniConditionalVariants(source, filename) {
|
|
31
32
|
const fail = reason => uniConditionalError(filename, 0, reason, source);
|
|
33
|
+
const limit = reason => uniConditionalLimitError(filename, 0, reason, source);
|
|
32
34
|
if (typeof source !== 'string') throw fail('Conditional inspection requires actual source text');
|
|
33
35
|
const unchanged = () => ({ directives: [], variants: [{ source, key: '', label: 'unconditional' }] });
|
|
34
36
|
if (!/(?:<!--|\/\/|\/\*)[ \t]*#/i.test(source)) return unchanged();
|
|
35
|
-
if (Buffer.byteLength(source) > 2_097_152) throw
|
|
37
|
+
if (Buffer.byteLength(source) > 2_097_152) throw limit('Conditional source exceeds its 2 MiB inspection bound');
|
|
36
38
|
try {
|
|
37
39
|
const directives = uniDirectives(source, filename);
|
|
38
40
|
if (!directives.length) return unchanged();
|
|
39
41
|
const { conditions, segments } = conditionalSegments(source, directives, filename), contexts = uniConditionContexts(conditions, fail);
|
|
40
|
-
if (Buffer.byteLength(source) * contexts.length > 33_554_432) throw
|
|
42
|
+
if (Buffer.byteLength(source) * contexts.length > 33_554_432) throw limit('Conditional source variants exceed their 32 MiB inspection bound');
|
|
41
43
|
for (const segment of segments) if (!segment.directive && source.slice(segment.start, segment.end).trim()
|
|
42
44
|
&& !contexts.some(context => activeSegment(segment, context.key))) {
|
|
43
45
|
throw uniConditionalError(filename, segment.start, 'Conditional branch has no proven platform context; custom or contradictory build definitions require explicit support', source);
|
|
@@ -48,7 +50,7 @@ export function uniConditionalVariants(source, filename) {
|
|
|
48
50
|
}).join('') }));
|
|
49
51
|
return { directives: directives.map(({ kind, expression, syntax }) => ({ kind, expression, syntax })), variants };
|
|
50
52
|
} catch (error) {
|
|
51
|
-
if (error instanceof RangeError) throw
|
|
53
|
+
if (error instanceof RangeError) throw limit('Conditional syntax exceeds its parser or traversal bound');
|
|
52
54
|
throw error;
|
|
53
55
|
}
|
|
54
56
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import { uniConditionalError } from './local-scope-uni-expressions.mjs';
|
|
2
|
+
import { uniConditionalError, uniConditionalLimitError } from './local-scope-uni-expressions.mjs';
|
|
3
3
|
|
|
4
4
|
function directive(source, start, end, text, syntax, filename, output) {
|
|
5
5
|
const match = /^\s*#([a-z]+)\b([\s\S]*)$/i.exec(text);
|
|
@@ -10,15 +10,15 @@ function directive(source, start, end, text, syntax, filename, output) {
|
|
|
10
10
|
|| ['else', 'endif'].includes(kind) && expression) {
|
|
11
11
|
throw uniConditionalError(filename, start, 'Conditional directive syntax is unsupported', source);
|
|
12
12
|
}
|
|
13
|
-
if (output.length >= 256) throw
|
|
13
|
+
if (output.length >= 256) throw uniConditionalLimitError(filename, start, 'Conditional directives exceed their 256-entry bound', source);
|
|
14
14
|
output.push({ start, end, kind, expression, syntax });
|
|
15
15
|
}
|
|
16
|
-
function literalRanges(content, kind) {
|
|
16
|
+
function literalRanges(content, kind, filename, start, source) {
|
|
17
17
|
const parsed = ts.createSourceFile('uni-script.ts', content, ts.ScriptTarget.Latest, true, kind);
|
|
18
18
|
const pending = [parsed], ranges = []; let nodes = 0;
|
|
19
19
|
while (pending.length) {
|
|
20
20
|
const node = pending.pop();
|
|
21
|
-
if (++nodes > 200_000) throw
|
|
21
|
+
if (++nodes > 200_000) throw uniConditionalLimitError(filename, start, 'Conditional lexical inspection exceeds its 200000-node bound', source);
|
|
22
22
|
if (ts.isStringLiteralLike(node) || ts.isRegularExpressionLiteral(node)
|
|
23
23
|
|| [ts.SyntaxKind.TemplateHead, ts.SyntaxKind.TemplateMiddle, ts.SyntaxKind.TemplateTail].includes(node.kind)) {
|
|
24
24
|
ranges.push({ start: node.getStart(parsed), end: node.end });
|
|
@@ -27,7 +27,7 @@ function literalRanges(content, kind) {
|
|
|
27
27
|
return ranges.sort((a, b) => a.start - b.start);
|
|
28
28
|
}
|
|
29
29
|
function scriptComments(source, start, end, filename, output, kind) {
|
|
30
|
-
const content = source.slice(start, end), literals = literalRanges(content, kind); let literal = 0;
|
|
30
|
+
const content = source.slice(start, end), literals = literalRanges(content, kind, filename, start, source); let literal = 0;
|
|
31
31
|
for (let index = 0; index < content.length;) {
|
|
32
32
|
while (literals[literal]?.end <= index) literal++;
|
|
33
33
|
if (literals[literal]?.start <= index) { index = literals[literal].end; continue; }
|