modality-ts 0.0.3 → 0.0.5

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.
Files changed (67) hide show
  1. package/dist/checker/search/eval.d.ts.map +1 -1
  2. package/dist/checker/search/eval.js +57 -3
  3. package/dist/checker/search/eval.js.map +1 -1
  4. package/dist/checker/search/index.d.ts.map +1 -1
  5. package/dist/checker/search/index.js +32 -8
  6. package/dist/checker/search/index.js.map +1 -1
  7. package/dist/extraction/index.d.ts +4 -2
  8. package/dist/extraction/index.d.ts.map +1 -1
  9. package/dist/extraction/index.js +102 -21
  10. package/dist/extraction/index.js.map +1 -1
  11. package/dist/extraction/pipeline/index.d.ts +2 -0
  12. package/dist/extraction/pipeline/index.d.ts.map +1 -1
  13. package/dist/extraction/pipeline/index.js +3 -1
  14. package/dist/extraction/pipeline/index.js.map +1 -1
  15. package/dist/harness/index.d.ts +29 -2
  16. package/dist/harness/index.d.ts.map +1 -1
  17. package/dist/harness/index.js +79 -9
  18. package/dist/harness/index.js.map +1 -1
  19. package/dist/kernel/overlay/index.d.ts +16 -1
  20. package/dist/kernel/overlay/index.d.ts.map +1 -1
  21. package/dist/kernel/overlay/index.js +56 -1
  22. package/dist/kernel/overlay/index.js.map +1 -1
  23. package/dist/kernel/props/index.d.ts +1 -0
  24. package/dist/kernel/props/index.d.ts.map +1 -1
  25. package/dist/kernel/props/index.js +1 -0
  26. package/dist/kernel/props/index.js.map +1 -1
  27. package/dist/kernel/report/types.d.ts +4 -0
  28. package/dist/kernel/report/types.d.ts.map +1 -1
  29. package/dist/modality/cli.js +15 -9
  30. package/dist/modality/cli.js.map +1 -1
  31. package/dist/modality/codegen/replay-test.d.ts.map +1 -1
  32. package/dist/modality/codegen/replay-test.js +13 -4
  33. package/dist/modality/codegen/replay-test.js.map +1 -1
  34. package/dist/modality/features/ci/command.d.ts +2 -0
  35. package/dist/modality/features/ci/command.d.ts.map +1 -1
  36. package/dist/modality/features/ci/command.js +2 -0
  37. package/dist/modality/features/ci/command.js.map +1 -1
  38. package/dist/modality/features/conform/command.d.ts +2 -0
  39. package/dist/modality/features/conform/command.d.ts.map +1 -1
  40. package/dist/modality/features/conform/command.js +91 -6
  41. package/dist/modality/features/conform/command.js.map +1 -1
  42. package/dist/modality/features/export/command.d.ts +29 -1
  43. package/dist/modality/features/export/command.d.ts.map +1 -1
  44. package/dist/modality/features/export/command.js +87 -1
  45. package/dist/modality/features/export/command.js.map +1 -1
  46. package/dist/modality/features/export/index.d.ts +2 -2
  47. package/dist/modality/features/export/index.d.ts.map +1 -1
  48. package/dist/modality/features/export/index.js +1 -1
  49. package/dist/modality/features/export/index.js.map +1 -1
  50. package/dist/modality/features/extract/command.js +4 -4
  51. package/dist/modality/features/extract/command.js.map +1 -1
  52. package/dist/modality/features/replay/command.d.ts +2 -0
  53. package/dist/modality/features/replay/command.d.ts.map +1 -1
  54. package/dist/modality/features/replay/command.js +89 -6
  55. package/dist/modality/features/replay/command.js.map +1 -1
  56. package/dist/modality/overlay.d.ts +2 -1
  57. package/dist/modality/overlay.d.ts.map +1 -1
  58. package/dist/modality/overlay.js +13 -2
  59. package/dist/modality/overlay.js.map +1 -1
  60. package/dist/runtime/index.d.ts.map +1 -1
  61. package/dist/runtime/index.js +22 -2
  62. package/dist/runtime/index.js.map +1 -1
  63. package/dist/sources/swr/index.d.ts +3 -0
  64. package/dist/sources/swr/index.d.ts.map +1 -1
  65. package/dist/sources/swr/index.js +40 -3
  66. package/dist/sources/swr/index.js.map +1 -1
  67. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import * as ts from "typescript";
