@volter/editor-react 0.5.210 → 0.5.212

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.
@@ -1,21 +1,6 @@
1
- /**
2
- * The relative import specifier a human would write from one module to
3
- * another — the ONE copy of the shared-prefix walk that existed five times
4
- * (owner-directed overlap audit, 2026-08-22): `R3fSourceAuthoringAdapter.
5
- * relativeModuleSpecifier`, `ReactRootAuthoringAdapter.relativeProjectModule`,
6
- * `pixi-source-write-target`'s `relativeProjectModule`, and the
7
- * `relativeSpecifier` locals in `plan-fork-component.ts` and
8
- * `plan-extract-component.ts`.
9
- *
10
- * Dependency-free on purpose (no `node:path`): two of the five consumers are
11
- * pure planner modules that must stay headlessly probeable, and a specifier is
12
- * a POSIX-slash artifact regardless of host platform.
13
- *
14
- * What stays at the call site, deliberately: resolving the two paths into the
15
- * SAME address space (project-relative vs absolute — both work, the walk eats
16
- * any shared prefix, but they must match). A source extension on the target is
17
- * stripped; an extension-less module id passes through unchanged.
18
- */
1
+ /** Construct a relative import specifier from two paths in the same address
2
+ * space, using POSIX slashes and stripping the target source extension. No
3
+ * platform path API is needed, so browser and Node planners share this walk. */
19
4
  export function relativeImportSpecifier(fromFile: string, toModule: string): string {
20
5
  const from = normalize(fromFile).split('/').filter(Boolean).slice(0, -1);
21
6
  const to = normalize(toModule)
@@ -1,36 +1,7 @@
1
- /**
2
- * R3F hierarchy reparent hardening — the PURE plan for "move this JSX element into
3
- * that one", covering R1 (scope safety), R2 (world-transform preservation) and R3
4
- * (destination legality) before a single byte is written.
5
- *
6
- * Why here: `plan-source-edit.ts` is the ONE place both tiers (dev server and hosted
7
- * browser) route a source edit through, and its `reparent` case used to be a bare
8
- * `reparentElement` — a textual line-block move that is sound only between two parents
9
- * in the same lexical scope with identical accumulated transforms. The moment the
10
- * hierarchy invites arbitrary drags it is unsound three ways, all of them SILENT:
11
- *
12
- * 1. Lexical capture — an element authored inside a component function may reference
13
- * that function's own bindings (`color={tint}`, a `ref`, a mapped variable).
14
- * Textually moving it elsewhere produces code that does not compile, or compiles
15
- * against a DIFFERENT binding with the same name.
16
- * 2. Transform re-basing — a verbatim move preserves the authored `position` tuple,
17
- * not the world position. Between identity parents those are the same thing;
18
- * between transformed parents the object teleports on the next mount.
19
- * 3. Boundary blindness — a self-closing destination (which is what nearly every R3F
20
- * component callsite is) made `reparentElement` insert a PRECEDING SIBLING instead
21
- * of a child, and a destination rendered N times would author the element once and
22
- * render it N times.
23
- *
24
- * The response to all three is the same and is the anti-shim rule: REFUSE, naming the
25
- * thing that made the move unsafe. This module never rewrites an expression, never
26
- * renames a binding, and never writes a computed value over an authored one — the
27
- * literal-vs-dynamic guard is as sacred here as at the gizmo.
28
- *
29
- * R4 (one transaction) falls out of living here: every rule is decided before the first
30
- * edit, a refusal returns the input string identity-equal, and an accepted move returns
31
- * ONE new whole-file string (tuple rewrites + slot opening + the move), which the
32
- * caller's existing whole-file snapshot inverse turns into a single undo step.
33
- */
1
+ /** Plan a JSX reparent after checking lexical capture, world-transform
2
+ * preservation, and destination legality. Unsupported moves return the input
3
+ * unchanged with a named refusal; accepted moves return one complete next-file
4
+ * source string for one history transaction. */
34
5
 
35
6
  import ts from 'typescript';
36
7
  import { parseAuthoringTsx } from './ts-ast';
@@ -53,12 +24,8 @@ const EPS = 1e-6;
53
24
 
54
25
  export interface ReparentOptions {
55
26
  readonly rebase?: ReparentRebase | undefined;
56
- /**
57
- * How many live objects the DESTINATION's oid resolves to. More than one means the
58
- * destination is authored once and rendered N times (a `.map`ped callsite, or a
59
- * component definition with N instances), so the moved element would be authored
60
- * once and rendered N times too — R3 refuses that with the count.
61
- */
27
+ /** Number of live objects sharing the destination OID. Multiple instances
28
+ * would repeat the moved child, so the planner refuses that destination. */
62
29
  readonly destinationInstances?: number | undefined;
63
30
  /**
64
31
  * The same count for the element being MOVED. More than one is the mirrored case and
@@ -72,7 +39,7 @@ export interface ReparentPlan extends StructEditResult {
72
39
  /** The named reason the move was refused. Present iff `changed` is false and the
73
40
  * input was left byte-identical because a rule said no. */
74
41
  readonly error?: string;
75
- /** Applied, but with something the author should know (R3's mirrored case). */
42
+ /** Applied with a warning for the author. */
76
43
  readonly warning?: string;
77
44
  }
78
45
 
@@ -115,9 +82,7 @@ function contains(outer: ts.Node, sf: ts.SourceFile, offset: number): boolean {
115
82
  return outer.getStart(sf) <= offset && offset < outer.getEnd();
116
83
  }
117
84
 
118
- // ------------------------------------------------------------- R1: scope safety
119
-
120
- /** Every name a declaration binds, flattened through destructuring patterns. */
85
+ // --- Scope safety ---
121
86
  function boundNames(name: ts.BindingName, into: Set<string>): void {
122
87
  if (ts.isIdentifier(name)) {
123
88
  into.add(name.text);
@@ -227,19 +192,9 @@ function lineOf(node: ts.Node, sf: ts.SourceFile): number {
227
192
  return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
228
193
  }
229
194
 
230
- /**
231
- * The identifiers the element's JSX subtree REFERENCES from outside itself — the R1
232
- * input. Same machinery as the contract analyzer (a plain `ts` walk over the TSX AST).
233
- *
234
- * It counts an identifier it is unsure about, so an unrecognized shape refuses rather
235
- * than moves. The one deliberate subtraction is names the subtree binds FOR ITSELF —
236
- * without it, a `.map((angle) => …)` inside the moved element would read as a capture
237
- * of the outer scope and nothing containing a mapped list could ever move. Stated
238
- * plainly, because it is not free: a name bound inside the subtree in one place and
239
- * captured from outside it in another is subtracted too, and that capture is missed.
240
- * A tighter answer needs a real binder, not a name set; this is the honest limit of
241
- * the cheap one.
242
- */
195
+ /** Find names referenced outside the subtree. Names bound anywhere inside
196
+ * it are subtracted; the name-set walk does not resolve shadowed captures.
197
+ * Unrecognized reference shapes count conservatively as captures. */
243
198
  export function referencedIdentifiers(node: JsxElementNode): Map<string, ts.Node> {
244
199
  const referenced = new Map<string, ts.Node>();
245
200
  const localBindings = new Set<string>();
@@ -295,7 +250,7 @@ function leftmostIdentifier(node: ts.PropertyAccessExpression): ts.Node {
295
250
  return cursor;
296
251
  }
297
252
 
298
- // -------------------------------------------------- R3: destination legality
253
+ // --- Destination legality ---
299
254
 
300
255
  type ChildrenSlot = 'paired' | 'expandable';
301
256
 
@@ -409,7 +364,7 @@ export function planReparent(
409
364
  return refuse(`<${destinationTag}> is inside <${elementTag}> — a move there would be a cycle.`);
410
365
  }
411
366
 
412
- // --- R3: destination legality ------------------------------------------------
367
+ // --- Destination legality ---
413
368
  const destinationInstances = options.destinationInstances;
414
369
  if (destinationInstances !== undefined && destinationInstances > 1) {
415
370
  return refuse(
@@ -433,7 +388,7 @@ export function planReparent(
433
388
  }
434
389
  }
435
390
 
436
- // --- R1: scope safety --------------------------------------------------------
391
+ // Keep the destination within the element's lexical scope.
437
392
  for (const [name, reference] of referencedIdentifiers(element)) {
438
393
  const scope = bindingScopeOf(name, reference);
439
394
  if (!scope || contains(scope, sf, parentStart)) continue;
@@ -443,7 +398,7 @@ export function planReparent(
443
398
  );
444
399
  }
445
400
 
446
- // --- R2: world-transform preservation ---------------------------------------
401
+ // Preserve the world transform across the new parent.
447
402
  const rebase = options.rebase;
448
403
  const tagEnd = findTagEnd(code, elementStart);
449
404
  if (tagEnd < 0) return refuse(`<${elementTag}>'s opening tag could not be read.`);
@@ -476,7 +431,7 @@ export function planReparent(
476
431
  });
477
432
  }
478
433
 
479
- // --- R4: everything above decided; only now does anything get written --------
434
+ // All checks passed; apply tuple rewrites and the element move.
480
435
  let next = code;
481
436
  for (const { channel, values, scalar } of pending) {
482
437
  const uniform =
@@ -1,22 +1,4 @@
1
- /**
2
- * The SOURCE-EDIT REQUEST — the one wire shape a source edit is stated in, and
3
- * deliberately its own file.
4
- *
5
- * It sits between two programs that must NOT share a type graph: the browser
6
- * editor (`ui-source/source-write-backend.ts`, which posts it) and the NODE
7
- * dev server (`vite-plugin-ui-oid.ts`'s middleware and `ui-source/plan-source-edit.ts`,
8
- * which plan and apply it). Until 2026-09-20 both sides imported the type from
9
- * `source-write-backend.ts` — and that module value-imports `files/project-files.ts`,
10
- * which reaches `storage/` and from there the editor SHELL STORE, so
11
- * `tsconfig.server.json` compiled 231 browser modules including 36 React ones to
12
- * typecheck a Node program (the fourth blind review's "the Node server's type graph
13
- * now reaching the browser app"; the config's own header recorded the 310 errors
14
- * that forced its `paths` and `jsx` settings).
15
- *
16
- * So: the REQUEST lives here, importing nothing but its two sibling AST types, and
17
- * `source-write-backend.ts` re-exports it for the browser callers that already name
18
- * it. Anything added to this file must stay free of browser and Node imports alike —
19
- * it is read by both.
20
- */
1
+ /** Shared source-edit wire contracts, used by browser authoring and the
2
+ * Node serving plugin without importing either implementation graph. */
21
3
 
22
4
  export type { PreparedSourceEdit, SourceEditRequest, StructReparentContext } from '@volter/sdk/source-authoring';
@@ -1,72 +1,10 @@
1
- /**
2
- * THE MECHANICAL TypeScript-AST HELPERS every `ui-source/` reader repeats.
3
- *
4
- * Not an abstraction over the TypeScript compiler API — each function here
5
- * hands back the compiler's OWN node and gets out of the way, and every caller
6
- * keeps programming against `ts.*`. What they remove is the retyping: the
7
- * eleven modules in this directory that read a TSX module each spelled the same
8
- * four or five little walks out again, and the copies had begun to disagree
9
- * about things that matter.
10
- *
11
- * What "matters" means, concretely — this is the drift these functions existed
12
- * in nine, three, three, two and two copies of, and what each copy got wrong:
13
- *
14
- * - {@link parseAuthoringTsx}: `ts.createSourceFile(file, code,
15
- * ScriptTarget.Latest, true, ScriptKind.TSX)` appeared at NINE sites, two of
16
- * them already wrapped in a local `parseTsx`/`sourceFile` whose only
17
- * difference was argument order. Both of the last two arguments are silent
18
- * if wrong: `setParentNodes: false` leaves `node.parent` undefined, which
19
- * breaks every upward walk here (including {@link enclosingScope}) at
20
- * runtime rather than at compile time, and a missing `ScriptKind.TSX`
21
- * parses JSX as type assertions.
22
- * - {@link hasModifier}: two copies, ALREADY drifted —
23
- * `plan-fork-component.ts`'s guarded with `ts.canHaveModifiers` and
24
- * `r3f-project-contracts.ts`'s cast the node to `ts.HasModifiers` and hoped.
25
- * The guarded form is the one kept.
26
- * - {@link enclosingScope}: three copies of one loop, returning
27
- * `FunctionLikeDeclaration` in two and `SignatureDeclaration` in the third
28
- * for the same four node kinds.
29
- * - {@link numericLiteral}: three copies (`numeric`, `numericLiteral`) of the
30
- * unary-sign rule — the part a fourth author would plausibly write without,
31
- * silently losing every negative literal.
32
- * - {@link jsxAttribute} / {@link refIdentifier}: two and three copies of
33
- * "find this attribute", one of the `refIdentifier`s returning `undefined`
34
- * where the others return `null`.
35
- *
36
- * Deliberately NOT here: anything whose two versions differ for a caller's own
37
- * reason. `plan-fork-component.ts`'s `jsxAtOffset` and `reparent-guard.ts`'s
38
- * `jsxElementAt` look like twins and are not — one wants the OPENING element to
39
- * read its tag, the other wants the whole `ts.JsxElement` to move it, and the
40
- * second prunes the walk by span while the first does not.
41
- */
1
+ /** Shared TypeScript AST helpers. Callers retain compiler nodes, parent
2
+ * pointers, and JSX syntax for their own source analyses. */
42
3
 
43
4
  import * as ts from 'typescript';
44
5
 
45
- /**
46
- * Parse one authored TSX/TS module the way every reader in this directory
47
- * needs it: newest syntax, parent pointers set, JSX enabled.
48
- *
49
- * `file` is the module's path — it is what the compiler reports in positions
50
- * and what `node.getText(sf)` reads against, so pass the real one where there
51
- * is one.
52
- */
53
- /**
54
- * ONE parse per (file, bytes) — the same source is parsed several times over
55
- * for one bundle otherwise.
56
- *
57
- * `ProjectContractResolver` calls this three times for a single file
58
- * (`importedForSource`, `typeAliasesForSource`, `propSpecsForSource`), the
59
- * stamping transform parses it again, and every one of those runs on EVERY
60
- * structural edit.
61
- * Measured on `examples/third-person`, the contract walk alone costs 380ms of
62
- * the ~1.1s an edit spends on CPU.
63
- *
64
- * Keyed on the BYTES, so it is sound in a way an analysis cache is not: a
65
- * parse is a pure function of its input, while what a file MEANS also depends
66
- * on the files it imports. Edited bytes miss the cache and re-parse; the entry
67
- * for the old bytes is dropped, so the map holds one node tree per live file
68
- * rather than one per revision.
69
- */
6
+ /** Parse TSX with current syntax and parent pointers. Cache one parse per
7
+ * file and exact source text; changed bytes replace the previous tree. */
70
8
  const parsedByFile = new Map<
71
9
  string,
72
10
  { readonly code: string; readonly sourceFile: ts.SourceFile }
@@ -86,14 +24,6 @@ export function parseAuthoringTsx(file: string, code: string): ts.SourceFile {
86
24
  return sourceFile;
87
25
  }
88
26
 
89
- /** Drop every cached parse — for a project switch, where the next project's
90
- * files may reuse these paths with different bytes. Bytes are compared on
91
- * every hit anyway, so this is hygiene against unbounded growth, not
92
- * correctness. */
93
- export function clearAuthoringParseCache(): void {
94
- parsedByFile.clear();
95
- }
96
-
97
27
  /** Whether `node` carries a given modifier (`export`, `default`, …). False for
98
28
  * a node kind that cannot carry modifiers at all, rather than throwing. */
99
29
  export function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
@@ -124,29 +54,9 @@ export function enclosingScope(node: ts.Node): ts.FunctionLikeDeclaration | null
124
54
  return null;
125
55
  }
126
56
 
127
- /**
128
- * The number an expression literally IS, or `undefined` when it is anything
129
- * computed. A leading `-`/`+` counts: `position={[-1, 0, 2]}` is authored
130
- * constantly and its first entry is a `PrefixUnaryExpression`, not a numeric
131
- * literal.
132
- */
133
- export function numericLiteral(expression: ts.Expression): number | undefined {
134
- if (ts.isNumericLiteral(expression)) return Number(expression.text);
135
- if (
136
- ts.isPrefixUnaryExpression(expression) &&
137
- (expression.operator === ts.SyntaxKind.MinusToken ||
138
- expression.operator === ts.SyntaxKind.PlusToken) &&
139
- ts.isNumericLiteral(expression.operand)
140
- ) {
141
- const value = Number(expression.operand.text);
142
- return expression.operator === ts.SyntaxKind.MinusToken ? -value : value;
143
- }
144
- return undefined;
145
- }
146
-
147
57
  /** The named attribute on a JSX opening element, or `undefined`. Spread
148
58
  * attributes are not attributes and are skipped. */
149
- export function jsxAttribute(
59
+ function jsxAttribute(
150
60
  element: ts.JsxOpeningLikeElement,
151
61
  name: string,
152
62
  ): ts.JsxAttribute | undefined {
@@ -1,35 +1,7 @@
1
- /**
2
- * "Does THIS project interpret utility classes?" — the evidence gate in front of
3
- * `writeStyleAuto`'s className route.
4
- *
5
- * Why it exists: the router used to pick the Tailwind arbitrary-value class
6
- * (`bg-[#059669]`) whenever an element merely HAD a literal `className`, with no
7
- * evidence anything would ever compile that class. On a plain-CSS project the
8
- * write lands in source and paints NOTHING — measured on the vendored pmndrs
9
- * racing-game (plain Vite + `src/styles.css`, no tailwind in its tree or ours):
10
- * the editor's visible result was only the adapter's optimistic inline patch, a
11
- * cold remount rendered `rgba(0, 0, 0, 0)`, and the game's own build has no
12
- * transformer for it either. Inert bytes in someone's source that tell a lie
13
- * about their game.
14
- *
15
- * The fix is the same observation-over-inference rule the OID dialect decision
16
- * follows: route on what was OBSERVED about the project, not on what the
17
- * element's shape suggests. A tier gathers evidence with whatever I/O it has
18
- * (`node:fs` on the dev server, `StorageBackend` in the hosted browser) and
19
- * hands the bytes here; the decision itself is pure and lives ONLY here, so the
20
- * two tiers cannot drift into different answers.
21
- *
22
- * **The probe is deliberately strict, and the asymmetry is why.** A false
23
- * NEGATIVE costs an inline `style={{…}}` write — a less idiomatic expression of
24
- * the same edit that paints correctly in every project, in and out of the
25
- * editor. A false POSITIVE costs a write that is a lie. So "a dependency is
26
- * listed" is not enough on its own: something must also show the pipeline is
27
- * actually wired (a config file, a postcss/vite/astro plugin mention, or a css
28
- * entry that imports the framework). A CDN/runtime interpreter in HTML stands
29
- * alone because it needs no build step at all.
30
- */
31
-
32
- /** The routing answer, plus what was actually observed to produce it. */
1
+ /** Determine whether project evidence proves that utility classes are compiled
2
+ * or interpreted. A declared dependency needs matching pipeline configuration;
3
+ * a CDN interpreter needs no build step. Unproven support routes style writes
4
+ * to inline CSS. The probe is pure; callers supply the files they can read. */
33
5
  export interface UtilityClassSupport {
34
6
  /** True only when the project was OBSERVED to interpret utility classes. */
35
7
  readonly supported: boolean;