@llui/compiler 0.3.0 → 0.3.1

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/dist/transform.js CHANGED
@@ -28,47 +28,7 @@ import { itemDedupModule } from './modules/item-dedup.js';
28
28
  import { elementRewriteModule, ELEMENT_REWRITE_SLOT, } from './modules/element-rewrite.js';
29
29
  import { rowFactoryModule } from './modules/row-factory.js';
30
30
  import { coreSynthesisModule, CORE_SYNTHESIS_SLOT, } from './modules/core-synthesis.js';
31
- import { bitmaskOverflowModule } from './modules/bitmask-overflow.js';
32
- import { asyncUpdateModule } from './modules/async-update.js';
33
- import { mapOnStateArrayModule } from './modules/map-on-state-array.js';
34
- import { nestedSendInUpdateModule } from './modules/nested-send-in-update.js';
35
- import { directStateInViewModule } from './modules/direct-state-in-view.js';
36
- import { imperativeDomInViewModule } from './modules/imperative-dom-in-view.js';
37
- import { accessorSideEffectModule } from './modules/accessor-side-effect.js';
38
- import { stateMutationModule } from './modules/state-mutation.js';
39
- import { effectWithoutHandlerModule } from './modules/effect-without-handler.js';
40
- import { exhaustiveEffectHandlingModule } from './modules/exhaustive-effect-handling.js';
41
- import { noEagerItemAccessorModule } from './modules/no-eager-item-accessor.js';
42
- import { pureUpdateFunctionModule } from './modules/pure-update-function.js';
43
- import { exhaustiveUpdateModule } from './modules/exhaustive-update.js';
44
- import { noLetReactiveAccessorModule } from './modules/no-let-reactive-accessor.js';
45
- import { eachClosureViolationModule } from './modules/each-closure-violation.js';
46
- import { stringEffectCallbackModule } from './modules/string-effect-callback.js';
47
- import { agentMissingIntentModule } from './modules/agent-missing-intent.js';
48
- import { agentWarningOnConfirmModule } from './modules/agent-warning-on-confirm.js';
49
- import { agentExampleOnPayloadModule } from './modules/agent-example-on-payload.js';
50
- import { agentExclusiveAnnotationsModule } from './modules/agent-exclusive-annotations.js';
51
- import { agentOptionalFieldUndocumentedModule } from './modules/agent-optional-field-undocumented.js';
52
- import { agentTagsendTranslatorMissingModule } from './modules/agent-tagsend-translator-missing.js';
53
- import { agentNonextractableHandlerModule } from './modules/agent-nonextractable-handler.js';
54
- import { subappRequiresReasonModule } from './modules/subapp-requires-reason.js';
55
- import { emptyPropsModule } from './modules/empty-props.js';
56
- import { forgottenSpreadModule } from './modules/forgotten-spread.js';
57
- import { accessibilityModule } from './modules/accessibility.js';
58
- import { viewBagImportModule } from './modules/view-bag-import.js';
59
- import { controlledInputModule } from './modules/controlled-input.js';
60
- import { missingMemoModule } from './modules/missing-memo.js';
61
- import { namespaceImportModule } from './modules/namespace-import.js';
62
- import { noBarrelImportWhenSubpathExistsModule } from './modules/no-barrel-import-when-subpath-exists.js';
63
- import { formBoilerplateModule } from './modules/form-boilerplate.js';
64
- import { spreadInChildrenModule } from './modules/spread-in-children.js';
65
- import { staticItemsModule } from './modules/static-items.js';
66
- import { staticOnModule } from './modules/static-on.js';
67
- import { noListRenderInSampleModule } from './modules/no-list-render-in-sample.js';
68
- import { noSampleInAccessorModule } from './modules/no-sample-in-accessor.js';
69
- import { noSampleInReactivePositionModule } from './modules/no-sample-in-reactive-position.js';
70
- import { agentEmitsDriftModule } from './modules/agent-emits-drift.js';
71
- import { agentMsgResolvableModule } from './modules/agent-msg-resolvable.js';
31
+ import { createLintModules } from './lint-modules.js';
72
32
  export function createMaskLiteral(f, mask) {
73
33
  if (mask >= 0)
74
34
  return f.createNumericLiteral(mask);
@@ -227,6 +187,16 @@ export function transformLlui(source, _filename, devMode = false, emitAgentMetad
227
187
  let usesElSplit = false;
228
188
  let usesMemo = false;
229
189
  let usesApplyBinding = false;
190
+ // v0.4 size-cut: element-rewrite emits `__bindUncertain` for prop values
191
+ // it can't statically classify (e.g. function-parameter identifiers).
192
+ // The flag drives the cleanupImports pass to add the runtime import.
193
+ let usesBindUncertain = false;
194
+ // v0.4 size-cut (Tier 1.2): per-file set of primitive imports needed by
195
+ // the `__view` factories we synthesize alongside each `component()` call.
196
+ // The runtime calls `def.__view(send)` instead of `createView(send)`, so
197
+ // each component only pulls in the primitives it actually destructures —
198
+ // killing the view-bag tree-shaking leak that pulled all primitives.
199
+ const viewBagPrimitivesNeeded = new Set();
230
200
  let usesCloneStaticTemplate = false;
231
201
  const f = ts.factory;
232
202
  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
@@ -320,55 +290,13 @@ export function transformLlui(source, _filename, devMode = false, emitAgentMetad
320
290
  // the umbrella's last remaining inline injector
321
291
  // (`injectCompilerEmittedMarker`, deleted below).
322
292
  activeModules.push(compilerStampModule);
323
- // bitmaskOverflowModule is always-on. Emits `llui/bitmask-overflow`
324
- // (severity: error) when a component reads more than 62 unique state
325
- // paths. Migrated from @llui/eslint-plugin (v0.x); promoted from
326
- // ESLint warning to compiler error because LLMs ignore warnings.
327
- activeModules.push(bitmaskOverflowModule());
328
- // Correctness rules migrated from @llui/eslint-plugin as compile-time
329
- // errors. LLM-first authoring requires non-bypassable correctness
330
- // signals; ESLint warnings are silently ignored by LLM agents and
331
- // may not even be installed in a downstream project.
332
- activeModules.push(asyncUpdateModule());
333
- activeModules.push(mapOnStateArrayModule());
334
- activeModules.push(nestedSendInUpdateModule());
335
- activeModules.push(directStateInViewModule());
336
- activeModules.push(imperativeDomInViewModule());
337
- activeModules.push(accessorSideEffectModule());
338
- activeModules.push(stateMutationModule());
339
- activeModules.push(effectWithoutHandlerModule());
340
- activeModules.push(exhaustiveEffectHandlingModule());
341
- activeModules.push(noEagerItemAccessorModule());
342
- activeModules.push(pureUpdateFunctionModule());
343
- activeModules.push(exhaustiveUpdateModule());
344
- activeModules.push(noLetReactiveAccessorModule());
345
- activeModules.push(eachClosureViolationModule());
346
- activeModules.push(stringEffectCallbackModule());
347
- activeModules.push(agentMissingIntentModule());
348
- activeModules.push(agentWarningOnConfirmModule());
349
- activeModules.push(agentExampleOnPayloadModule());
350
- activeModules.push(agentExclusiveAnnotationsModule());
351
- activeModules.push(agentOptionalFieldUndocumentedModule());
352
- activeModules.push(agentTagsendTranslatorMissingModule());
353
- activeModules.push(agentNonextractableHandlerModule());
354
- activeModules.push(subappRequiresReasonModule());
355
- activeModules.push(emptyPropsModule());
356
- activeModules.push(forgottenSpreadModule());
357
- activeModules.push(accessibilityModule());
358
- activeModules.push(viewBagImportModule());
359
- activeModules.push(controlledInputModule());
360
- activeModules.push(missingMemoModule());
361
- activeModules.push(namespaceImportModule());
362
- activeModules.push(noBarrelImportWhenSubpathExistsModule());
363
- activeModules.push(formBoilerplateModule());
364
- activeModules.push(spreadInChildrenModule());
365
- activeModules.push(staticItemsModule());
366
- activeModules.push(staticOnModule());
367
- activeModules.push(noListRenderInSampleModule());
368
- activeModules.push(noSampleInAccessorModule());
369
- activeModules.push(noSampleInReactivePositionModule());
370
- activeModules.push(agentEmitsDriftModule());
371
- activeModules.push(agentMsgResolvableModule());
293
+ // Always-on lint rules. Single source of truth lives in
294
+ // `./lint-modules.ts` so adding/removing a rule propagates to both
295
+ // the transform pipeline and the rule-docs generator
296
+ // (`scripts/generate-rule-docs.ts`). LLM-first authoring requires
297
+ // non-bypassable correctness signals: every rule emits at
298
+ // `severity: error`.
299
+ activeModules.push(...createLintModules());
372
300
  // eachMemoModule wraps allocating each() items accessors in
373
301
  // `memo(...)` via `transformCallEnter`. Activated when the file
374
302
  // has any reactive paths (mirrors the inline call's gating).
@@ -455,7 +383,10 @@ export function transformLlui(source, _filename, devMode = false, emitAgentMetad
455
383
  lluiImport,
456
384
  }));
457
385
  const registry = new ModuleRegistry(activeModules);
458
- const registryResult = registry.run(sourceFile);
386
+ // `typeSources` flows through to lint modules that need cross-file
387
+ // visibility (e.g. agent-emits-drift's imported-Msg case). Same
388
+ // shape as `ModuleExternalTypes`.
389
+ const registryResult = registry.run(sourceFile, undefined, typeSources);
459
390
  // The registry phases (preTransform v2c/decomp-7, transformCall
460
391
  // v2c/decomp-11/12) may have mutated the source file — replace our
461
392
  // local reference so all subsequent code (fieldBits, visitor,
@@ -496,6 +427,8 @@ export function transformLlui(source, _filename, devMode = false, emitAgentMetad
496
427
  usesElTemplate = true;
497
428
  if (erState.usesCloneStaticTemplate)
498
429
  usesCloneStaticTemplate = true;
430
+ if (erState.usesBindUncertain)
431
+ usesBindUncertain = true;
499
432
  if (erState.compiled.size > 0 ||
500
433
  erState.usesElSplit ||
501
434
  erState.usesElTemplate ||
@@ -716,6 +649,13 @@ export function transformLlui(source, _filename, devMode = false, emitAgentMetad
716
649
  // the prior `if (devMode)` guard. When `devMode` is false the
717
650
  // registry is empty and this call is a no-op.
718
651
  result = applyRegistryEmissions(result ?? node, node);
652
+ // v0.4 size-cut (Tier 1.2): synthesize __view = (send) => ({ send, ... })
653
+ // containing ONLY the primitives this component's view callback
654
+ // destructures. The runtime prefers __view over createView for compiled
655
+ // components, eliminating the all-primitives reference chain through
656
+ // view-helpers.ts. Each primitive becomes its own top-level import,
657
+ // tree-shaken by Rollup when no component destructures it.
658
+ result = injectViewBag(result ?? node, viewBagPrimitivesNeeded, f);
719
659
  // __schemaHash: migrated to schemaHashModule (v2c/decomp-5).
720
660
  // When shouldEmitAgentMetadata is true, schemaHashModule is in
721
661
  // the active module list and produces the emission via the
@@ -745,7 +685,7 @@ export function transformLlui(source, _filename, devMode = false, emitAgentMetad
745
685
  // Pass 3: Clean up imports — use the old cleanupImports approach
746
686
  // which operates on the transformed SourceFile safely
747
687
  const safeToRemove = new Set([...compiledHelpers].filter((h) => !bailedHelpers.has(h)));
748
- transformed = cleanupImports(transformed, lluiImport, importedHelpers, safeToRemove, usesElSplit, usesElTemplate, usesMemo, usesApplyBinding, usesCloneStaticTemplate, scopeRegistrationsInjected, f);
688
+ transformed = cleanupImports(transformed, lluiImport, importedHelpers, safeToRemove, usesElSplit, usesElTemplate, usesMemo, usesApplyBinding, usesCloneStaticTemplate, scopeRegistrationsInjected, viewBagPrimitivesNeeded, usesBindUncertain, f);
749
689
  if (edits.length === 0) {
750
690
  // No element-helper rewrites — but registry may still have
751
691
  // collected diagnostics (e.g. agent-rule errors on Msg variants
@@ -1007,6 +947,118 @@ const VIEW_HELPER_PRIMITIVES = new Set([
1007
947
  'slice',
1008
948
  'send',
1009
949
  ]);
950
+ // v0.4 size-cut (Tier 1.2): bag-field → runtime-primitive map. `ctx` is
951
+ // the only rename — every other destructured name maps 1:1 to its
952
+ // primitive's exported identifier from `@llui/dom`. Fields not in this
953
+ // map (e.g. `send`) are handled separately or omitted.
954
+ const VIEW_BAG_FIELD_TO_PRIMITIVE = {
955
+ show: 'show',
956
+ branch: 'branch',
957
+ scope: 'scope',
958
+ each: 'each',
959
+ text: 'text',
960
+ unsafeHtml: 'unsafeHtml',
961
+ memo: 'memo',
962
+ selector: 'selector',
963
+ sample: 'sample',
964
+ clientOnly: 'clientOnly',
965
+ ctx: 'useContext',
966
+ };
967
+ /**
968
+ * Splice a `__view: (send) => ({ send, name1, name2, ... })` property into
969
+ * a `component({...})` call's config-arg literal. The synthesized factory
970
+ * lets the runtime build a minimal view bag that references only the
971
+ * primitives this component's view destructures — replacing the static
972
+ * `createView` call in mount.ts and eliminating its all-primitives import
973
+ * chain. Bag-field names other than `send` are added to `needed` so
974
+ * `cleanupImports` injects matching `@llui/dom` imports at the file level.
975
+ *
976
+ * Idempotent — returns `call` unchanged when:
977
+ * • the config arg is not an object literal
978
+ * • no `view:` property exists, or its value is not an arrow/function
979
+ * • the view's first parameter is not an ObjectBindingPattern
980
+ * • the config arg already has a `__view` property (re-run safety)
981
+ *
982
+ * Bag fields that aren't in `VIEW_BAG_FIELD_TO_PRIMITIVE` (e.g. unknown
983
+ * names from a user-typed View extension) are skipped so the runtime falls
984
+ * back to `createView` for any access to them.
985
+ */
986
+ function injectViewBag(call, needed, f) {
987
+ const configArg = call.arguments[0];
988
+ if (!configArg || !ts.isObjectLiteralExpression(configArg))
989
+ return call;
990
+ // Skip if a `__view` is already present (idempotency for re-runs).
991
+ for (const prop of configArg.properties) {
992
+ if (ts.isPropertyAssignment(prop) &&
993
+ ts.isIdentifier(prop.name) &&
994
+ prop.name.text === '__view') {
995
+ return call;
996
+ }
997
+ }
998
+ // Find view: arrow/function.
999
+ let viewFn = null;
1000
+ for (const prop of configArg.properties) {
1001
+ if (ts.isPropertyAssignment(prop) &&
1002
+ ts.isIdentifier(prop.name) &&
1003
+ prop.name.text === 'view' &&
1004
+ (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))) {
1005
+ viewFn = prop.initializer;
1006
+ break;
1007
+ }
1008
+ }
1009
+ if (!viewFn)
1010
+ return call;
1011
+ // Inspect the first parameter — must be an ObjectBindingPattern.
1012
+ const firstParam = viewFn.parameters[0];
1013
+ if (!firstParam || !ts.isObjectBindingPattern(firstParam.name))
1014
+ return call;
1015
+ const entries = [];
1016
+ for (const elem of firstParam.name.elements) {
1017
+ const localName = ts.isIdentifier(elem.name) ? elem.name.text : null;
1018
+ const sourceName = elem.propertyName && ts.isIdentifier(elem.propertyName) ? elem.propertyName.text : localName;
1019
+ if (!localName || !sourceName)
1020
+ continue;
1021
+ if (sourceName === 'send') {
1022
+ entries.push({ localName, primitive: null });
1023
+ continue;
1024
+ }
1025
+ const primitive = VIEW_BAG_FIELD_TO_PRIMITIVE[sourceName];
1026
+ if (!primitive)
1027
+ continue; // unknown name — let the runtime fail at runtime if accessed
1028
+ entries.push({ localName, primitive });
1029
+ }
1030
+ if (entries.length === 0)
1031
+ return call;
1032
+ // Synthesize: __view: ($send) => ({ localA: $send, localB: text, localC: each, ... })
1033
+ // We use a fixed parameter name `$send` to avoid shadowing — the bag
1034
+ // entries that map to send use this identifier.
1035
+ const sendParamName = f.createIdentifier('$send');
1036
+ const bagProps = [];
1037
+ for (const e of entries) {
1038
+ if (e.primitive === null) {
1039
+ // local name → send parameter
1040
+ bagProps.push(e.localName === '$send'
1041
+ ? f.createShorthandPropertyAssignment(sendParamName)
1042
+ : f.createPropertyAssignment(f.createIdentifier(e.localName), sendParamName));
1043
+ }
1044
+ else if (e.localName === e.primitive) {
1045
+ bagProps.push(f.createShorthandPropertyAssignment(f.createIdentifier(e.localName)));
1046
+ needed.add(e.primitive);
1047
+ }
1048
+ else {
1049
+ bagProps.push(f.createPropertyAssignment(f.createIdentifier(e.localName), f.createIdentifier(e.primitive)));
1050
+ needed.add(e.primitive);
1051
+ }
1052
+ }
1053
+ const viewBagFactory = f.createArrowFunction(undefined, undefined, [
1054
+ f.createParameterDeclaration(undefined, undefined, sendParamName, undefined, undefined, undefined),
1055
+ ], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createParenthesizedExpression(f.createObjectLiteralExpression(bagProps, false)));
1056
+ const newConfig = f.createObjectLiteralExpression([...configArg.properties, f.createPropertyAssignment('__view', viewBagFactory)], true);
1057
+ return f.createCallExpression(call.expression, call.typeArguments, [
1058
+ newConfig,
1059
+ ...call.arguments.slice(1),
1060
+ ]);
1061
+ }
1010
1062
  function collectViewHelperAliases(sf, lluiImport, helperNames) {
1011
1063
  const aliases = new Map();
1012
1064
  function addFromBindingPattern(pattern) {
@@ -1115,14 +1167,16 @@ export function isHelperCall(expr, name, helperNames, aliases) {
1115
1167
  // `structuralMaskModule` (v2c/decomp-14). Module fires top-down
1116
1168
  // (transformCallEnter) so the visitor sees the masked options literal.
1117
1169
  // ── Pass 3: Import cleanup ───────────────────────────────────────
1118
- function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElTemplate, usesMemo, usesApplyBinding, usesCloneStaticTemplate, usesRegisterScopeVariants, f) {
1170
+ function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElTemplate, usesMemo, usesApplyBinding, usesCloneStaticTemplate, usesRegisterScopeVariants, viewBagPrimitivesNeeded, usesBindUncertain, f) {
1119
1171
  if (compiled.size === 0 &&
1120
1172
  !usesElTemplate &&
1121
1173
  !usesElSplit &&
1122
1174
  !usesMemo &&
1123
1175
  !usesApplyBinding &&
1124
1176
  !usesCloneStaticTemplate &&
1125
- !usesRegisterScopeVariants)
1177
+ !usesRegisterScopeVariants &&
1178
+ viewBagPrimitivesNeeded.size === 0 &&
1179
+ !usesBindUncertain)
1126
1180
  return sf;
1127
1181
  const clause = lluiImport.importClause;
1128
1182
  if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings))
@@ -1132,6 +1186,10 @@ function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElT
1132
1186
  if (!hasElSplit && usesElSplit) {
1133
1187
  remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('elSplit')));
1134
1188
  }
1189
+ const hasBindUncertain = clause.namedBindings.elements.some((s) => s.name.text === '__bindUncertain');
1190
+ if (!hasBindUncertain && usesBindUncertain) {
1191
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('__bindUncertain')));
1192
+ }
1135
1193
  const hasElTemplate = clause.namedBindings.elements.some((s) => s.name.text === 'elTemplate');
1136
1194
  if (!hasElTemplate && usesElTemplate) {
1137
1195
  remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('elTemplate')));
