@llui/compiler 0.3.0
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/LICENSE +21 -0
- package/dist/accessor-resolver.d.ts +58 -0
- package/dist/accessor-resolver.d.ts.map +1 -0
- package/dist/accessor-resolver.js +119 -0
- package/dist/accessor-resolver.js.map +1 -0
- package/dist/binding-descriptors.d.ts +105 -0
- package/dist/binding-descriptors.d.ts.map +1 -0
- package/dist/binding-descriptors.js +340 -0
- package/dist/binding-descriptors.js.map +1 -0
- package/dist/collect-deps.d.ts +49 -0
- package/dist/collect-deps.d.ts.map +1 -0
- package/dist/collect-deps.js +444 -0
- package/dist/collect-deps.js.map +1 -0
- package/dist/compiler-cache.d.ts +20 -0
- package/dist/compiler-cache.d.ts.map +1 -0
- package/dist/compiler-cache.js +20 -0
- package/dist/compiler-cache.js.map +1 -0
- package/dist/cross-file-resolver.d.ts +109 -0
- package/dist/cross-file-resolver.d.ts.map +1 -0
- package/dist/cross-file-resolver.js +530 -0
- package/dist/cross-file-resolver.js.map +1 -0
- package/dist/cross-file-walker.d.ts +63 -0
- package/dist/cross-file-walker.d.ts.map +1 -0
- package/dist/cross-file-walker.js +516 -0
- package/dist/cross-file-walker.js.map +1 -0
- package/dist/diagnostic.d.ts +76 -0
- package/dist/diagnostic.d.ts.map +1 -0
- package/dist/diagnostic.js +59 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/introspection-factory.d.ts +54 -0
- package/dist/introspection-factory.d.ts.map +1 -0
- package/dist/introspection-factory.js +46 -0
- package/dist/introspection-factory.js.map +1 -0
- package/dist/manifest.d.ts +144 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +209 -0
- package/dist/manifest.js.map +1 -0
- package/dist/module.d.ts +222 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +256 -0
- package/dist/module.js.map +1 -0
- package/dist/modules/_element-helpers.d.ts +4 -0
- package/dist/modules/_element-helpers.d.ts.map +1 -0
- package/dist/modules/_element-helpers.js +138 -0
- package/dist/modules/_element-helpers.js.map +1 -0
- package/dist/modules/_msg-variants.d.ts +10 -0
- package/dist/modules/_msg-variants.d.ts.map +1 -0
- package/dist/modules/_msg-variants.js +97 -0
- package/dist/modules/_msg-variants.js.map +1 -0
- package/dist/modules/_shared.d.ts +16 -0
- package/dist/modules/_shared.d.ts.map +1 -0
- package/dist/modules/_shared.js +30 -0
- package/dist/modules/_shared.js.map +1 -0
- package/dist/modules/accessibility.d.ts +3 -0
- package/dist/modules/accessibility.d.ts.map +1 -0
- package/dist/modules/accessibility.js +82 -0
- package/dist/modules/accessibility.js.map +1 -0
- package/dist/modules/accessor-side-effect.d.ts +3 -0
- package/dist/modules/accessor-side-effect.d.ts.map +1 -0
- package/dist/modules/accessor-side-effect.js +113 -0
- package/dist/modules/accessor-side-effect.js.map +1 -0
- package/dist/modules/agent-emits-drift.d.ts +3 -0
- package/dist/modules/agent-emits-drift.d.ts.map +1 -0
- package/dist/modules/agent-emits-drift.js +158 -0
- package/dist/modules/agent-emits-drift.js.map +1 -0
- package/dist/modules/agent-example-on-payload.d.ts +3 -0
- package/dist/modules/agent-example-on-payload.d.ts.map +1 -0
- package/dist/modules/agent-example-on-payload.js +53 -0
- package/dist/modules/agent-example-on-payload.js.map +1 -0
- package/dist/modules/agent-exclusive-annotations.d.ts +3 -0
- package/dist/modules/agent-exclusive-annotations.d.ts.map +1 -0
- package/dist/modules/agent-exclusive-annotations.js +68 -0
- package/dist/modules/agent-exclusive-annotations.js.map +1 -0
- package/dist/modules/agent-missing-intent.d.ts +3 -0
- package/dist/modules/agent-missing-intent.d.ts.map +1 -0
- package/dist/modules/agent-missing-intent.js +47 -0
- package/dist/modules/agent-missing-intent.js.map +1 -0
- package/dist/modules/agent-msg-resolvable.d.ts +3 -0
- package/dist/modules/agent-msg-resolvable.d.ts.map +1 -0
- package/dist/modules/agent-msg-resolvable.js +161 -0
- package/dist/modules/agent-msg-resolvable.js.map +1 -0
- package/dist/modules/agent-nonextractable-handler.d.ts +3 -0
- package/dist/modules/agent-nonextractable-handler.d.ts.map +1 -0
- package/dist/modules/agent-nonextractable-handler.js +127 -0
- package/dist/modules/agent-nonextractable-handler.js.map +1 -0
- package/dist/modules/agent-optional-field-undocumented.d.ts +3 -0
- package/dist/modules/agent-optional-field-undocumented.d.ts.map +1 -0
- package/dist/modules/agent-optional-field-undocumented.js +67 -0
- package/dist/modules/agent-optional-field-undocumented.js.map +1 -0
- package/dist/modules/agent-tagsend-translator-missing.d.ts +3 -0
- package/dist/modules/agent-tagsend-translator-missing.d.ts.map +1 -0
- package/dist/modules/agent-tagsend-translator-missing.js +58 -0
- package/dist/modules/agent-tagsend-translator-missing.js.map +1 -0
- package/dist/modules/agent-warning-on-confirm.d.ts +3 -0
- package/dist/modules/agent-warning-on-confirm.d.ts.map +1 -0
- package/dist/modules/agent-warning-on-confirm.js +46 -0
- package/dist/modules/agent-warning-on-confirm.js.map +1 -0
- package/dist/modules/async-update.d.ts +3 -0
- package/dist/modules/async-update.d.ts.map +1 -0
- package/dist/modules/async-update.js +86 -0
- package/dist/modules/async-update.js.map +1 -0
- package/dist/modules/binding-descriptors.d.ts +4 -0
- package/dist/modules/binding-descriptors.d.ts.map +1 -0
- package/dist/modules/binding-descriptors.js +48 -0
- package/dist/modules/binding-descriptors.js.map +1 -0
- package/dist/modules/bitmask-overflow.d.ts +3 -0
- package/dist/modules/bitmask-overflow.d.ts.map +1 -0
- package/dist/modules/bitmask-overflow.js +152 -0
- package/dist/modules/bitmask-overflow.js.map +1 -0
- package/dist/modules/compiler-stamp.d.ts +3 -0
- package/dist/modules/compiler-stamp.d.ts.map +1 -0
- package/dist/modules/compiler-stamp.js +44 -0
- package/dist/modules/compiler-stamp.js.map +1 -0
- package/dist/modules/component-meta.d.ts +3 -0
- package/dist/modules/component-meta.d.ts.map +1 -0
- package/dist/modules/component-meta.js +44 -0
- package/dist/modules/component-meta.js.map +1 -0
- package/dist/modules/controlled-input.d.ts +3 -0
- package/dist/modules/controlled-input.d.ts.map +1 -0
- package/dist/modules/controlled-input.js +68 -0
- package/dist/modules/controlled-input.js.map +1 -0
- package/dist/modules/core-synthesis.d.ts +18 -0
- package/dist/modules/core-synthesis.d.ts.map +1 -0
- package/dist/modules/core-synthesis.js +748 -0
- package/dist/modules/core-synthesis.js.map +1 -0
- package/dist/modules/direct-state-in-view.d.ts +3 -0
- package/dist/modules/direct-state-in-view.d.ts.map +1 -0
- package/dist/modules/direct-state-in-view.js +103 -0
- package/dist/modules/direct-state-in-view.js.map +1 -0
- package/dist/modules/each-closure-violation.d.ts +3 -0
- package/dist/modules/each-closure-violation.d.ts.map +1 -0
- package/dist/modules/each-closure-violation.js +255 -0
- package/dist/modules/each-closure-violation.js.map +1 -0
- package/dist/modules/each-memo.d.ts +15 -0
- package/dist/modules/each-memo.d.ts.map +1 -0
- package/dist/modules/each-memo.js +115 -0
- package/dist/modules/each-memo.js.map +1 -0
- package/dist/modules/effect-without-handler.d.ts +3 -0
- package/dist/modules/effect-without-handler.d.ts.map +1 -0
- package/dist/modules/effect-without-handler.js +92 -0
- package/dist/modules/effect-without-handler.js.map +1 -0
- package/dist/modules/element-rewrite.d.ts +22 -0
- package/dist/modules/element-rewrite.d.ts.map +1 -0
- package/dist/modules/element-rewrite.js +1017 -0
- package/dist/modules/element-rewrite.js.map +1 -0
- package/dist/modules/empty-props.d.ts +3 -0
- package/dist/modules/empty-props.d.ts.map +1 -0
- package/dist/modules/empty-props.js +50 -0
- package/dist/modules/empty-props.js.map +1 -0
- package/dist/modules/exhaustive-effect-handling.d.ts +3 -0
- package/dist/modules/exhaustive-effect-handling.d.ts.map +1 -0
- package/dist/modules/exhaustive-effect-handling.js +61 -0
- package/dist/modules/exhaustive-effect-handling.js.map +1 -0
- package/dist/modules/exhaustive-update.d.ts +3 -0
- package/dist/modules/exhaustive-update.d.ts.map +1 -0
- package/dist/modules/exhaustive-update.js +146 -0
- package/dist/modules/exhaustive-update.js.map +1 -0
- package/dist/modules/forgotten-spread.d.ts +3 -0
- package/dist/modules/forgotten-spread.d.ts.map +1 -0
- package/dist/modules/forgotten-spread.js +51 -0
- package/dist/modules/forgotten-spread.js.map +1 -0
- package/dist/modules/form-boilerplate.d.ts +3 -0
- package/dist/modules/form-boilerplate.d.ts.map +1 -0
- package/dist/modules/form-boilerplate.js +101 -0
- package/dist/modules/form-boilerplate.js.map +1 -0
- package/dist/modules/imperative-dom-in-view.d.ts +3 -0
- package/dist/modules/imperative-dom-in-view.d.ts.map +1 -0
- package/dist/modules/imperative-dom-in-view.js +123 -0
- package/dist/modules/imperative-dom-in-view.js.map +1 -0
- package/dist/modules/item-dedup.d.ts +7 -0
- package/dist/modules/item-dedup.d.ts.map +1 -0
- package/dist/modules/item-dedup.js +204 -0
- package/dist/modules/item-dedup.js.map +1 -0
- package/dist/modules/map-on-state-array.d.ts +3 -0
- package/dist/modules/map-on-state-array.d.ts.map +1 -0
- package/dist/modules/map-on-state-array.js +84 -0
- package/dist/modules/map-on-state-array.js.map +1 -0
- package/dist/modules/mask-legend.d.ts +10 -0
- package/dist/modules/mask-legend.d.ts.map +1 -0
- package/dist/modules/mask-legend.js +50 -0
- package/dist/modules/mask-legend.js.map +1 -0
- package/dist/modules/missing-memo.d.ts +3 -0
- package/dist/modules/missing-memo.d.ts.map +1 -0
- package/dist/modules/missing-memo.js +114 -0
- package/dist/modules/missing-memo.js.map +1 -0
- package/dist/modules/msg-annotations.d.ts +9 -0
- package/dist/modules/msg-annotations.d.ts.map +1 -0
- package/dist/modules/msg-annotations.js +54 -0
- package/dist/modules/msg-annotations.js.map +1 -0
- package/dist/modules/msg-schema.d.ts +10 -0
- package/dist/modules/msg-schema.d.ts.map +1 -0
- package/dist/modules/msg-schema.js +70 -0
- package/dist/modules/msg-schema.js.map +1 -0
- package/dist/modules/namespace-import.d.ts +3 -0
- package/dist/modules/namespace-import.d.ts.map +1 -0
- package/dist/modules/namespace-import.js +80 -0
- package/dist/modules/namespace-import.js.map +1 -0
- package/dist/modules/nested-send-in-update.d.ts +3 -0
- package/dist/modules/nested-send-in-update.d.ts.map +1 -0
- package/dist/modules/nested-send-in-update.js +77 -0
- package/dist/modules/nested-send-in-update.js.map +1 -0
- package/dist/modules/no-barrel-import-when-subpath-exists.d.ts +3 -0
- package/dist/modules/no-barrel-import-when-subpath-exists.d.ts.map +1 -0
- package/dist/modules/no-barrel-import-when-subpath-exists.js +100 -0
- package/dist/modules/no-barrel-import-when-subpath-exists.js.map +1 -0
- package/dist/modules/no-eager-item-accessor.d.ts +3 -0
- package/dist/modules/no-eager-item-accessor.d.ts.map +1 -0
- package/dist/modules/no-eager-item-accessor.js +74 -0
- package/dist/modules/no-eager-item-accessor.js.map +1 -0
- package/dist/modules/no-let-reactive-accessor.d.ts +3 -0
- package/dist/modules/no-let-reactive-accessor.d.ts.map +1 -0
- package/dist/modules/no-let-reactive-accessor.js +227 -0
- package/dist/modules/no-let-reactive-accessor.js.map +1 -0
- package/dist/modules/no-list-render-in-sample.d.ts +3 -0
- package/dist/modules/no-list-render-in-sample.d.ts.map +1 -0
- package/dist/modules/no-list-render-in-sample.js +89 -0
- package/dist/modules/no-list-render-in-sample.js.map +1 -0
- package/dist/modules/no-sample-in-accessor.d.ts +3 -0
- package/dist/modules/no-sample-in-accessor.d.ts.map +1 -0
- package/dist/modules/no-sample-in-accessor.js +141 -0
- package/dist/modules/no-sample-in-accessor.js.map +1 -0
- package/dist/modules/no-sample-in-reactive-position.d.ts +3 -0
- package/dist/modules/no-sample-in-reactive-position.d.ts.map +1 -0
- package/dist/modules/no-sample-in-reactive-position.js +72 -0
- package/dist/modules/no-sample-in-reactive-position.js.map +1 -0
- package/dist/modules/pure-update-function.d.ts +3 -0
- package/dist/modules/pure-update-function.d.ts.map +1 -0
- package/dist/modules/pure-update-function.js +127 -0
- package/dist/modules/pure-update-function.js.map +1 -0
- package/dist/modules/reactive-paths.d.ts +3 -0
- package/dist/modules/reactive-paths.d.ts.map +1 -0
- package/dist/modules/reactive-paths.js +77 -0
- package/dist/modules/reactive-paths.js.map +1 -0
- package/dist/modules/row-factory.d.ts +12 -0
- package/dist/modules/row-factory.d.ts.map +1 -0
- package/dist/modules/row-factory.js +385 -0
- package/dist/modules/row-factory.js.map +1 -0
- package/dist/modules/schema-hash.d.ts +15 -0
- package/dist/modules/schema-hash.d.ts.map +1 -0
- package/dist/modules/schema-hash.js +70 -0
- package/dist/modules/schema-hash.js.map +1 -0
- package/dist/modules/spread-in-children.d.ts +3 -0
- package/dist/modules/spread-in-children.d.ts.map +1 -0
- package/dist/modules/spread-in-children.js +144 -0
- package/dist/modules/spread-in-children.js.map +1 -0
- package/dist/modules/state-mutation.d.ts +3 -0
- package/dist/modules/state-mutation.d.ts.map +1 -0
- package/dist/modules/state-mutation.js +138 -0
- package/dist/modules/state-mutation.js.map +1 -0
- package/dist/modules/state-schema.d.ts +8 -0
- package/dist/modules/state-schema.d.ts.map +1 -0
- package/dist/modules/state-schema.js +55 -0
- package/dist/modules/state-schema.js.map +1 -0
- package/dist/modules/static-items.d.ts +3 -0
- package/dist/modules/static-items.d.ts.map +1 -0
- package/dist/modules/static-items.js +125 -0
- package/dist/modules/static-items.js.map +1 -0
- package/dist/modules/static-on.d.ts +3 -0
- package/dist/modules/static-on.d.ts.map +1 -0
- package/dist/modules/static-on.js +100 -0
- package/dist/modules/static-on.js.map +1 -0
- package/dist/modules/string-effect-callback.d.ts +3 -0
- package/dist/modules/string-effect-callback.d.ts.map +1 -0
- package/dist/modules/string-effect-callback.js +50 -0
- package/dist/modules/string-effect-callback.js.map +1 -0
- package/dist/modules/structural-mask.d.ts +8 -0
- package/dist/modules/structural-mask.d.ts.map +1 -0
- package/dist/modules/structural-mask.js +76 -0
- package/dist/modules/structural-mask.js.map +1 -0
- package/dist/modules/subapp-requires-reason.d.ts +3 -0
- package/dist/modules/subapp-requires-reason.d.ts.map +1 -0
- package/dist/modules/subapp-requires-reason.js +129 -0
- package/dist/modules/subapp-requires-reason.js.map +1 -0
- package/dist/modules/text-mask.d.ts +12 -0
- package/dist/modules/text-mask.d.ts.map +1 -0
- package/dist/modules/text-mask.js +63 -0
- package/dist/modules/text-mask.js.map +1 -0
- package/dist/modules/view-bag-import.d.ts +3 -0
- package/dist/modules/view-bag-import.d.ts.map +1 -0
- package/dist/modules/view-bag-import.js +80 -0
- package/dist/modules/view-bag-import.js.map +1 -0
- package/dist/msg-annotations.d.ts +104 -0
- package/dist/msg-annotations.d.ts.map +1 -0
- package/dist/msg-annotations.js +242 -0
- package/dist/msg-annotations.js.map +1 -0
- package/dist/msg-schema.d.ts +130 -0
- package/dist/msg-schema.d.ts.map +1 -0
- package/dist/msg-schema.js +770 -0
- package/dist/msg-schema.js.map +1 -0
- package/dist/schema-hash.d.ts +16 -0
- package/dist/schema-hash.d.ts.map +1 -0
- package/dist/schema-hash.js +31 -0
- package/dist/schema-hash.js.map +1 -0
- package/dist/state-schema.d.ts +41 -0
- package/dist/state-schema.d.ts.map +1 -0
- package/dist/state-schema.js +156 -0
- package/dist/state-schema.js.map +1 -0
- package/dist/transform.d.ts +109 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/transform.js +1390 -0
- package/dist/transform.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// `agent-warning-on-confirm` — errors when a Msg variant tagged
|
|
2
|
+
// `@requiresConfirm` lacks a `@warning("…")`. Without the warning,
|
|
3
|
+
// the LLM sees "this needs confirmation" with no reason — so it
|
|
4
|
+
// either dispatches blindly or refuses. The warning is what makes
|
|
5
|
+
// the confirm gate honest. Migrated from
|
|
6
|
+
// `@llui/eslint-plugin/src/rules/agent-warning-on-confirm.ts`.
|
|
7
|
+
import ts from 'typescript';
|
|
8
|
+
import { rangeFromOffsets } from '../diagnostic.js';
|
|
9
|
+
import { forEachMsgVariant } from './_msg-variants.js';
|
|
10
|
+
export function agentWarningOnConfirmModule() {
|
|
11
|
+
return {
|
|
12
|
+
name: 'agent-warning-on-confirm',
|
|
13
|
+
compilerVersion: '^0.3.0',
|
|
14
|
+
diagnostics: [
|
|
15
|
+
{
|
|
16
|
+
id: 'llui/agent-warning-on-confirm',
|
|
17
|
+
description: '@requiresConfirm without @warning — LLM should see why the action is gated.',
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
visitors: {
|
|
21
|
+
[ts.SyntaxKind.SourceFile]: (ctx, node) => {
|
|
22
|
+
const visited = node;
|
|
23
|
+
const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true);
|
|
24
|
+
forEachMsgVariant(sf, ({ variant, node: typeLit, leadingCommentText }) => {
|
|
25
|
+
if (!/@requiresConfirm\b/.test(leadingCommentText))
|
|
26
|
+
return;
|
|
27
|
+
if (/@warning\s*\(/.test(leadingCommentText))
|
|
28
|
+
return;
|
|
29
|
+
ctx.reportDiagnostic({
|
|
30
|
+
id: 'llui/agent-warning-on-confirm',
|
|
31
|
+
severity: 'error',
|
|
32
|
+
category: 'agent',
|
|
33
|
+
message: `Msg variant "${variant}" has \`@requiresConfirm\` but no \`@warning("…")\`. ` +
|
|
34
|
+
`Add a short consequence-shaped warning so the LLM sees why this action is gated: ` +
|
|
35
|
+
`\`/** @requiresConfirm @warning("Deletes all unsaved drafts.") */\`.`,
|
|
36
|
+
location: {
|
|
37
|
+
file: sf.fileName,
|
|
38
|
+
range: rangeFromOffsets(sf.text, typeLit.getStart(sf), typeLit.getEnd()),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=agent-warning-on-confirm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-warning-on-confirm.js","sourceRoot":"","sources":["../../src/modules/agent-warning-on-confirm.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,mEAAmE;AACnE,gEAAgE;AAChE,kEAAkE;AAClE,yCAAyC;AACzC,+DAA+D;AAE/D,OAAO,EAAE,MAAM,YAAY,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAEnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEtD,MAAM,UAAU,2BAA2B;IACzC,OAAO;QACL,IAAI,EAAE,0BAA0B;QAChC,eAAe,EAAE,QAAQ;QACzB,WAAW,EAAE;YACX;gBACE,EAAE,EAAE,+BAA+B;gBACnC,WAAW,EAAE,6EAA6E;aAC3F;SACF;QACD,QAAQ,EAAE;YACR,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;gBACxC,MAAM,OAAO,GAAG,IAAqB,CAAA;gBACrC,MAAM,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;gBAC5F,iBAAiB,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,kBAAkB,EAAE,EAAE,EAAE;oBACvE,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,kBAAkB,CAAC;wBAAE,OAAM;oBAC1D,IAAI,eAAe,CAAC,IAAI,CAAC,kBAAkB,CAAC;wBAAE,OAAM;oBACpD,GAAG,CAAC,gBAAgB,CAAC;wBACnB,EAAE,EAAE,+BAA+B;wBACnC,QAAQ,EAAE,OAAO;wBACjB,QAAQ,EAAE,OAAO;wBACjB,OAAO,EACL,gBAAgB,OAAO,uDAAuD;4BAC9E,mFAAmF;4BACnF,sEAAsE;wBACxE,QAAQ,EAAE;4BACR,IAAI,EAAE,EAAE,CAAC,QAAQ;4BACjB,KAAK,EAAE,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;yBACzE;qBACF,CAAC,CAAA;gBACJ,CAAC,CAAC,CAAA;YACJ,CAAC;SACF;KACF,CAAA;AACH,CAAC","sourcesContent":["// `agent-warning-on-confirm` — errors when a Msg variant tagged\n// `@requiresConfirm` lacks a `@warning(\"…\")`. Without the warning,\n// the LLM sees \"this needs confirmation\" with no reason — so it\n// either dispatches blindly or refuses. The warning is what makes\n// the confirm gate honest. Migrated from\n// `@llui/eslint-plugin/src/rules/agent-warning-on-confirm.ts`.\n\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\nimport { forEachMsgVariant } from './_msg-variants.js'\n\nexport function agentWarningOnConfirmModule(): CompilerModule {\n return {\n name: 'agent-warning-on-confirm',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/agent-warning-on-confirm',\n description: '@requiresConfirm without @warning — LLM should see why the action is gated.',\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n forEachMsgVariant(sf, ({ variant, node: typeLit, leadingCommentText }) => {\n if (!/@requiresConfirm\\b/.test(leadingCommentText)) return\n if (/@warning\\s*\\(/.test(leadingCommentText)) return\n ctx.reportDiagnostic({\n id: 'llui/agent-warning-on-confirm',\n severity: 'error',\n category: 'agent',\n message:\n `Msg variant \"${variant}\" has \\`@requiresConfirm\\` but no \\`@warning(\"…\")\\`. ` +\n `Add a short consequence-shaped warning so the LLM sees why this action is gated: ` +\n `\\`/** @requiresConfirm @warning(\"Deletes all unsaved drafts.\") */\\`.`,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, typeLit.getStart(sf), typeLit.getEnd()),\n },\n })\n })\n },\n },\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-update.d.ts","sourceRoot":"","sources":["../../src/modules/async-update.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAkBlD,wBAAgB,iBAAiB,IAAI,cAAc,CAkElD"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// `async-update` — errors on `async update` or `await` inside a
|
|
2
|
+
// component's update function. Update must be synchronous and pure;
|
|
3
|
+
// async work belongs in effects. Migrated from
|
|
4
|
+
// `@llui/eslint-plugin/src/rules/async-update.ts`.
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
import { rangeFromOffsets } from '../diagnostic.js';
|
|
7
|
+
import { findComponentCalls } from './_shared.js';
|
|
8
|
+
function findUpdateProperty(call) {
|
|
9
|
+
const arg = call.arguments[0];
|
|
10
|
+
if (!arg || !ts.isObjectLiteralExpression(arg))
|
|
11
|
+
return undefined;
|
|
12
|
+
for (const prop of arg.properties) {
|
|
13
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
14
|
+
ts.isIdentifier(prop.name) &&
|
|
15
|
+
prop.name.text === 'update') {
|
|
16
|
+
return prop;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
export function asyncUpdateModule() {
|
|
22
|
+
return {
|
|
23
|
+
name: 'async-update',
|
|
24
|
+
compilerVersion: '^0.3.0',
|
|
25
|
+
diagnostics: [
|
|
26
|
+
{
|
|
27
|
+
id: 'llui/async-update',
|
|
28
|
+
description: 'update() must be synchronous and pure. Move async operations to effects.',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
visitors: {
|
|
32
|
+
[ts.SyntaxKind.SourceFile]: (ctx, node) => {
|
|
33
|
+
const visited = node;
|
|
34
|
+
const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true);
|
|
35
|
+
for (const call of findComponentCalls(sf)) {
|
|
36
|
+
const updateProp = findUpdateProperty(call);
|
|
37
|
+
if (!updateProp)
|
|
38
|
+
continue;
|
|
39
|
+
const fn = updateProp.initializer;
|
|
40
|
+
if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn))
|
|
41
|
+
continue;
|
|
42
|
+
const asyncMod = fn.modifiers?.find((m) => m.kind === ts.SyntaxKind.AsyncKeyword);
|
|
43
|
+
if (asyncMod) {
|
|
44
|
+
ctx.reportDiagnostic({
|
|
45
|
+
id: 'llui/async-update',
|
|
46
|
+
severity: 'error',
|
|
47
|
+
category: 'reactivity',
|
|
48
|
+
message: 'update() is declared `async`. update() must be synchronous and pure — return effects instead, and run async work in the effect handler.',
|
|
49
|
+
location: {
|
|
50
|
+
file: sf.fileName,
|
|
51
|
+
range: rangeFromOffsets(sf.text, fn.getStart(sf), fn.getEnd()),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Walk the function body top-down. An `await` outside a nested
|
|
56
|
+
// function boundary is inside update() itself; an `await` inside
|
|
57
|
+
// a nested arrow/function is fine (those are deferred callbacks).
|
|
58
|
+
const walk = (n) => {
|
|
59
|
+
if (ts.isArrowFunction(n) ||
|
|
60
|
+
ts.isFunctionExpression(n) ||
|
|
61
|
+
ts.isFunctionDeclaration(n)) {
|
|
62
|
+
if (n !== fn)
|
|
63
|
+
return; // boundary — don't descend
|
|
64
|
+
}
|
|
65
|
+
if (ts.isAwaitExpression(n)) {
|
|
66
|
+
ctx.reportDiagnostic({
|
|
67
|
+
id: 'llui/async-update',
|
|
68
|
+
severity: 'error',
|
|
69
|
+
category: 'reactivity',
|
|
70
|
+
message: '`await` inside update(). update() must be synchronous — move the async operation to an effect (return `[newState, [myEffect]]`).',
|
|
71
|
+
location: {
|
|
72
|
+
file: sf.fileName,
|
|
73
|
+
range: rangeFromOffsets(sf.text, n.getStart(sf), n.getEnd()),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
ts.forEachChild(n, walk);
|
|
78
|
+
};
|
|
79
|
+
if (fn.body)
|
|
80
|
+
walk(fn.body);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=async-update.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-update.js","sourceRoot":"","sources":["../../src/modules/async-update.ts"],"names":[],"mappings":"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","sourcesContent":["// `async-update` — errors on `async update` or `await` inside a\n// component's update function. Update must be synchronous and pure;\n// async work belongs in effects. Migrated from\n// `@llui/eslint-plugin/src/rules/async-update.ts`.\n\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\nimport { findComponentCalls } from './_shared.js'\n\nfunction findUpdateProperty(call: ts.CallExpression): ts.PropertyAssignment | undefined {\n const arg = call.arguments[0]\n if (!arg || !ts.isObjectLiteralExpression(arg)) return undefined\n for (const prop of arg.properties) {\n if (\n ts.isPropertyAssignment(prop) &&\n ts.isIdentifier(prop.name) &&\n prop.name.text === 'update'\n ) {\n return prop\n }\n }\n return undefined\n}\n\nexport function asyncUpdateModule(): CompilerModule {\n return {\n name: 'async-update',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/async-update',\n description: 'update() must be synchronous and pure. Move async operations to effects.',\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n for (const call of findComponentCalls(sf)) {\n const updateProp = findUpdateProperty(call)\n if (!updateProp) continue\n const fn = updateProp.initializer\n if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) continue\n\n const asyncMod = fn.modifiers?.find((m) => m.kind === ts.SyntaxKind.AsyncKeyword)\n if (asyncMod) {\n ctx.reportDiagnostic({\n id: 'llui/async-update',\n severity: 'error',\n category: 'reactivity',\n message:\n 'update() is declared `async`. update() must be synchronous and pure — return effects instead, and run async work in the effect handler.',\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, fn.getStart(sf), fn.getEnd()),\n },\n })\n }\n\n // Walk the function body top-down. An `await` outside a nested\n // function boundary is inside update() itself; an `await` inside\n // a nested arrow/function is fine (those are deferred callbacks).\n const walk = (n: ts.Node): void => {\n if (\n ts.isArrowFunction(n) ||\n ts.isFunctionExpression(n) ||\n ts.isFunctionDeclaration(n)\n ) {\n if (n !== fn) return // boundary — don't descend\n }\n if (ts.isAwaitExpression(n)) {\n ctx.reportDiagnostic({\n id: 'llui/async-update',\n severity: 'error',\n category: 'reactivity',\n message:\n '`await` inside update(). update() must be synchronous — move the async operation to an effect (return `[newState, [myEffect]]`).',\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, n.getStart(sf), n.getEnd()),\n },\n })\n }\n ts.forEachChild(n, walk)\n }\n if (fn.body) walk(fn.body)\n }\n },\n },\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding-descriptors.d.ts","sourceRoot":"","sources":["../../src/modules/binding-descriptors.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AASlD,eAAO,MAAM,wBAAwB,8BAA8B,CAAA;AAEnE,eAAO,MAAM,wBAAwB,EAAE,cAuBtC,CAAA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// `binding-descriptors` — agent-runtime hook tagging. Pre-transform
|
|
2
|
+
// CompilerModule wrapping `tagDispatchHandlers` + `injectScopeVariantRegistrations`.
|
|
3
|
+
//
|
|
4
|
+
// Both functions mutate the AST in ways the visitor model can't
|
|
5
|
+
// cleanly express:
|
|
6
|
+
// - `tagDispatchHandlers`: walks every arrow/fn expression, wraps
|
|
7
|
+
// those whose body contains `send({type:'X'})` with
|
|
8
|
+
// `Object.assign(arrow, {__lluiVariants: ['X']})`. The visitor
|
|
9
|
+
// pattern records findings but doesn't rewrite; rewriting is what
|
|
10
|
+
// this pass does.
|
|
11
|
+
// - `injectScopeVariantRegistrations`: walks for `*.connect(get,
|
|
12
|
+
// send, ...)` call sites and inserts `__registerScopeVariants([...])`
|
|
13
|
+
// adjacent statements. The visitor pattern can't insert siblings
|
|
14
|
+
// into the source file at arbitrary positions.
|
|
15
|
+
//
|
|
16
|
+
// Both are pre-passes — they produce a rewritten SourceFile that the
|
|
17
|
+
// main visitor walks. v2c §2.1's "walker runs once per file" invariant
|
|
18
|
+
// is preserved: the VISITOR walk runs once. preTransform passes are
|
|
19
|
+
// additional but typically cheap and targeted.
|
|
20
|
+
//
|
|
21
|
+
// The `injected` flag from `injectScopeVariantRegistrations` is
|
|
22
|
+
// surfaced via a shared analysis slot — the umbrella's
|
|
23
|
+
// `cleanupImports` reads it to know whether to add the
|
|
24
|
+
// `__registerScopeVariants` runtime helper to the @llui/dom imports.
|
|
25
|
+
import { tagDispatchHandlers, injectScopeVariantRegistrations } from '../binding-descriptors.js';
|
|
26
|
+
export const BINDING_DESCRIPTORS_SLOT = 'binding-descriptors:state';
|
|
27
|
+
export const bindingDescriptorsModule = {
|
|
28
|
+
name: 'binding-descriptors',
|
|
29
|
+
compilerVersion: '^0.3.0',
|
|
30
|
+
diagnostics: [],
|
|
31
|
+
visitors: {},
|
|
32
|
+
preTransform(ctx, sf) {
|
|
33
|
+
// injectScopeVariantRegistrations runs FIRST so its
|
|
34
|
+
// `collectLocalFns` resolver still sees raw arrow initializers in
|
|
35
|
+
// const declarations (the universal tagger below replaces those
|
|
36
|
+
// initializers with `Object.assign(...)` wrappers).
|
|
37
|
+
const injection = injectScopeVariantRegistrations(sf, ctx.factory);
|
|
38
|
+
const tagged = tagDispatchHandlers(injection.sf, ctx.factory);
|
|
39
|
+
// Surface the injected flag for the umbrella's cleanupImports
|
|
40
|
+
// pass. The umbrella reads this from `analysis.perModule` after
|
|
41
|
+
// `registry.run()` returns.
|
|
42
|
+
ctx.analysis.perModule.set(BINDING_DESCRIPTORS_SLOT, {
|
|
43
|
+
scopeRegistrationsInjected: injection.injected,
|
|
44
|
+
});
|
|
45
|
+
return tagged;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=binding-descriptors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding-descriptors.js","sourceRoot":"","sources":["../../src/modules/binding-descriptors.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,qFAAqF;AACrF,EAAE;AACF,gEAAgE;AAChE,mBAAmB;AACnB,oEAAoE;AACpE,wDAAwD;AACxD,mEAAmE;AACnE,sEAAsE;AACtE,sBAAsB;AACtB,mEAAmE;AACnE,0EAA0E;AAC1E,qEAAqE;AACrE,mDAAmD;AACnD,EAAE;AACF,qEAAqE;AACrE,uEAAuE;AACvE,oEAAoE;AACpE,+CAA+C;AAC/C,EAAE;AACF,gEAAgE;AAChE,uDAAuD;AACvD,uDAAuD;AACvD,qEAAqE;AAErE,OAAO,EAAE,mBAAmB,EAAE,+BAA+B,EAAE,MAAM,2BAA2B,CAAA;AAUhG,MAAM,CAAC,MAAM,wBAAwB,GAAG,2BAA2B,CAAA;AAEnE,MAAM,CAAC,MAAM,wBAAwB,GAAmB;IACtD,IAAI,EAAE,qBAAqB;IAC3B,eAAe,EAAE,QAAQ;IACzB,WAAW,EAAE,EAAE;IACf,QAAQ,EAAE,EAAE;IAEZ,YAAY,CAAC,GAAG,EAAE,EAAE;QAClB,oDAAoD;QACpD,kEAAkE;QAClE,gEAAgE;QAChE,oDAAoD;QACpD,MAAM,SAAS,GAAG,+BAA+B,CAAC,EAAE,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;QAClE,MAAM,MAAM,GAAG,mBAAmB,CAAC,SAAS,CAAC,EAAE,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;QAE7D,8DAA8D;QAC9D,gEAAgE;QAChE,4BAA4B;QAC5B,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,wBAAwB,EAAE;YACnD,0BAA0B,EAAE,SAAS,CAAC,QAAQ;SACrB,CAAC,CAAA;QAE5B,OAAO,MAAM,CAAA;IACf,CAAC;CACF,CAAA","sourcesContent":["// `binding-descriptors` — agent-runtime hook tagging. Pre-transform\n// CompilerModule wrapping `tagDispatchHandlers` + `injectScopeVariantRegistrations`.\n//\n// Both functions mutate the AST in ways the visitor model can't\n// cleanly express:\n// - `tagDispatchHandlers`: walks every arrow/fn expression, wraps\n// those whose body contains `send({type:'X'})` with\n// `Object.assign(arrow, {__lluiVariants: ['X']})`. The visitor\n// pattern records findings but doesn't rewrite; rewriting is what\n// this pass does.\n// - `injectScopeVariantRegistrations`: walks for `*.connect(get,\n// send, ...)` call sites and inserts `__registerScopeVariants([...])`\n// adjacent statements. The visitor pattern can't insert siblings\n// into the source file at arbitrary positions.\n//\n// Both are pre-passes — they produce a rewritten SourceFile that the\n// main visitor walks. v2c §2.1's \"walker runs once per file\" invariant\n// is preserved: the VISITOR walk runs once. preTransform passes are\n// additional but typically cheap and targeted.\n//\n// The `injected` flag from `injectScopeVariantRegistrations` is\n// surfaced via a shared analysis slot — the umbrella's\n// `cleanupImports` reads it to know whether to add the\n// `__registerScopeVariants` runtime helper to the @llui/dom imports.\n\nimport { tagDispatchHandlers, injectScopeVariantRegistrations } from '../binding-descriptors.js'\nimport type { CompilerModule } from '../module.js'\n\ninterface BindingDescriptorsSlot {\n /** Whether `injectScopeVariantRegistrations` actually inserted any\n * call sites — drives import-cleanup's decision about the\n * `__registerScopeVariants` runtime helper. */\n scopeRegistrationsInjected: boolean\n}\n\nexport const BINDING_DESCRIPTORS_SLOT = 'binding-descriptors:state'\n\nexport const bindingDescriptorsModule: CompilerModule = {\n name: 'binding-descriptors',\n compilerVersion: '^0.3.0',\n diagnostics: [],\n visitors: {},\n\n preTransform(ctx, sf) {\n // injectScopeVariantRegistrations runs FIRST so its\n // `collectLocalFns` resolver still sees raw arrow initializers in\n // const declarations (the universal tagger below replaces those\n // initializers with `Object.assign(...)` wrappers).\n const injection = injectScopeVariantRegistrations(sf, ctx.factory)\n const tagged = tagDispatchHandlers(injection.sf, ctx.factory)\n\n // Surface the injected flag for the umbrella's cleanupImports\n // pass. The umbrella reads this from `analysis.perModule` after\n // `registry.run()` returns.\n ctx.analysis.perModule.set(BINDING_DESCRIPTORS_SLOT, {\n scopeRegistrationsInjected: injection.injected,\n } as BindingDescriptorsSlot)\n\n return tagged\n },\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmask-overflow.d.ts","sourceRoot":"","sources":["../../src/modules/bitmask-overflow.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAgDlD,wBAAgB,qBAAqB,IAAI,cAAc,CAqFtD"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// `bitmask-overflow` — errors when a component reads more than 62 unique
|
|
2
|
+
// state paths. The runtime bitmask is two 31-bit words (`mask` 0..30,
|
|
3
|
+
// `maskHi` 31..61); paths past the 62-path limit collapse to FULL_MASK
|
|
4
|
+
// (-1) and re-evaluate every binding on change, negating the optimization.
|
|
5
|
+
//
|
|
6
|
+
// Migrated from `@llui/eslint-plugin`'s bitmask-overflow rule
|
|
7
|
+
// (v0.x). Promoted to a compiler error (was an ESLint warning) so the
|
|
8
|
+
// LLM-first authoring path cannot silently ship overflowing components.
|
|
9
|
+
// LLMs ignore warnings; the compiler-error channel is non-bypassable.
|
|
10
|
+
//
|
|
11
|
+
// The diagnostic message includes:
|
|
12
|
+
// - the per-top-level-field breakdown so authors know where to slice,
|
|
13
|
+
// - a co-occurrence note for fields whose every sub-path always fires
|
|
14
|
+
// in the same accessor sets (those collapse to one bit if the parent
|
|
15
|
+
// object is read as a unit),
|
|
16
|
+
// - a recommendation to restructure state so the most-read paths
|
|
17
|
+
// collapse to fewer top-level prefixes, or to factor the offending
|
|
18
|
+
// subtree into a subApp.
|
|
19
|
+
//
|
|
20
|
+
// Anchors the diagnostic on the file's first `component(...)` call so
|
|
21
|
+
// the location is meaningful. Files without a `component()` call are
|
|
22
|
+
// silent — utility modules that happen to read state-shaped accessors
|
|
23
|
+
// don't trigger the rule.
|
|
24
|
+
import ts from 'typescript';
|
|
25
|
+
import { collectAccessorPathSets, collectStatePathsFromSource } from '../collect-deps.js';
|
|
26
|
+
import { rangeFromOffsets } from '../diagnostic.js';
|
|
27
|
+
import { findComponentCalls } from './_shared.js';
|
|
28
|
+
const PATH_LIMIT = 62;
|
|
29
|
+
function setsEqual(a, b) {
|
|
30
|
+
if (a.size !== b.size)
|
|
31
|
+
return false;
|
|
32
|
+
for (const x of a)
|
|
33
|
+
if (!b.has(x))
|
|
34
|
+
return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
function findCooccurringFields(paths, accessorSets) {
|
|
38
|
+
const subPathsByTop = new Map();
|
|
39
|
+
for (const p of paths) {
|
|
40
|
+
const dot = p.indexOf('.');
|
|
41
|
+
if (dot < 0)
|
|
42
|
+
continue;
|
|
43
|
+
const top = p.slice(0, dot);
|
|
44
|
+
const arr = subPathsByTop.get(top) ?? [];
|
|
45
|
+
arr.push(p);
|
|
46
|
+
subPathsByTop.set(top, arr);
|
|
47
|
+
}
|
|
48
|
+
const appearances = new Map();
|
|
49
|
+
for (let i = 0; i < accessorSets.length; i++) {
|
|
50
|
+
for (const path of accessorSets[i]) {
|
|
51
|
+
if (!appearances.has(path))
|
|
52
|
+
appearances.set(path, new Set());
|
|
53
|
+
appearances.get(path).add(i);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const [field, subPaths] of subPathsByTop) {
|
|
58
|
+
if (subPaths.length < 2)
|
|
59
|
+
continue;
|
|
60
|
+
const first = appearances.get(subPaths[0]) ?? new Set();
|
|
61
|
+
let uniform = true;
|
|
62
|
+
for (let i = 1; i < subPaths.length; i++) {
|
|
63
|
+
const set = appearances.get(subPaths[i]) ?? new Set();
|
|
64
|
+
if (!setsEqual(first, set)) {
|
|
65
|
+
uniform = false;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (uniform)
|
|
70
|
+
out.push({ field, saved: subPaths.length - 1 });
|
|
71
|
+
}
|
|
72
|
+
return out.sort((a, b) => b.saved - a.saved);
|
|
73
|
+
}
|
|
74
|
+
export function bitmaskOverflowModule() {
|
|
75
|
+
return {
|
|
76
|
+
name: 'bitmask-overflow',
|
|
77
|
+
compilerVersion: '^0.3.0',
|
|
78
|
+
diagnostics: [
|
|
79
|
+
{
|
|
80
|
+
id: 'llui/bitmask-overflow',
|
|
81
|
+
description: 'Component reads more than 62 unique state paths — paths past the limit fall back to FULL_MASK.',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
visitors: {
|
|
85
|
+
[ts.SyntaxKind.SourceFile]: (ctx, node) => {
|
|
86
|
+
const visited = node;
|
|
87
|
+
// Re-parse from text. PreTransform passes (e.g.
|
|
88
|
+
// `bindingDescriptorsModule` in agent mode) produce synthetic
|
|
89
|
+
// AST nodes that lack parent pointers; `collectStatePathsFromSource`
|
|
90
|
+
// walks parent chains and crashes on undefined. Re-parsing
|
|
91
|
+
// guarantees a clean tree with `setParentNodes=true` — the
|
|
92
|
+
// analysis is over the user's source, which is what we want
|
|
93
|
+
// regardless of upstream rewrites.
|
|
94
|
+
const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true);
|
|
95
|
+
const componentCalls = findComponentCalls(sf);
|
|
96
|
+
if (componentCalls.length === 0)
|
|
97
|
+
return;
|
|
98
|
+
const componentCall = componentCalls[0];
|
|
99
|
+
const paths = collectStatePathsFromSource(sf);
|
|
100
|
+
const pathCount = paths.size;
|
|
101
|
+
if (pathCount <= PATH_LIMIT)
|
|
102
|
+
return;
|
|
103
|
+
const overflow = pathCount - PATH_LIMIT;
|
|
104
|
+
const byTopLevel = new Map();
|
|
105
|
+
for (const p of paths) {
|
|
106
|
+
const top = p.split('.', 1)[0];
|
|
107
|
+
byTopLevel.set(top, (byTopLevel.get(top) ?? 0) + 1);
|
|
108
|
+
}
|
|
109
|
+
const sorted = [...byTopLevel.entries()].sort((a, b) => b[1] - a[1]);
|
|
110
|
+
const candidates = [];
|
|
111
|
+
let saved = 0;
|
|
112
|
+
for (const [field, n] of sorted) {
|
|
113
|
+
if (pathCount - saved <= 31)
|
|
114
|
+
break;
|
|
115
|
+
candidates.push(field);
|
|
116
|
+
saved += n;
|
|
117
|
+
}
|
|
118
|
+
const breakdown = sorted.map(([field, n]) => `${field} (${n})`).join(', ');
|
|
119
|
+
const candidateList = candidates.map((f) => `\`${f}\``).join(', ');
|
|
120
|
+
const accessorSets = collectAccessorPathSets(sf);
|
|
121
|
+
const cooccurring = findCooccurringFields(paths, accessorSets);
|
|
122
|
+
const cooccurrenceNote = cooccurring.length > 0
|
|
123
|
+
? `\n\nCo-occurrence detected: ` +
|
|
124
|
+
cooccurring
|
|
125
|
+
.map(({ field, saved: s }) => `every sub-path under \`${field}\` always fires together; reading \`s.${field}\` as one unit saves ${s} bit${s === 1 ? '' : 's'}`)
|
|
126
|
+
.join('; ') +
|
|
127
|
+
`. Bundle those reads into a single \`s.${cooccurring[0].field}\` access (e.g. \`const ${cooccurring[0].field} = s.${cooccurring[0].field}\`) before extraction — cheaper refactor, same budget relief.`
|
|
128
|
+
: '';
|
|
129
|
+
const message = `Component has ${pathCount} unique state access paths (${overflow} past the ${PATH_LIMIT}-path limit). ` +
|
|
130
|
+
`Paths ${PATH_LIMIT + 1}..${pathCount} fall back to FULL_MASK — their changes re-evaluate every binding ` +
|
|
131
|
+
`in the component, negating the bitmask optimization for those updates.\n\n` +
|
|
132
|
+
`Top-level fields by path count: ${breakdown}.${cooccurrenceNote}\n\n` +
|
|
133
|
+
`Recommended fix: restructure state so ${candidateList} are grouped under one or two ` +
|
|
134
|
+
`reference-stable parents, or factor that subtree into a separate module that consumes ` +
|
|
135
|
+
`the parent's state via the standard view-function \`(props, send)\` convention. ` +
|
|
136
|
+
`Alternative: use \`combine()\` to split the reducer into slices when the parent's \`update()\` ` +
|
|
137
|
+
`is mostly mechanical routing.`;
|
|
138
|
+
ctx.reportDiagnostic({
|
|
139
|
+
id: 'llui/bitmask-overflow',
|
|
140
|
+
severity: 'error',
|
|
141
|
+
category: 'perf',
|
|
142
|
+
message,
|
|
143
|
+
location: {
|
|
144
|
+
file: sf.fileName,
|
|
145
|
+
range: rangeFromOffsets(sf.text, componentCall.getStart(sf), componentCall.getEnd()),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=bitmask-overflow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmask-overflow.js","sourceRoot":"","sources":["../../src/modules/bitmask-overflow.ts"],"names":[],"mappings":"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","sourcesContent":["// `bitmask-overflow` — errors when a component reads more than 62 unique\n// state paths. The runtime bitmask is two 31-bit words (`mask` 0..30,\n// `maskHi` 31..61); paths past the 62-path limit collapse to FULL_MASK\n// (-1) and re-evaluate every binding on change, negating the optimization.\n//\n// Migrated from `@llui/eslint-plugin`'s bitmask-overflow rule\n// (v0.x). Promoted to a compiler error (was an ESLint warning) so the\n// LLM-first authoring path cannot silently ship overflowing components.\n// LLMs ignore warnings; the compiler-error channel is non-bypassable.\n//\n// The diagnostic message includes:\n// - the per-top-level-field breakdown so authors know where to slice,\n// - a co-occurrence note for fields whose every sub-path always fires\n// in the same accessor sets (those collapse to one bit if the parent\n// object is read as a unit),\n// - a recommendation to restructure state so the most-read paths\n// collapse to fewer top-level prefixes, or to factor the offending\n// subtree into a subApp.\n//\n// Anchors the diagnostic on the file's first `component(...)` call so\n// the location is meaningful. Files without a `component()` call are\n// silent — utility modules that happen to read state-shaped accessors\n// don't trigger the rule.\n\nimport ts from 'typescript'\nimport { collectAccessorPathSets, collectStatePathsFromSource } from '../collect-deps.js'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\nimport { findComponentCalls } from './_shared.js'\n\nconst PATH_LIMIT = 62\n\nfunction setsEqual<T>(a: Set<T>, b: Set<T>): boolean {\n if (a.size !== b.size) return false\n for (const x of a) if (!b.has(x)) return false\n return true\n}\n\nfunction findCooccurringFields(\n paths: Set<string>,\n accessorSets: Set<string>[],\n): Array<{ field: string; saved: number }> {\n const subPathsByTop = new Map<string, string[]>()\n for (const p of paths) {\n const dot = p.indexOf('.')\n if (dot < 0) continue\n const top = p.slice(0, dot)\n const arr = subPathsByTop.get(top) ?? []\n arr.push(p)\n subPathsByTop.set(top, arr)\n }\n const appearances = new Map<string, Set<number>>()\n for (let i = 0; i < accessorSets.length; i++) {\n for (const path of accessorSets[i]!) {\n if (!appearances.has(path)) appearances.set(path, new Set())\n appearances.get(path)!.add(i)\n }\n }\n const out: Array<{ field: string; saved: number }> = []\n for (const [field, subPaths] of subPathsByTop) {\n if (subPaths.length < 2) continue\n const first = appearances.get(subPaths[0]!) ?? new Set<number>()\n let uniform = true\n for (let i = 1; i < subPaths.length; i++) {\n const set = appearances.get(subPaths[i]!) ?? new Set<number>()\n if (!setsEqual(first, set)) {\n uniform = false\n break\n }\n }\n if (uniform) out.push({ field, saved: subPaths.length - 1 })\n }\n return out.sort((a, b) => b.saved - a.saved)\n}\n\nexport function bitmaskOverflowModule(): CompilerModule {\n return {\n name: 'bitmask-overflow',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/bitmask-overflow',\n description:\n 'Component reads more than 62 unique state paths — paths past the limit fall back to FULL_MASK.',\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n // Re-parse from text. PreTransform passes (e.g.\n // `bindingDescriptorsModule` in agent mode) produce synthetic\n // AST nodes that lack parent pointers; `collectStatePathsFromSource`\n // walks parent chains and crashes on undefined. Re-parsing\n // guarantees a clean tree with `setParentNodes=true` — the\n // analysis is over the user's source, which is what we want\n // regardless of upstream rewrites.\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n const componentCalls = findComponentCalls(sf)\n if (componentCalls.length === 0) return\n const componentCall = componentCalls[0]!\n\n const paths = collectStatePathsFromSource(sf)\n const pathCount = paths.size\n if (pathCount <= PATH_LIMIT) return\n\n const overflow = pathCount - PATH_LIMIT\n const byTopLevel = new Map<string, number>()\n for (const p of paths) {\n const top = p.split('.', 1)[0]!\n byTopLevel.set(top, (byTopLevel.get(top) ?? 0) + 1)\n }\n const sorted = [...byTopLevel.entries()].sort((a, b) => b[1] - a[1])\n const candidates: string[] = []\n let saved = 0\n for (const [field, n] of sorted) {\n if (pathCount - saved <= 31) break\n candidates.push(field)\n saved += n\n }\n const breakdown = sorted.map(([field, n]) => `${field} (${n})`).join(', ')\n const candidateList = candidates.map((f) => `\\`${f}\\``).join(', ')\n\n const accessorSets = collectAccessorPathSets(sf)\n const cooccurring = findCooccurringFields(paths, accessorSets)\n const cooccurrenceNote =\n cooccurring.length > 0\n ? `\\n\\nCo-occurrence detected: ` +\n cooccurring\n .map(\n ({ field, saved: s }) =>\n `every sub-path under \\`${field}\\` always fires together; reading \\`s.${field}\\` as one unit saves ${s} bit${s === 1 ? '' : 's'}`,\n )\n .join('; ') +\n `. Bundle those reads into a single \\`s.${cooccurring[0]!.field}\\` access (e.g. \\`const ${cooccurring[0]!.field} = s.${cooccurring[0]!.field}\\`) before extraction — cheaper refactor, same budget relief.`\n : ''\n\n const message =\n `Component has ${pathCount} unique state access paths (${overflow} past the ${PATH_LIMIT}-path limit). ` +\n `Paths ${PATH_LIMIT + 1}..${pathCount} fall back to FULL_MASK — their changes re-evaluate every binding ` +\n `in the component, negating the bitmask optimization for those updates.\\n\\n` +\n `Top-level fields by path count: ${breakdown}.${cooccurrenceNote}\\n\\n` +\n `Recommended fix: restructure state so ${candidateList} are grouped under one or two ` +\n `reference-stable parents, or factor that subtree into a separate module that consumes ` +\n `the parent's state via the standard view-function \\`(props, send)\\` convention. ` +\n `Alternative: use \\`combine()\\` to split the reducer into slices when the parent's \\`update()\\` ` +\n `is mostly mechanical routing.`\n\n ctx.reportDiagnostic({\n id: 'llui/bitmask-overflow',\n severity: 'error',\n category: 'perf',\n message,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, componentCall.getStart(sf), componentCall.getEnd()),\n },\n })\n },\n },\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compiler-stamp.d.ts","sourceRoot":"","sources":["../../src/modules/compiler-stamp.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAwB,MAAM,cAAc,CAAA;AAGxE,eAAO,MAAM,mBAAmB,EAAE,cA0BjC,CAAA"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// `compiler-stamp` — emits the umbrella-level integrity marker on
|
|
2
|
+
// every compiled `component()` call:
|
|
3
|
+
//
|
|
4
|
+
// - `__lluiCompilerEmitted: 1` — the marker scanned by the Vite
|
|
5
|
+
// adapter's closeBundle integrity check. A bundled component
|
|
6
|
+
// missing this property indicates the LLui transform was skipped
|
|
7
|
+
// for that file, which is a hard build error.
|
|
8
|
+
// - `__compilerVersion: '<semver>'` — the runtime contract version
|
|
9
|
+
// (v2a §2.4 / v2b §5). The runtime gates feature-detection on
|
|
10
|
+
// this; a missing version falls back to `genericUpdate`
|
|
11
|
+
// (FULL_MASK) with a console warning.
|
|
12
|
+
//
|
|
13
|
+
// Per-target emission so each `component()` call in a multi-component
|
|
14
|
+
// file gets its own pair. Always-on regardless of agent/dev mode —
|
|
15
|
+
// no activation gate. This is the umbrella's mandatory module.
|
|
16
|
+
import { COMPILER_VERSION } from '../version.js';
|
|
17
|
+
import { findComponentCalls } from './_shared.js';
|
|
18
|
+
export const compilerStampModule = {
|
|
19
|
+
name: 'compiler-stamp',
|
|
20
|
+
compilerVersion: '^0.3.0',
|
|
21
|
+
diagnostics: [],
|
|
22
|
+
// Targets captured in `emit` — see `_shared.ts`.
|
|
23
|
+
visitors: {},
|
|
24
|
+
emit(ctx, analysis) {
|
|
25
|
+
const f = ctx.factory;
|
|
26
|
+
const contributions = [];
|
|
27
|
+
for (const n of findComponentCalls(analysis.sourceFile)) {
|
|
28
|
+
contributions.push({
|
|
29
|
+
module: 'compiler-stamp',
|
|
30
|
+
field: '__lluiCompilerEmitted',
|
|
31
|
+
value: f.createNumericLiteral(1),
|
|
32
|
+
target: n,
|
|
33
|
+
});
|
|
34
|
+
contributions.push({
|
|
35
|
+
module: 'compiler-stamp',
|
|
36
|
+
field: '__compilerVersion',
|
|
37
|
+
value: f.createStringLiteral(COMPILER_VERSION),
|
|
38
|
+
target: n,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return contributions;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=compiler-stamp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compiler-stamp.js","sourceRoot":"","sources":["../../src/modules/compiler-stamp.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,qCAAqC;AACrC,EAAE;AACF,kEAAkE;AAClE,iEAAiE;AACjE,qEAAqE;AACrE,kDAAkD;AAClD,qEAAqE;AACrE,kEAAkE;AAClE,4DAA4D;AAC5D,0CAA0C;AAC1C,EAAE;AACF,sEAAsE;AACtE,mEAAmE;AACnE,+DAA+D;AAE/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAEhD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEjD,MAAM,CAAC,MAAM,mBAAmB,GAAmB;IACjD,IAAI,EAAE,gBAAgB;IACtB,eAAe,EAAE,QAAQ;IACzB,WAAW,EAAE,EAAE;IACf,iDAAiD;IACjD,QAAQ,EAAE,EAAE;IAEZ,IAAI,CAAC,GAAG,EAAE,QAAQ;QAChB,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAA;QACrB,MAAM,aAAa,GAA2B,EAAE,CAAA;QAChD,KAAK,MAAM,CAAC,IAAI,kBAAkB,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YACxD,aAAa,CAAC,IAAI,CAAC;gBACjB,MAAM,EAAE,gBAAgB;gBACxB,KAAK,EAAE,uBAAuB;gBAC9B,KAAK,EAAE,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC;gBAChC,MAAM,EAAE,CAAC;aACV,CAAC,CAAA;YACF,aAAa,CAAC,IAAI,CAAC;gBACjB,MAAM,EAAE,gBAAgB;gBACxB,KAAK,EAAE,mBAAmB;gBAC1B,KAAK,EAAE,CAAC,CAAC,mBAAmB,CAAC,gBAAgB,CAAC;gBAC9C,MAAM,EAAE,CAAC;aACV,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,aAAa,CAAA;IACtB,CAAC;CACF,CAAA","sourcesContent":["// `compiler-stamp` — emits the umbrella-level integrity marker on\n// every compiled `component()` call:\n//\n// - `__lluiCompilerEmitted: 1` — the marker scanned by the Vite\n// adapter's closeBundle integrity check. A bundled component\n// missing this property indicates the LLui transform was skipped\n// for that file, which is a hard build error.\n// - `__compilerVersion: '<semver>'` — the runtime contract version\n// (v2a §2.4 / v2b §5). The runtime gates feature-detection on\n// this; a missing version falls back to `genericUpdate`\n// (FULL_MASK) with a console warning.\n//\n// Per-target emission so each `component()` call in a multi-component\n// file gets its own pair. Always-on regardless of agent/dev mode —\n// no activation gate. This is the umbrella's mandatory module.\n\nimport { COMPILER_VERSION } from '../version.js'\nimport type { CompilerModule, EmissionContribution } from '../module.js'\nimport { findComponentCalls } from './_shared.js'\n\nexport const compilerStampModule: CompilerModule = {\n name: 'compiler-stamp',\n compilerVersion: '^0.3.0',\n diagnostics: [],\n // Targets captured in `emit` — see `_shared.ts`.\n visitors: {},\n\n emit(ctx, analysis) {\n const f = ctx.factory\n const contributions: EmissionContribution[] = []\n for (const n of findComponentCalls(analysis.sourceFile)) {\n contributions.push({\n module: 'compiler-stamp',\n field: '__lluiCompilerEmitted',\n value: f.createNumericLiteral(1),\n target: n,\n })\n contributions.push({\n module: 'compiler-stamp',\n field: '__compilerVersion',\n value: f.createStringLiteral(COMPILER_VERSION),\n target: n,\n })\n }\n return contributions\n },\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-meta.d.ts","sourceRoot":"","sources":["../../src/modules/component-meta.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAwB,MAAM,cAAc,CAAA;AAGxE,eAAO,MAAM,mBAAmB,EAAE,cAoCjC,CAAA"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// `component-meta` — emits `__componentMeta: { file, line }` per
|
|
2
|
+
// `component()` call. Today's `injectComponentMeta` in transform.ts
|
|
3
|
+
// is gated on devMode; this module keeps the gating semantics by
|
|
4
|
+
// accepting an option (the umbrella passes `devMode` through).
|
|
5
|
+
//
|
|
6
|
+
// This is the first per-component-targeted emission in the registry.
|
|
7
|
+
// It validates the `EmissionContribution.target` shape (v2c §7.9
|
|
8
|
+
// finding from the schema-hash POC): when multiple `component()`
|
|
9
|
+
// calls live in one file, each gets its own `__componentMeta` with
|
|
10
|
+
// its own line number, and the conflict-detector lets each emission
|
|
11
|
+
// through because the `(field, target)` tuples are distinct.
|
|
12
|
+
import { findComponentCalls } from './_shared.js';
|
|
13
|
+
export const componentMetaModule = {
|
|
14
|
+
name: 'component-meta',
|
|
15
|
+
compilerVersion: '^0.3.0',
|
|
16
|
+
diagnostics: [],
|
|
17
|
+
// Targets captured in `emit` (not `visit`) — see `_shared.ts`
|
|
18
|
+
// `findComponentCalls` for the rationale (Phase 2b rebuilds
|
|
19
|
+
// ancestor nodes, invalidating visit-captured refs).
|
|
20
|
+
visitors: {},
|
|
21
|
+
emit(ctx, analysis) {
|
|
22
|
+
const sf = analysis.sourceFile;
|
|
23
|
+
const calls = findComponentCalls(sf);
|
|
24
|
+
if (calls.length === 0)
|
|
25
|
+
return [];
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const n of calls) {
|
|
28
|
+
const pos = n.pos >= 0 ? n.getStart(sf) : 0;
|
|
29
|
+
const { line } = sf.getLineAndCharacterOfPosition(pos);
|
|
30
|
+
const meta = ctx.factory.createObjectLiteralExpression([
|
|
31
|
+
ctx.factory.createPropertyAssignment('file', ctx.factory.createStringLiteral(sf.fileName)),
|
|
32
|
+
ctx.factory.createPropertyAssignment('line', ctx.factory.createNumericLiteral(line + 1)),
|
|
33
|
+
], false);
|
|
34
|
+
out.push({
|
|
35
|
+
module: 'component-meta',
|
|
36
|
+
field: '__componentMeta',
|
|
37
|
+
value: meta,
|
|
38
|
+
target: n,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=component-meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-meta.js","sourceRoot":"","sources":["../../src/modules/component-meta.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,oEAAoE;AACpE,iEAAiE;AACjE,+DAA+D;AAC/D,EAAE;AACF,qEAAqE;AACrE,iEAAiE;AACjE,iEAAiE;AACjE,mEAAmE;AACnE,oEAAoE;AACpE,6DAA6D;AAG7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEjD,MAAM,CAAC,MAAM,mBAAmB,GAAmB;IACjD,IAAI,EAAE,gBAAgB;IACtB,eAAe,EAAE,QAAQ;IACzB,WAAW,EAAE,EAAE;IACf,8DAA8D;IAC9D,4DAA4D;IAC5D,qDAAqD;IACrD,QAAQ,EAAE,EAAE;IAEZ,IAAI,CAAC,GAAG,EAAE,QAAQ;QAChB,MAAM,EAAE,GAAG,QAAQ,CAAC,UAAU,CAAA;QAC9B,MAAM,KAAK,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAA;QACpC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAA;QACjC,MAAM,GAAG,GAA2B,EAAE,CAAA;QACtC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YAC3C,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,6BAA6B,CAAC,GAAG,CAAC,CAAA;YACtD,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,6BAA6B,CACpD;gBACE,GAAG,CAAC,OAAO,CAAC,wBAAwB,CAClC,MAAM,EACN,GAAG,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,CAAC,QAAQ,CAAC,CAC7C;gBACD,GAAG,CAAC,OAAO,CAAC,wBAAwB,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;aACzF,EACD,KAAK,CACN,CAAA;YACD,GAAG,CAAC,IAAI,CAAC;gBACP,MAAM,EAAE,gBAAgB;gBACxB,KAAK,EAAE,iBAAiB;gBACxB,KAAK,EAAE,IAAI;gBACX,MAAM,EAAE,CAAC;aACV,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC;CACF,CAAA","sourcesContent":["// `component-meta` — emits `__componentMeta: { file, line }` per\n// `component()` call. Today's `injectComponentMeta` in transform.ts\n// is gated on devMode; this module keeps the gating semantics by\n// accepting an option (the umbrella passes `devMode` through).\n//\n// This is the first per-component-targeted emission in the registry.\n// It validates the `EmissionContribution.target` shape (v2c §7.9\n// finding from the schema-hash POC): when multiple `component()`\n// calls live in one file, each gets its own `__componentMeta` with\n// its own line number, and the conflict-detector lets each emission\n// through because the `(field, target)` tuples are distinct.\n\nimport type { CompilerModule, EmissionContribution } from '../module.js'\nimport { findComponentCalls } from './_shared.js'\n\nexport const componentMetaModule: CompilerModule = {\n name: 'component-meta',\n compilerVersion: '^0.3.0',\n diagnostics: [],\n // Targets captured in `emit` (not `visit`) — see `_shared.ts`\n // `findComponentCalls` for the rationale (Phase 2b rebuilds\n // ancestor nodes, invalidating visit-captured refs).\n visitors: {},\n\n emit(ctx, analysis): EmissionContribution[] {\n const sf = analysis.sourceFile\n const calls = findComponentCalls(sf)\n if (calls.length === 0) return []\n const out: EmissionContribution[] = []\n for (const n of calls) {\n const pos = n.pos >= 0 ? n.getStart(sf) : 0\n const { line } = sf.getLineAndCharacterOfPosition(pos)\n const meta = ctx.factory.createObjectLiteralExpression(\n [\n ctx.factory.createPropertyAssignment(\n 'file',\n ctx.factory.createStringLiteral(sf.fileName),\n ),\n ctx.factory.createPropertyAssignment('line', ctx.factory.createNumericLiteral(line + 1)),\n ],\n false,\n )\n out.push({\n module: 'component-meta',\n field: '__componentMeta',\n value: meta,\n target: n,\n })\n }\n return out\n },\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controlled-input.d.ts","sourceRoot":"","sources":["../../src/modules/controlled-input.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAalD,wBAAgB,qBAAqB,IAAI,cAAc,CAoDtD"}
|