2
+ import { effectReads, effectWrites } from "modality-ts/kernel";
2
3
  export * from "./pipeline/index.js";
3
4
  export * from "./spi/index.js";
4
5
  function setterBindingFromDecl(decl) {
@@ -48,6 +49,8 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
48
49
  const warnings = [];
49
50
  const route = options.route ?? "/";
50
51
  const effectApis = new Set(options.effectApis ?? []);
52
+ const sourcePlugins = options.sourcePlugins ?? [];
53
+ const routerPlugin = options.routerPlugin;
51
54
  const setters = new Map();
52
55
  const globalTaints = new Set();
53
56
  const components = componentDeclarations(source);
@@ -137,7 +140,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
137
140
  }
138
141
  }
139
142
  if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.initializer && isForwardablePropName(node.name.text) && !isIntrinsicJsxAttribute(node)) {
140
- const extracted = transitionsFromComponentPropAttribute(source, fileName, node, setters, handlers, components, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, warnings);
143
+ const extracted = transitionsFromComponentPropAttribute(source, fileName, node, setters, handlers, components, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, warnings);
141
144
  transitions.push(...extracted);
142
145
  if (extracted.length === 0) {
143
146
  warnings.push({ message: `Unextractable handler ${nextComponent ?? "Anonymous"}.${node.name.text}`, ...lineAndColumn(source, node) });
@@ -167,7 +170,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
167
170
  renderGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous", guardLocals),
168
171
  disabledGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous", guardLocals)
169
172
  ]);
170
- const extracted = transitionsFromJsxAttribute(source, fileName, node, setters, handlers, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, guard, warnings);
173
+ const extracted = transitionsFromJsxAttribute(source, fileName, node, setters, handlers, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, guard, warnings);
171
174
  transitions.push(...extracted);
172
175
  if (extracted.length === 0 && !forwardsComponentProp(node, handlers, components.get(nextComponent ?? "")) && !handlerSchedulesModeledTimer(node, handlers, setters)) {
173
176
  warnings.push({ message: `Unextractable handler ${nextComponent ?? "Anonymous"}.${node.name.text}`, ...lineAndColumn(source, node) });
@@ -501,7 +504,7 @@ function handlerSchedulesModeledTimer(attribute, handlers, setters) {
501
504
  visit(handler.body);
502
505
  return found;
503
506
  }
504
- function transitionsFromJsxAttribute(source, fileName, node, setters, handlers, component, effectApis, asyncOutcomes, disabledGuard, warnings) {
507
+ function transitionsFromJsxAttribute(source, fileName, node, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, warnings) {
505
508
  if (!node.initializer)
506
509
  return [];
507
510
  const expression = ts.isJsxExpression(node.initializer) ? node.initializer.expression : undefined;
@@ -512,9 +515,9 @@ function transitionsFromJsxAttribute(source, fileName, node, setters, handlers,
512
515
  return [];
513
516
  const attr = node.name.text;
514
517
  const locator = locatorForEventAttribute(node);
515
- return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, disabledGuard, locator, warnings), handler);
518
+ return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, locator, warnings), handler);
516
519
  }
517
- function transitionsFromComponentPropAttribute(source, fileName, node, setters, handlers, components, component, effectApis, asyncOutcomes, warnings) {
520
+ function transitionsFromComponentPropAttribute(source, fileName, node, setters, handlers, components, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, warnings) {
518
521
  if (!node.initializer || !ts.isIdentifier(node.name))
519
522
  return [];
520
523
  const tag = jsxTagName(node);
@@ -535,7 +538,7 @@ function transitionsFromComponentPropAttribute(source, fileName, node, setters,
535
538
  renderGuardFor(node, setters, warnings, source, component, guardLocals),
536
539
  disabledGuardFor(node, setters, warnings, source, component, guardLocals)
537
540
  ]);
538
- return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, trigger.attr, handler, setters, handlers, component, effectApis, asyncOutcomes, combineParsedGuards([trigger.guard, callerGuard]), trigger.locator, warnings), handler);
541
+ return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, trigger.attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, combineParsedGuards([trigger.guard, callerGuard]), trigger.locator, warnings), handler);
539
542
  }