@@ -1159,8 +1217,19 @@ function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElT
1159
1217
  if (!hasRegisterScopeVariants && usesRegisterScopeVariants) {
1160
1218
  remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('__registerScopeVariants')));
1161
1219
  }
1220
+ // v0.4 size-cut (Tier 1.2): add @llui/dom imports for each primitive
1221
+ // referenced by synthesized __view factories. `ctx` is the destructured
1222
+ // bag name; its primitive is `useContext` (the only rename pair).
1223
+ for (const prim of viewBagPrimitivesNeeded) {
1224
+ if (!clause.namedBindings.elements.some((s) => s.name.text === prim)) {
1225
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier(prim)));
1226
+ }
1227
+ }
1162
1228
  const newBindings = f.createNamedImports(remaining);
1163
- const newClause = f.createImportClause(false, undefined, newBindings);
1229
+ // New TS 6 signature: first arg is `phaseModifier` (undefined =
1230
+ // regular import; `ts.SyntaxKind.TypeKeyword` = `import type`).
1231
+ // The legacy boolean overload is deprecated.
1232
+ const newClause = f.createImportClause(undefined, undefined, newBindings);
1164
1233
  const newImportDecl = f.createImportDeclaration(undefined, newClause, lluiImport.moduleSpecifier);
1165
1234
  let replaced = false;
1166
1235
  const statements = sf.statements.map((stmt) => {
@@ -1168,7 +1237,10 @@ function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElT
1168
1237
  ts.isImportDeclaration(stmt) &&
1169
1238
  ts.isStringLiteral(stmt.moduleSpecifier) &&
1170
1239
  stmt.moduleSpecifier.text === '@llui/dom' &&
1171
- !stmt.importClause?.isTypeOnly) {
1240
+ // `phaseModifier === ts.SyntaxKind.TypeKeyword` is `import type
1241
+ // …`; we only want to rewrite value imports. Replaces deprecated
1242
+ // `isTypeOnly` from the TS<6 API.
1243
+ stmt.importClause?.phaseModifier !== ts.SyntaxKind.TypeKeyword) {
1172
1244
  replaced = true;
1173
1245
  return newImportDecl;
1174
1246
  }