@llui/compiler 0.3.0 → 0.3.2

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,141 @@ 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 config arg already has a `__view` property (re-run safety)
980
+ *
981
+ * Bag fields that aren't in `VIEW_BAG_FIELD_TO_PRIMITIVE` (e.g. unknown
982
+ * names from a user-typed View extension) are skipped — the runtime
983
+ * cannot fabricate them, so accessing them at runtime is the user's
984
+ * problem (matches dev-mode behavior).
985
+ *
986
+ * Identifier-style view params (`view: (h) => ...` or `view: (send) => ...`)
987
+ * can't be statically narrowed to a known subset of primitives — `h` may
988
+ * be passed to helpers, destructured later, or read dynamically. For
989
+ * those we emit `__view: ($send) => createView($send)` so the runtime
990
+ * gets the full bag. The instance-level `_viewBag` cache on
991
+ * `getInstanceViewBag` means this is still one allocation per mount.
992
+ */
993
+ function injectViewBag(call, needed, f) {
994
+ const configArg = call.arguments[0];
995
+ if (!configArg || !ts.isObjectLiteralExpression(configArg))
996
+ return call;
997
+ // Skip if a `__view` is already present (idempotency for re-runs).
998
+ for (const prop of configArg.properties) {
999
+ if (ts.isPropertyAssignment(prop) &&
1000
+ ts.isIdentifier(prop.name) &&
1001
+ prop.name.text === '__view') {
1002
+ return call;
1003
+ }
1004
+ }
1005
+ // Find view: arrow/function.
1006
+ let viewFn = null;
1007
+ for (const prop of configArg.properties) {
1008
+ if (ts.isPropertyAssignment(prop) &&
1009
+ ts.isIdentifier(prop.name) &&
1010
+ prop.name.text === 'view' &&
1011
+ (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))) {
1012
+ viewFn = prop.initializer;
1013
+ break;
1014
+ }
1015
+ }
1016
+ if (!viewFn)
1017
+ return call;
1018
+ const sendParamName = f.createIdentifier('$send');
1019
+ // Build the __view factory body. Two shapes:
1020
+ //
1021
+ // Destructured param — `view: ({ send, text, each }) => ...`
1022
+ // emit `__view: ($send) => ({ send: $send, text, each })`
1023
+ // (tree-shakes unused primitives — the Tier 1.2 size cut).
1024
+ //
1025
+ // Identifier / no param — `view: (h) => ...`, `view: () => ...`,
1026
+ // `view: (send) => ...`, etc.
1027
+ // emit `__view: ($send) => createView($send)`
1028
+ // The compiler can't see which fields `h` is accessed on (it
1029
+ // may be passed to a helper, destructured later, read by
1030
+ // name dynamically). Full bag, instance-cached.
1031
+ const firstParam = viewFn.parameters[0];
1032
+ const isDestructured = !!firstParam && ts.isObjectBindingPattern(firstParam.name);
1033
+ let factoryBody;
1034
+ if (isDestructured) {
1035
+ const entries = [];
1036
+ for (const elem of firstParam.name.elements) {
1037
+ const localName = ts.isIdentifier(elem.name) ? elem.name.text : null;
1038
+ const sourceName = elem.propertyName && ts.isIdentifier(elem.propertyName) ? elem.propertyName.text : localName;
1039
+ if (!localName || !sourceName)
1040
+ continue;
1041
+ if (sourceName === 'send') {
1042
+ entries.push({ localName, primitive: null });
1043
+ continue;
1044
+ }
1045
+ const primitive = VIEW_BAG_FIELD_TO_PRIMITIVE[sourceName];
1046
+ if (!primitive)
1047
+ continue; // unknown name — accessing it at runtime is the user's problem
1048
+ entries.push({ localName, primitive });
1049
+ }
1050
+ const bagProps = [];
1051
+ for (const e of entries) {
1052
+ if (e.primitive === null) {
1053
+ bagProps.push(e.localName === '$send'
1054
+ ? f.createShorthandPropertyAssignment(sendParamName)
1055
+ : f.createPropertyAssignment(f.createIdentifier(e.localName), sendParamName));
1056
+ }
1057
+ else if (e.localName === e.primitive) {
1058
+ bagProps.push(f.createShorthandPropertyAssignment(f.createIdentifier(e.localName)));
1059
+ needed.add(e.primitive);
1060
+ }
1061
+ else {
1062
+ bagProps.push(f.createPropertyAssignment(f.createIdentifier(e.localName), f.createIdentifier(e.primitive)));
1063
+ needed.add(e.primitive);
1064
+ }
1065
+ }
1066
+ factoryBody = f.createParenthesizedExpression(f.createObjectLiteralExpression(bagProps, false));
1067
+ }
1068
+ else {
1069
+ // Identifier-style or zero-arg view: emit `createView($send)` and
1070
+ // pull `createView` into the file imports via cleanupImports.
1071
+ needed.add('createView');
1072
+ factoryBody = f.createCallExpression(f.createIdentifier('createView'), undefined, [
1073
+ sendParamName,
1074
+ ]);
1075
+ }
1076
+ const viewBagFactory = f.createArrowFunction(undefined, undefined, [
1077
+ f.createParameterDeclaration(undefined, undefined, sendParamName, undefined, undefined, undefined),
1078
+ ], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factoryBody);
1079
+ const newConfig = f.createObjectLiteralExpression([...configArg.properties, f.createPropertyAssignment('__view', viewBagFactory)], true);
1080
+ return f.createCallExpression(call.expression, call.typeArguments, [
1081
+ newConfig,
1082
+ ...call.arguments.slice(1),
1083
+ ]);
1084
+ }
1010
1085
  function collectViewHelperAliases(sf, lluiImport, helperNames) {
1011
1086
  const aliases = new Map();
1012
1087
  function addFromBindingPattern(pattern) {
@@ -1115,14 +1190,16 @@ export function isHelperCall(expr, name, helperNames, aliases) {
1115
1190
  // `structuralMaskModule` (v2c/decomp-14). Module fires top-down
1116
1191
  // (transformCallEnter) so the visitor sees the masked options literal.
1117
1192
  // ── Pass 3: Import cleanup ───────────────────────────────────────
1118
- function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElTemplate, usesMemo, usesApplyBinding, usesCloneStaticTemplate, usesRegisterScopeVariants, f) {
1193
+ function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElTemplate, usesMemo, usesApplyBinding, usesCloneStaticTemplate, usesRegisterScopeVariants, viewBagPrimitivesNeeded, usesBindUncertain, f) {
1119
1194
  if (compiled.size === 0 &&
1120
1195
  !usesElTemplate &&
1121
1196
  !usesElSplit &&
1122
1197
  !usesMemo &&
1123
1198
  !usesApplyBinding &&
1124
1199
  !usesCloneStaticTemplate &&
1125
- !usesRegisterScopeVariants)
1200
+ !usesRegisterScopeVariants &&
1201
+ viewBagPrimitivesNeeded.size === 0 &&
1202
+ !usesBindUncertain)
1126
1203
  return sf;
1127
1204
  const clause = lluiImport.importClause;
1128
1205
  if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings))