540
543
  function transitionsFromBoundedListAttribute(source, fileName, node, setters, handlers, component, listInfo) {
541
544
  if (!node.initializer || !ts.isIdentifier(node.name))
@@ -595,7 +598,7 @@ function normalizedAstKey(node) {
595
598
  .replace(/\/\/.*$/gm, "")
596
599
  .replace(/\s+/g, "");
597
600
  }
598
- function transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, disabledGuard, locator, warnings) {
601
+ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, locator, warnings) {
599
602
  const asyncTransitions = transitionsFromAsyncHandler(source, fileName, attr, handler, setters, component, effectApis, asyncOutcomes, locator, warnings);
600
603
  if (asyncTransitions.length > 0)
601
604
  return applyParsedGuard(asyncTransitions, disabledGuard);
@@ -614,15 +617,18 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
614
617
  const inlined = inlinedHelperCall(summary.call, handlers, setters);
615
618
  const inlinedCall = inlined?.call ?? summary.call;
616
619
  const locals = inlined?.locals ?? summary.locals;
617
- const navigation = navigationTransition(source, fileName, node, attr, component, inlinedCall, locator);
620
+ const navigation = navigationTransition(source, fileName, node, attr, component, inlinedCall, locator, routerPlugin);
618
621
  if (navigation)
619
622
  return applyParsedGuard([navigation], disabledGuard);
623
+ const pluginWrite = pluginWriteTransition(source, fileName, node, attr, component, inlinedCall, setters, locals, sourcePlugins, locator);
624
+ if (pluginWrite)
625
+ return applyParsedGuard([pluginWrite], disabledGuard);
620
626
  const swrMutate = swrMutateTransition(source, fileName, node, attr, component, inlinedCall, locator);
621
627
  if (swrMutate)
622
628
  return applyParsedGuard([swrMutate], disabledGuard);
623
629
  const setterCall = setterCallFrom(inlinedCall, setters);
624
630
  if (!setterCall) {
625
- const escaped = escapedSetters(inlinedCall, setters);
631
+ const escaped = escapedSetters(inlinedCall, setters, locals);
626
632
  if (escaped.length === 0)
627
633
  return [];
628
634
  return applyParsedGuard(escapedSetterTransitions(source, fileName, node, attr, component, escaped, locator), disabledGuard);
@@ -818,6 +824,10 @@ function valueExpr(expression, setters, locals) {
818
824
  }
819
825
  if (ts.isParenthesizedExpression(expression))
820
826
  return valueExpr(expression.expression, setters, locals);
827
+ if (ts.isBinaryExpression(expression) &&
828
+ (expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || expression.operatorToken.kind === ts.SyntaxKind.BarBarToken)) {
829
+ return booleanExpr(expression, setters, locals);
830
+ }
821
831
  if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
822
832
  return nullishOptionalReadExpr(expression, setters, locals);
823
833
  }
@@ -1076,6 +1086,66 @@ function callSummaryFromHandler(handler, setters, initialLocals = new Map()) {
1076
1086
  }
1077
1087
  return undefined;
1078
1088
  }
