@velarscript/web 0.28.1 → 0.29.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/dist/analysis/watch-cycles.d.ts +143 -0
- package/dist/analysis/watch-cycles.d.ts.map +1 -0
- package/dist/analysis/watch-cycles.js +241 -0
- package/dist/analysis/watch-cycles.js.map +1 -0
- package/dist/analyzer.d.ts +45 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +107 -181
- package/dist/analyzer.js.map +1 -1
- package/dist/emitter.js +25 -5
- package/dist/emitter.js.map +1 -1
- package/dist/runtime-foundation.d.ts.map +1 -1
- package/dist/runtime-foundation.js +5 -5
- package/dist/runtime-foundation.js.map +1 -1
- package/package.json +2 -2
package/dist/analyzer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mechanicalEdits, mechanicalFix, semanticTypeIdentity } from "@velarscript/compiler";
|
|
2
|
-
import { Analyzer, anyType, boolType, describeType, expressionContainsDirectAwait, invalidType, isInvalidType, isAssignable, isReadonlyView,
|
|
2
|
+
import { Analyzer, anyType, boolType, describeType, expressionContainsDirectAwait, invalidType, isInvalidType, isAssignable, isReadonlyView, nullType, nonOptional, numberType, optionalOf, spanIdentity, stringType, unknownType, } from "@velarscript/compiler/extension";
|
|
3
3
|
import { BROWSER_TEST_MODULE, BROWSER_TEST_SOURCE_SUFFIX, browserTestImportGuidance } from "./browser-test.js";
|
|
4
4
|
import { cssTokens } from "./css-tokens.js";
|
|
5
5
|
import { LOOK_ABSENT_MEDIA_SUBJECTS, LOOK_ARITHMETIC_HINT, LOOK_ANIMATION_DIRECTIONS, LOOK_ANIMATION_EASINGS, LOOK_ANIMATION_FILLS, LOOK_BORDER_STYLE_NAMES, LOOK_BUILDER_NUMERIC_RANGES, LOOK_BUILDER_SIGNATURES, LOOK_BUILDERS, LOOK_EXCLUDED_PROPERTIES, LOOK_HOOKS, LOOK_CSS_WIDE_KEYWORDS, LOOK_COLOR_KEYWORDS, LOOK_LARGE_KEYWORD_SETS, LOOK_LENGTH_BUILDERS, LOOK_MEDIA_LENGTH_UNITS, LOOK_MEDIA_SUBJECTS, LOOK_NUMERIC_TYPE_NAMES, LOOK_NON_ANIMATABLE_PROPERTIES, LOOK_PARTIAL_KEYWORD_PROPERTIES, LOOK_PROPERTIES, LOOK_PROPERTY_KEYWORDS, LOOK_PROPERTY_VALUE_KINDS, LOOK_SHARED_METRIC_KEYWORDS, LOOK_TARGETS, LOOK_TOKEN_NAME_RULE, LOOK_TOKEN_NO_FALLBACK_GUIDANCE, LOOK_UNIT_TYPES, LOOK_UNITLESS_PROPERTIES, isLookTokenName, isLookVarReference, lookOwnKeywords, lookShorthandOverlap, lookShorthandParts, lookTokenReference, lookVarReferenceName, nearestLookName, } from "./look.js";
|
|
@@ -10,6 +10,7 @@ import { dynamicChildLeaves, JSX_SCALAR_TEXT_HINT } from "./emitter.js";
|
|
|
10
10
|
import { isWebCustomElementName, WEB_ARIA_ATTRIBUTES, WEB_ARIA_ENUMERATED_VALUES, WEB_ARIA_ROLES, WEB_ARIA_ROLE_SYNONYMS, WEB_BOOL_PRESENCE_HTML_ATTRIBUTES, WEB_HTML_ELEMENTS, WEB_MISSPELLED_ATTRIBUTES, WEB_NATIVE_ELEMENTS, } from "./elements.js";
|
|
11
11
|
import { isWebExpression, isWebJsx, isWebKeyframes, isWebLook, isWebStatement, isWebUnit, } from "./ast.js";
|
|
12
12
|
import { isWebComponentConstructor, isWebComponentType, isWebComputedExport, isWebNodeType, normalizeWebComponentType, webComponentConstructor, webComponentHandle, webComponentIntrinsic, webComponentName, webNodeType, WEB_EVENT_TYPE_NAMES, WEB_OWNED_TYPE_NAMES, } from "./types.js";
|
|
13
|
+
import { collectionMutators, collectReactiveWriters, reactivePathOf, reactivePathRoot, reactiveStepsBelow, reactiveWriteCandidate, statementBindsName, topLevelCall, writerWritesPath, } from "./analysis/watch-cycles.js";
|
|
13
14
|
// The canonical nominal identity of the Web RouteContext record. Route checks
|
|
14
15
|
// probe with this identity so they succeed in modules that use route() without
|
|
15
16
|
// importing RouteContext by name.
|
|
@@ -1181,169 +1182,6 @@ function watchSubjectPath(expression) {
|
|
|
1181
1182
|
return false;
|
|
1182
1183
|
}
|
|
1183
1184
|
}
|
|
1184
|
-
/**
|
|
1185
|
-
* D114 W: a reactive place written as one comparable key, so "the write and the
|
|
1186
|
-
* subject name the same place" is one string equality.
|
|
1187
|
-
*
|
|
1188
|
-
* It is deliberately narrower than `renderWatchSubject`, which reconstructs any
|
|
1189
|
-
* expression for a message. A key has to *decide*, so only the parts that name
|
|
1190
|
-
* the same place on two evaluations are allowed into one: names, fields, and an
|
|
1191
|
-
* index that is either a literal or another such path. `items[next()]` renders
|
|
1192
|
-
* perfectly well and answers a different element every call, so it has no key
|
|
1193
|
-
* and the shapes below stay silent on it — which is the right answer for a
|
|
1194
|
-
* refusal that has to be right every time.
|
|
1195
|
-
*/
|
|
1196
|
-
function reactiveWritePath(expression) {
|
|
1197
|
-
switch (expression.kind) {
|
|
1198
|
-
case "IdentifierExpression":
|
|
1199
|
-
return expression.name;
|
|
1200
|
-
case "MemberExpression": {
|
|
1201
|
-
if (expression.optional)
|
|
1202
|
-
return null;
|
|
1203
|
-
const object = reactiveWritePath(expression.object);
|
|
1204
|
-
return object === null ? null : `${object}.${expression.property}`;
|
|
1205
|
-
}
|
|
1206
|
-
case "IndexExpression": {
|
|
1207
|
-
if (expression.optional)
|
|
1208
|
-
return null;
|
|
1209
|
-
const object = reactiveWritePath(expression.object);
|
|
1210
|
-
if (object === null)
|
|
1211
|
-
return null;
|
|
1212
|
-
const index = expression.index.kind === "LiteralExpression"
|
|
1213
|
-
? (typeof expression.index.value === "string" ? JSON.stringify(expression.index.value) : expression.index.raw)
|
|
1214
|
-
: reactiveWritePath(expression.index);
|
|
1215
|
-
return index === null ? null : `${object}[${index}]`;
|
|
1216
|
-
}
|
|
1217
|
-
default:
|
|
1218
|
-
return null;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
/** The root name a reactive path starts from, which is the binding it resolves through. */
|
|
1222
|
-
function reactivePathRoot(expression) {
|
|
1223
|
-
switch (expression.kind) {
|
|
1224
|
-
case "IdentifierExpression":
|
|
1225
|
-
return expression.name;
|
|
1226
|
-
case "MemberExpression":
|
|
1227
|
-
case "IndexExpression":
|
|
1228
|
-
return reactivePathRoot(expression.object);
|
|
1229
|
-
default:
|
|
1230
|
-
return null;
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
function bindingPatternBinds(pattern, name) {
|
|
1234
|
-
switch (pattern.kind) {
|
|
1235
|
-
case "NameBindingPattern":
|
|
1236
|
-
return pattern.name === name;
|
|
1237
|
-
case "ObjectBindingPattern":
|
|
1238
|
-
return pattern.rest?.name === name || pattern.entries.some((entry) => bindingPatternBinds(entry.pattern, name));
|
|
1239
|
-
case "ListBindingPattern":
|
|
1240
|
-
return pattern.rest?.name === name
|
|
1241
|
-
|| pattern.elements.some((element) => element !== null && bindingPatternBinds(element, name));
|
|
1242
|
-
default:
|
|
1243
|
-
return false;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
/**
|
|
1247
|
-
* D114 W: whether a body statement introduces its own binding of `name`. From
|
|
1248
|
-
* that statement on, the spelling names something else, and a write through it
|
|
1249
|
-
* is not a write of the watched place. The scan stops there rather than
|
|
1250
|
-
* guessing which of the two a later line meant.
|
|
1251
|
-
*/
|
|
1252
|
-
function statementBindsName(statement, name) {
|
|
1253
|
-
switch (statement.kind) {
|
|
1254
|
-
case "VariableDeclaration":
|
|
1255
|
-
return bindingPatternBinds(statement.pattern, name);
|
|
1256
|
-
case "UsingDeclaration":
|
|
1257
|
-
case "FunctionDeclaration":
|
|
1258
|
-
case "ClassDeclaration":
|
|
1259
|
-
return statement.name === name;
|
|
1260
|
-
default:
|
|
1261
|
-
return false;
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
/**
|
|
1265
|
-
* D114 W: the call a body statement makes when the statement is nothing but
|
|
1266
|
-
* that call. `detach` is included because it is how a synchronous watch body
|
|
1267
|
-
* starts asynchronous work — the tour and four charter fences spell the reload
|
|
1268
|
-
* that way — so a refusal that only saw the bare call would miss the shape it
|
|
1269
|
-
* exists for. Everything else (a call inside an `if`, an argument, an assigned
|
|
1270
|
-
* result) is not a plain top-level call and is not offered here.
|
|
1271
|
-
*/
|
|
1272
|
-
function topLevelCall(statement) {
|
|
1273
|
-
const expression = statement.kind === "ExpressionStatement" ? statement.expression
|
|
1274
|
-
: statement.kind === "DetachStatement" ? statement.expression
|
|
1275
|
-
: null;
|
|
1276
|
-
return expression !== null && expression.kind === "CallExpression" ? expression : null;
|
|
1277
|
-
}
|
|
1278
|
-
/**
|
|
1279
|
-
* D114 W: whether one plain body statement writes the reactive place `path`.
|
|
1280
|
-
* An assignment or a compound assignment to it is one; so is a call of a
|
|
1281
|
-
* mutating collection method on it, because a watch on a collection fires on
|
|
1282
|
-
* its deep mutation and `mutating` is the compiler's own roster of the calls
|
|
1283
|
-
* that mutate.
|
|
1284
|
-
*/
|
|
1285
|
-
function reactiveWriteOf(statement, path, mutating) {
|
|
1286
|
-
if (statement.kind === "AssignmentStatement")
|
|
1287
|
-
return reactiveWritePath(statement.target) === path;
|
|
1288
|
-
if (mutating === null)
|
|
1289
|
-
return false;
|
|
1290
|
-
const call = statement.kind === "ExpressionStatement" && statement.expression.kind === "CallExpression"
|
|
1291
|
-
? statement.expression
|
|
1292
|
-
: null;
|
|
1293
|
-
if (call === null || call.callee.kind !== "MemberExpression" || call.callee.optional)
|
|
1294
|
-
return false;
|
|
1295
|
-
return mutating.has(call.callee.property) && reactiveWritePath(call.callee.object) === path;
|
|
1296
|
-
}
|
|
1297
|
-
/**
|
|
1298
|
-
* D114 W A2(b): whether an `action` or `async def` writes `path` at its own top
|
|
1299
|
-
* level, unconditionally. One hop: what the callee itself calls is not
|
|
1300
|
-
* followed. A parameter of the callee's own that is spelled like the path's
|
|
1301
|
-
* root, or a binding it declares before the write, means the write is not of
|
|
1302
|
-
* the watched place and the answer is no.
|
|
1303
|
-
*/
|
|
1304
|
-
function writerWritesPath(writer, path, root, mutating) {
|
|
1305
|
-
if (writer.parameters.includes(root))
|
|
1306
|
-
return false;
|
|
1307
|
-
for (const statement of writer.body) {
|
|
1308
|
-
if (statementBindsName(statement, root))
|
|
1309
|
-
return false;
|
|
1310
|
-
if (reactiveWriteOf(statement, path, mutating))
|
|
1311
|
-
return true;
|
|
1312
|
-
}
|
|
1313
|
-
return false;
|
|
1314
|
-
}
|
|
1315
|
-
function collectReactiveWriters(program) {
|
|
1316
|
-
const writers = new Map();
|
|
1317
|
-
const claim = (name, declaration) => {
|
|
1318
|
-
writers.set(name, writers.has(name) ? null : declaration);
|
|
1319
|
-
};
|
|
1320
|
-
const record = (statements) => {
|
|
1321
|
-
for (const statement of statements) {
|
|
1322
|
-
if (statement.kind === "FunctionDeclaration") {
|
|
1323
|
-
claim(statement.name, statement.asynchronous
|
|
1324
|
-
? { spelling: "async def", parameters: statement.parameters.map((parameter) => parameter.name), body: statement.body }
|
|
1325
|
-
: null);
|
|
1326
|
-
record(statement.body);
|
|
1327
|
-
continue;
|
|
1328
|
-
}
|
|
1329
|
-
if (!isWebStatement(statement))
|
|
1330
|
-
continue;
|
|
1331
|
-
if (statement.kind === "ExtensionStatement:web:action") {
|
|
1332
|
-
claim(statement.name, {
|
|
1333
|
-
spelling: "action",
|
|
1334
|
-
parameters: statement.parameters.map((parameter) => parameter.name),
|
|
1335
|
-
body: statement.body,
|
|
1336
|
-
});
|
|
1337
|
-
record(statement.body);
|
|
1338
|
-
continue;
|
|
1339
|
-
}
|
|
1340
|
-
if (statement.kind === "ExtensionStatement:web:component")
|
|
1341
|
-
record(statement.body);
|
|
1342
|
-
}
|
|
1343
|
-
};
|
|
1344
|
-
record(program.body);
|
|
1345
|
-
return writers;
|
|
1346
|
-
}
|
|
1347
1185
|
/** The escapes a text literal carries back into source (`scanStringEscape`). */
|
|
1348
1186
|
const WATCH_SUBJECT_TEXT_ESCAPES = {
|
|
1349
1187
|
"\\": "\\\\",
|
|
@@ -2034,11 +1872,19 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
2034
1872
|
* `builtinTypeNameDeclarationMessage` in packages/compiler/src/analyzer.ts,
|
|
2035
1873
|
* reported as VEL3007. The rosters differ; the wording is meant to read
|
|
2036
1874
|
* alike, so a change to either sentence belongs in both.
|
|
1875
|
+
*
|
|
1876
|
+
* `Duration` is on both rosters — Core owns it as a primitive and
|
|
1877
|
+
* `velar/look` republishes it — so a Web module used to report it twice. This
|
|
1878
|
+
* refusal is the more specific of the two, because it names the surface the
|
|
1879
|
+
* author is writing against, so it marks the name refused and Core's stays
|
|
1880
|
+
* unsaid. The mark is Core's own hook, which is what lets this pass take
|
|
1881
|
+
* precedence without either side learning the other's roster.
|
|
2037
1882
|
*/
|
|
2038
1883
|
rejectWebOwnedTypeNames(program) {
|
|
2039
1884
|
const reject = (name, errorSpan, noun) => {
|
|
2040
1885
|
if (!WEB_OWNED_TYPE_NAMES.has(name))
|
|
2041
1886
|
return;
|
|
1887
|
+
this.markTypeNameRefused(name);
|
|
2042
1888
|
this.diagnostics.push(diagnostic("VEL5065", `'${name}' is a Web type name, so it cannot also name ${/^[aeiou]/iu.test(noun) ? "an" : "a"} ${noun}; every use of it in a Web module resolves to the built-in. Rename this declaration`, errorSpan));
|
|
2043
1889
|
};
|
|
2044
1890
|
for (const statement of program.body) {
|
|
@@ -2659,12 +2505,8 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
2659
2505
|
const root = reactivePathRoot(subject);
|
|
2660
2506
|
if (root === null)
|
|
2661
2507
|
return;
|
|
2662
|
-
const
|
|
2663
|
-
const
|
|
2664
|
-
const mutating = collection.kind === "list" || collection.kind === "map"
|
|
2665
|
-
|| collection.kind === "set" || collection.kind === "record"
|
|
2666
|
-
? mutatingCollectionMethods(collection.kind)
|
|
2667
|
-
: null;
|
|
2508
|
+
const place = reactivePathOf(subject);
|
|
2509
|
+
const writes = (steps, method) => this.watchSubjectWrite(watched, steps, method);
|
|
2668
2510
|
// A resource publishes `value`, `loading`, `ready` and `error`, and
|
|
2669
2511
|
// `reload` is the one member of the five that is not one of them. Asking it
|
|
2670
2512
|
// that way keeps `analyzeResourceDeclaration`'s field map the only roster:
|
|
@@ -2677,14 +2519,9 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
2677
2519
|
for (const statement of body) {
|
|
2678
2520
|
if (statementBindsName(statement, root))
|
|
2679
2521
|
return;
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
// would hand the author a line that does not compile.
|
|
2684
|
-
const derived = subject.kind === "IdentifierExpression"
|
|
2685
|
-
? `declare 'computed ${path} = ...' instead`
|
|
2686
|
-
: "write this value where it is produced instead";
|
|
2687
|
-
this.diagnostics.push(diagnostic("VEL5077", `This watch writes its own subject '${path}' at the top of its body, so every run re-triggers it and the runtime stops the loop after 100 rounds; write the condition that ends it, or watch the input this value follows and ${derived}`, statement.span));
|
|
2522
|
+
const selfWrite = place === null ? null : this.watchSelfWrite(subject, place, statement, writes);
|
|
2523
|
+
if (selfWrite !== null) {
|
|
2524
|
+
this.diagnostics.push(diagnostic("VEL5077", selfWrite, statement.span));
|
|
2688
2525
|
return;
|
|
2689
2526
|
}
|
|
2690
2527
|
const call = topLevelCall(statement);
|
|
@@ -2696,15 +2533,104 @@ export class VelarWebAnalyzer extends Analyzer {
|
|
|
2696
2533
|
this.diagnostics.push(diagnostic("VEL5078", `This watch reloads '${resource}' — the resource it watches — so every completed load re-triggers it; watch the input the load reads instead, as 'watch userId:' with 'detach ${resource}.reload()' in its body`, call.span));
|
|
2697
2534
|
return;
|
|
2698
2535
|
}
|
|
2699
|
-
if (
|
|
2536
|
+
if (place === null || call.callee.kind !== "IdentifierExpression")
|
|
2700
2537
|
continue;
|
|
2701
2538
|
const writer = this.reactiveWriters.get(call.callee.name) ?? null;
|
|
2702
|
-
|
|
2539
|
+
const written = writer === null ? null : writerWritesPath(writer, place, writes);
|
|
2540
|
+
if (written === null)
|
|
2703
2541
|
continue;
|
|
2704
|
-
|
|
2542
|
+
// D114 0.28.0 H-D1's other half, in the message family F1 gave VEL5077:
|
|
2543
|
+
// a writer that reaches a *part* of the subject names the part it wrote
|
|
2544
|
+
// and the subject it belongs to, because those are two different places
|
|
2545
|
+
// and the author has to find the one the helper touches.
|
|
2546
|
+
const reached = written.text === place.text
|
|
2547
|
+
? `'${place.text}' — the reactive value this watch is on`
|
|
2548
|
+
: `'${written.text}', a part of its subject '${place.text}'`;
|
|
2549
|
+
this.diagnostics.push(diagnostic("VEL5079", `This watch starts '${call.callee.name}', which writes ${reached} — so each completed run re-triggers the watch; make the write conditional, or watch the input '${call.callee.name}' reads`, call.span));
|
|
2705
2550
|
return;
|
|
2706
2551
|
}
|
|
2707
2552
|
}
|
|
2553
|
+
/**
|
|
2554
|
+
* D114 0.28.0 H-D1: the VEL5077 message one plain body statement earns, or
|
|
2555
|
+
* null when it earns none.
|
|
2556
|
+
*
|
|
2557
|
+
* §15 says a watch fires on a *deep* change of its subject, so `watch form:
|
|
2558
|
+
* form.name = …` and `watch items: items[0].done = …` are the same ring
|
|
2559
|
+
* `items.append(…)` already is — decided at the top of the body, with no
|
|
2560
|
+
* condition to end it — and were silent until the runtime's 100-round cap
|
|
2561
|
+
* stopped them. The rule is therefore stated on the path rather than on the
|
|
2562
|
+
* spelling: a write whose place is the subject, or any place below it, in an
|
|
2563
|
+
* assignment, a compound assignment, or a mutating call.
|
|
2564
|
+
*
|
|
2565
|
+
* Every existing exclusion stands, because each is answered somewhere else: a
|
|
2566
|
+
* conditional or nested write is not a plain body statement, a rebinding
|
|
2567
|
+
* stops the scan in `rejectWatchCycle`, and a sibling path (`watch form.name:`
|
|
2568
|
+
* writing `form.email`) or a different root fails the step comparison here.
|
|
2569
|
+
*/
|
|
2570
|
+
watchSelfWrite(subject, place, statement, writes) {
|
|
2571
|
+
const write = reactiveWriteCandidate(statement);
|
|
2572
|
+
if (write === null)
|
|
2573
|
+
return null;
|
|
2574
|
+
const steps = reactiveStepsBelow(place, write.place);
|
|
2575
|
+
if (steps === null || !writes(steps, write.method))
|
|
2576
|
+
return null;
|
|
2577
|
+
// A derived value is offered only where it could be declared. A field
|
|
2578
|
+
// or an element has no `computed` spelling of its own, so naming one
|
|
2579
|
+
// would hand the author a line that does not compile.
|
|
2580
|
+
const derived = subject.kind === "IdentifierExpression"
|
|
2581
|
+
? `declare 'computed ${place.text} = ...' instead`
|
|
2582
|
+
: "write this value where it is produced instead";
|
|
2583
|
+
const head = steps.length === 0
|
|
2584
|
+
? `This watch writes its own subject '${place.text}'`
|
|
2585
|
+
: `This watch writes '${write.place.text}', a part of its subject '${place.text}',`;
|
|
2586
|
+
return `${head} at the top of its body, so every run re-triggers it and the runtime stops the loop after 100`
|
|
2587
|
+
+ ` rounds; write the condition that ends it, or watch the input this value follows and ${derived}`;
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* D114 0.28.0 H-D1: the type of the place `steps` below the watched subject,
|
|
2591
|
+
* or null when the walk cannot reach one.
|
|
2592
|
+
*
|
|
2593
|
+
* Only the steps the reactive graph publishes as part of the subject are
|
|
2594
|
+
* walked — a record field and a collection element — because those are the
|
|
2595
|
+
* writes a watch on the containing value is woken by. A step that leaves them
|
|
2596
|
+
* (a class instance, a capability handle, a field the record does not declare)
|
|
2597
|
+
* is not provably part of the subject, and a refusal that must be right every
|
|
2598
|
+
* time answers no there rather than guessing.
|
|
2599
|
+
*/
|
|
2600
|
+
/**
|
|
2601
|
+
* Whether a write `steps` below the watched subject, made the given way, is a
|
|
2602
|
+
* write of the subject — the one definition both the body scan (VEL5077) and
|
|
2603
|
+
* the one-hop writer scan (VEL5079) read. An assignment to a place the walk
|
|
2604
|
+
* can reach is a write; a call is one only when the type at that depth is a
|
|
2605
|
+
* collection and the call is on its own mutating roster.
|
|
2606
|
+
*/
|
|
2607
|
+
watchSubjectWrite(watched, steps, method) {
|
|
2608
|
+
const written = this.reactivePlaceType(watched, steps);
|
|
2609
|
+
if (written === null)
|
|
2610
|
+
return false;
|
|
2611
|
+
if (method === null)
|
|
2612
|
+
return true;
|
|
2613
|
+
const mutating = collectionMutators(nonOptional(this.expandAliases(written)));
|
|
2614
|
+
return mutating !== null && mutating.has(method);
|
|
2615
|
+
}
|
|
2616
|
+
reactivePlaceType(subject, steps) {
|
|
2617
|
+
let current = subject;
|
|
2618
|
+
for (const step of steps) {
|
|
2619
|
+
const owner = nonOptional(this.expandAliases(current));
|
|
2620
|
+
const next = step.kind === "field"
|
|
2621
|
+
? (owner.kind === "object" ? owner.fields.get(step.name) ?? null
|
|
2622
|
+
: owner.kind === "record" ? owner.value
|
|
2623
|
+
: owner.kind === "named" ? this.fieldsOf(owner.identity ?? owner.name)?.get(step.name) ?? null
|
|
2624
|
+
: null)
|
|
2625
|
+
: (owner.kind === "list" || owner.kind === "set" ? owner.element
|
|
2626
|
+
: owner.kind === "map" || owner.kind === "record" ? owner.value
|
|
2627
|
+
: null);
|
|
2628
|
+
if (next === null)
|
|
2629
|
+
return null;
|
|
2630
|
+
current = next;
|
|
2631
|
+
}
|
|
2632
|
+
return current;
|
|
2633
|
+
}
|
|
2708
2634
|
/**
|
|
2709
2635
|
* D89 A4: records `list = list.map(item => {…})`, React's immutable update,
|
|
2710
2636
|
* where the callback builds a new record rather than changing a field.
|