@@ -1132,6 +1209,10 @@ function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElT
1132
1209
  if (!hasElSplit && usesElSplit) {
1133
1210
  remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('elSplit')));
1134
1211
  }
1212
+ const hasBindUncertain = clause.namedBindings.elements.some((s) => s.name.text === '__bindUncertain');
1213
+ if (!hasBindUncertain && usesBindUncertain) {
1214
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('__bindUncertain')));
1215
+ }
1135
1216
  const hasElTemplate = clause.namedBindings.elements.some((s) => s.name.text === 'elTemplate');
1136
1217
  if (!hasElTemplate && usesElTemplate) {
1137
1218
  remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('elTemplate')));
@@ -1159,8 +1240,19 @@ function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElT
1159
1240
  if (!hasRegisterScopeVariants && usesRegisterScopeVariants) {
1160
1241
  remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('__registerScopeVariants')));
1161
1242
  }
1243
+ // v0.4 size-cut (Tier 1.2): add @llui/dom imports for each primitive
1244
+ // referenced by synthesized __view factories. `ctx` is the destructured
1245
+ // bag name; its primitive is `useContext` (the only rename pair).
1246
+ for (const prim of viewBagPrimitivesNeeded) {
1247
+ if (!clause.namedBindings.elements.some((s) => s.name.text === prim)) {
1248
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier(prim)));
1249
+ }
1250
+ }
1162
1251
  const newBindings = f.createNamedImports(remaining);
1163
- const newClause = f.createImportClause(false, undefined, newBindings);
1252
+ // New TS 6 signature: first arg is `phaseModifier` (undefined =
1253
+ // regular import; `ts.SyntaxKind.TypeKeyword` = `import type`).
1254
+ // The legacy boolean overload is deprecated.
1255
+ const newClause = f.createImportClause(undefined, undefined, newBindings);
1164
1256
  const newImportDecl = f.createImportDeclaration(undefined, newClause, lluiImport.moduleSpecifier);
1165
1257
  let replaced = false;
1166
1258
  const statements = sf.statements.map((stmt) => {
@@ -1168,7 +1260,10 @@ function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElT
1168
1260
  ts.isImportDeclaration(stmt) &&
1169
1261
  ts.isStringLiteral(stmt.moduleSpecifier) &&
1170
1262
  stmt.moduleSpecifier.text === '@llui/dom' &&
1171
- !stmt.importClause?.isTypeOnly) {
1263
+ // `phaseModifier === ts.SyntaxKind.TypeKeyword` is `import type
1264
+ // …`; we only want to rewrite value imports. Replaces deprecated
1265
+ // `isTypeOnly` from the TS<6 API.
1266
+ stmt.importClause?.phaseModifier !== ts.SyntaxKind.TypeKeyword) {
1172
1267
  replaced = true;
1173
1268
  return newImportDecl;
1174
1269
  }