1089
+ function pluginWriteTransition(source, fileName, node, attr, component, call, setters, locals, sourcePlugins, locator) {
1090
+ const callee = callName(call.expression);
1091
+ if (!callee)
1092
+ return undefined;
1093
+ const ctx = {
1094
+ read: (name, path) => {
1095
+ const local = locals.get(name);
1096
+ if (local?.expr.kind === "read") {
1097
+ return { kind: "read", var: local.expr.var, path: [...(local.expr.path ?? []), ...(path ?? [])] };
1098
+ }
1099
+ const varId = stateVarForName(name, setters) ?? name;
1100
+ return { kind: "read", var: varId, ...(path && path.length > 0 ? { path } : {}) };
1101
+ },
1102
+ locator
1103
+ };
1104
+ const callSite = {
1105
+ callee,
1106
+ arguments: call.arguments.map(callArgumentValue),
1107
+ source: { file: fileName, ...lineAndColumn(source, call) }
1108
+ };
1109
+ for (const plugin of sourcePlugins) {
1110
+ const summary = plugin.summarizeWrite?.(callSite, ctx);
1111
+ if (!summary || summary === "unsupported")
1112
+ continue;
1113
+ const reads = [...effectReads(summary)].sort();
1114
+ const writes = [...effectWrites(summary)].sort();
1115
+ return {
1116
+ id: `${component}.${attr}.${safeId(plugin.id)}.${safeId(callee)}`,
1117
+ cls: "user",
1118
+ label: labelForEvent(attr, locator),
1119
+ source: [{ file: fileName, ...lineAndColumn(source, node) }],
1120
+ guard: { kind: "lit", value: true },
1121
+ effect: summary,
1122
+ reads,
1123
+ writes,
1124
+ confidence: "exact"
1125
+ };
1126
+ }
1127
+ return undefined;
1128
+ }
1129
+ function callArgumentValue(argument) {
1130
+ const literal = literalValue(argument);
1131
+ if (literal !== undefined)
1132
+ return literal;
1133
+ if (ts.isIdentifier(argument))
1134
+ return argument.text;
1135
+ if (ts.isObjectLiteralExpression(argument)) {
1136
+ const fields = {};
1137
+ for (const property of argument.properties) {
1138
+ if (!ts.isPropertyAssignment(property))
1139
+ return argument.getText();
1140
+ const name = propertyName(property.name);
1141
+ if (!name)
1142
+ return argument.getText();
1143
+ fields[name] = callArgumentValue(property.initializer);
1144
+ }
1145
+ return fields;
1146
+ }
1147
+ return argument.getText();
1148
+ }
1079
1149
  function swrMutateTransition(source, fileName, node, attr, component, call, locator) {
1080
1150
  if (!ts.isIdentifier(call.expression) || call.expression.text !== "mutate")
1081
1151
  return undefined;
@@ -1099,7 +1169,8 @@ function bindConstStatement(statement, setters, locals, partialBoolean = false)
1099
1169
  for (const declaration of statement.declarationList.declarations) {
1100
1170
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
1101
1171
  return false;
1102
- const binding = valueExpr(declaration.initializer, setters, locals) ??
1172
+ const setterAlias = ts.isIdentifier(declaration.initializer) ? setters.get(declaration.initializer.text) ?? locals.get(declaration.initializer.text)?.setter : undefined;
1173
+ const binding = setterAlias ? { expr: { kind: "lit", value: null }, reads: [], setter: setterAlias } : valueExpr(declaration.initializer, setters, locals) ??
1103
1174
  (partialBoolean ? parseConjunctiveGuardExpression(declaration.initializer, setters, locals) : booleanExpr(declaration.initializer, setters, locals));
1104
1175
  if (!binding)
1105
1176
  return false;
@@ -1113,8 +1184,8 @@ function inlinedHelperCall(call, handlers, setters) {
1113
1184
  const helper = handlers.get(call.expression.text);
1114
1185
  return helper ? callSummaryFromHandler(helper, setters) : undefined;
1115
1186
  }
1116
- function navigationTransition(source, fileName, node, attr, component, call, locator) {
1117
- const navigation = navigationCall(call);
1187
+ function navigationTransition(source, fileName, node, attr, component, call, locator, routerPlugin) {
1188
+ const navigation = navigationCall(call, routerPlugin);
1118
1189
  if (!navigation)
1119
1190
  return undefined;
1120
1191
  const routeId = navigation.to ? safeId(navigation.to) : "back";
@@ -1138,10 +1209,13 @@ function navigationTransition(source, fileName, node, attr, component, call, loc
1138
1209
  confidence: "exact"
1139
1210
  };
1140
1211
  }
1141
- function navigationCall(call) {
1212
+ function navigationCall(call, routerPlugin) {
1142
1213
  const name = callName(call.expression);
1143
1214
  if (!name)
1144
1215
  return undefined;
1216
+ const pluginNavigation = routerPlugin?.navigationCall(name, call.arguments.map(callArgumentValue));
1217
+ if (pluginNavigation && pluginNavigation !== "unsupported")
1218
+ return pluginNavigation;
1145
1219
  if (name === "navigate" && call.arguments.length === 1) {
1146
1220
  const to = literalValue(call.arguments[0]);
1147
1221
  return typeof to === "string" ? { mode: "push", to } : undefined;
@@ -1157,10 +1231,10 @@ function navigationCall(call) {
1157
1231
  }
1158
1232
  return undefined;
1159
1233
  }
1160
- function escapedSetters(call, setters) {
1234
+ function escapedSetters(call, setters, locals = new Map()) {
1161
1235
  return call.arguments
1162
1236
  .filter(ts.isIdentifier)
1163
- .map((arg) => setters.get(arg.text))
1237
+ .map((arg) => setters.get(arg.text) ?? locals.get(arg.text)?.setter)
1164
1238
  .filter((setter) => Boolean(setter));
1165
1239
  }
1166
1240
  function escapedSetterTransitions(source, fileName, node, attr, component, setters, locator) {
@@ -1990,6 +2064,7 @@ function renderGuardFor(eventAttribute, setters, warnings, source, component, lo
1990
2064
  const element = jsxElementForAttribute(eventAttribute);
1991
2065
  if (!element)
1992
2066
  return undefined;
2067
+ const guards = [];
1993
2068
  let current = element;
1994
2069
  while (current.parent) {
1995
2070
  const parent = current.parent;
@@ -2001,7 +2076,9 @@ function renderGuardFor(eventAttribute, setters, warnings, source, component, lo
2001
2076
  warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.left) });
2002
2077
  return undefined;
2003
2078
  }
2004
- return parsed;
2079
+ guards.push(parsed);
2080
+ current = parent;
2081
+ continue;
2005
2082
  }
2006
2083
  if (ts.isConditionalExpression(parent) && parent.whenTrue === current) {
2007
2084
  const parsed = parseConjunctiveGuardExpression(parent.condition, setters, locals);
@@ -2009,7 +2086,9 @@ function renderGuardFor(eventAttribute, setters, warnings, source, component, lo
2009
2086
  warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.condition) });
2010
2087
  return undefined;
2011
2088
  }
2012
- return parsed;
2089
+ guards.push(parsed);
2090
+ current = parent;
2091
+ continue;
2013
2092
  }
2014
2093
  if (ts.isConditionalExpression(parent) && parent.whenFalse === current) {
2015
2094
  const parsed = parseConjunctiveGuardExpression(parent.condition, setters, locals);
@@ -2017,15 +2096,17 @@ function renderGuardFor(eventAttribute, setters, warnings, source, component, lo
2017
2096
  warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.condition) });
2018
2097
  return undefined;
2019
2098
  }
2020
- return { expr: { kind: "not", args: [parsed.expr] }, reads: parsed.reads };
2099
+ guards.push({ expr: { kind: "not", args: [parsed.expr] }, reads: parsed.reads });
2100
+ current = parent;
2101
+ continue;
2021
2102
  }
2022
- if (ts.isParenthesizedExpression(parent) || ts.isJsxExpression(parent)) {
2103
+ if (ts.isParenthesizedExpression(parent) || ts.isJsxExpression(parent) || ts.isJsxElement(parent) || ts.isJsxFragment(parent)) {
2023
2104
  current = parent;
2024
2105
  continue;
2025
2106
  }
2026
- return undefined;
2107
+ return combineParsedGuards(guards);
2027
2108
  }
2028
- return undefined;
2109
+ return combineParsedGuards(guards);
2029
2110
  }
2030
2111
  function jsxElementForAttribute(attribute) {
2031
2112
  const attrs = attribute.parent;