refactor(loader): tighten the disabled gate and rehome the note

Address review: the gate module docstring now states the actual evaluation
contexts (config after injections against the plugin context, disabled at
every mount decision against the loader context); metadataExpressionErrors
rejects expressions nested below disabled and syntax-checks the disabled
expression itself so an unparseable gate fails at the gate instead of the
boot. The tutorial's !!js claims follow, and the loader note moves to
implemented/architecture with its inbound links retargeted.
This commit is contained in:
Huanqi Cao
2026-08-11 18:52:52 +08:00
parent 25329fcb79
commit e56b1ccab2
17 changed files with 75 additions and 25 deletions
+16
View File
@@ -20,4 +20,20 @@ describe('verify-cordis-config metadata expressions', () => {
const problems = metadataExpressionErrors({ id: { __jsExpr: 'process.platform' }, name: 'pkg' }, '[0]')
expect(problems).toContain('[0].id: !!js is not interpolated here')
})
it('rejects an expression nested below disabled (only the field itself interpolates)', () => {
const problems = metadataExpressionErrors(
{ id: 'tool-bash', name: 'pkg', disabled: { when: { __jsExpr: 'process.platform' } } },
'[0]',
)
expect(problems).toContain('[0].disabled.when: !!js is not interpolated here')
})
it('rejects a disabled expression that does not parse (the loader would fail the boot)', () => {
const problems = metadataExpressionErrors(
{ id: 'tool-bash', name: 'pkg', disabled: { __jsExpr: 'process.platform ===' } },
'[0]',
)
expect(problems.some(problem => problem.includes('[0].disabled: disabled expression does not parse'))).toBe(true)
})
})
+42 -8
View File
@@ -1,13 +1,13 @@
/**
* Validate Cordis Loader entry metadata and package resolution.
*
* The Loader interpolates a plugin entry's `config` and the entry `disabled`
* field (both evaluate against the loader context; `disabled` at tree build).
* Every other entry metadata field stays static, so an expression there
* remains truthy data and silently changes composition. Example configs and
* the dsh Web composition resolve named plugins from their owning workspace
* manifests. Local example packages must also be in the root TypeScript
* project graph.
* The Loader interpolates a plugin entry's `config` (after declared injections
* activate, against that plugin context) and the entry `disabled` field (at
* every mount decision, against the loader context). Every other entry
* metadata field stays static, so an expression there remains truthy data and
* silently changes composition. Example configs and the dsh Web composition
* resolve named plugins from their owning workspace manifests. Local example
* packages must also be in the root TypeScript project graph.
*/
import { globSync, readFileSync } from 'node:fs'
@@ -387,7 +387,9 @@ function validateMetadata(entry: Record<string, unknown>, file: string, path: st
/**
* Expression-node diagnostics for one entry. `disabled` is the single
* interpolated metadata field; every other metadata field must stay static.
* interpolated metadata field: its own `!!js` expression node is allowed and
* must parse, while expressions nested below it stay truthy data; every other
* metadata field must stay fully static.
* @param entry - one loader entry (or patch row).
* @param path - the entry's diagnostic path prefix.
* @returns one diagnostic per offending expression.
@@ -400,9 +402,41 @@ export function metadataExpressionErrors(entry: Record<string, unknown>, path: s
collectExpressionPaths(entry[field], `${path}.${field}`, expressionPaths)
for (const expressionPath of expressionPaths) problems.push(`${expressionPath}: !!js is not interpolated here`)
}
const disabled = entry.disabled
if (disabled !== undefined) {
if (isJsExpr(disabled)) {
const detail = disabledExpressionProblem(disabled.__jsExpr)
if (detail !== undefined) problems.push(`${path}.disabled${detail}`)
} else {
// A non-expression value gates on Boolean() at mount; an expression
// nested anywhere below it never evaluates, so it must stay literal.
const expressionPaths: string[] = []
collectExpressionPaths(disabled, `${path}.disabled`, expressionPaths)
for (const expressionPath of expressionPaths) problems.push(`${expressionPath}: !!js is not interpolated here`)
}
}
return problems
}
/**
* Parse-only validation of a `disabled` expression: the Loader evaluates it
* at every mount decision, and a syntax error would fail the boot — rejecting
* it here moves that failure to the earliest resolvable point.
* @param expression - the `!!js` expression text.
* @returns the diagnostic suffix, or `undefined` when the expression parses.
*/
function disabledExpressionProblem(expression: string): string | undefined {
try {
// Compilation only — the constructor never executes the body.
// eslint-disable-next-line no-new-func
new Function(`return (${expression})`)
return undefined
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
return `: disabled expression does not parse: ${detail}`
}
}
function collectExpressionPaths(value: unknown, path: string, output: string[]): void {
if (isJsExpr(value)) {
output.push(path)