@volter/editor-react 0.5.211 → 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,29 +1,6 @@
1
- /**
2
- * CSF STORY WRITES — the planner behind "save these args as a story",
3
- * rename, and delete (the design ledger's CSF-writing order: stories are
4
- * discovered, composed, framed and switched, and until this nothing WROTE
5
- * one except Extract Component's emitted file).
6
- *
7
- * Pure text surgery over the module source, in the repo's writer idiom
8
- * (`plan-extract-component.ts` is the emit-shape precedent): every refusal
9
- * is a named sentence, and nothing here parses with a full AST — a CSF
10
- * story export is a top-level `export const <Name>` with a braced
11
- * initializer, and balanced-brace scanning over that shape is the whole
12
- * grammar this file needs. Anything outside the shape refuses rather than
13
- * guessing.
14
- *
15
- * IT STAYS IN THE HOST, and the reason is the same one unit 6 recorded for the
16
- * harness and generation estates: its caller is the SERVER tier
17
- * (`vite-plugin-ui-oid.ts`'s `POST /__ui-source/csf-story`), and the
18
- * contribution points are page-side only — there is no server-side `.service`
19
- * or route point for a package to register a handler into. It was moved out
20
- * once and came straight back when the probe refused to boot: a grep over
21
- * `src` and `server` had reported zero importers, and the one importer is a
22
- * file at the package ROOT. (CLAUDE.md: never conclude "dead code" from a
23
- * search that returned zero.)
24
- */
25
-
26
- /** One planned edit: the whole next source, or the refusal. */
1
+ /** Plan saving, renaming, and deleting CSF story exports. Source is supplied
2
+ * by the route; balanced-brace scanning handles top-level exported constants
3
+ * with braced initializers. Unsupported forms return named refusals. */
27
4
  export type CsfStoryPlan =
28
5
  | { ok: true; nextSource: string; summary: string }
29
6
  | { ok: false; reason: string };
@@ -1,48 +1,8 @@
1
- /**
2
- * `planComponentExtraction` — the PURE core of "Extract Component…": one
3
- * already-read source string in, THREE exact artifacts out — a new component
4
- * module, a portable CSF story beside it, and the callsite edit that replaces
5
- * the extracted subtree with an instance. No I/O, no hashing, no transport —
6
- * the same split `plan-fork-component.ts` uses, and for the same reason (the
7
- * dev server reads with `node:fs`; this module stays dependency-free so it is
8
- * probeable headlessly).
9
- *
10
- * WHAT AN EXTRACTION IS. The inverse of pasting inline: a subtree an author
11
- * assembled in place becomes a reusable component under `src/prefabs/` — the
12
- * scene/component/prefab taxonomy the template teaches — with a story so the
13
- * component board can mount it in isolation, and the original site becomes an
14
- * ordinary instance. There is deliberately NO parallel prefab format: the
15
- * output is a plain component file plus plain portable CSF, both of which a
16
- * developer could have written by hand (WORK.md's own constraint).
17
- *
18
- * IMPORTS TRAVEL; EVERYTHING ELSE REFUSES BY NAME. The subtree's free
19
- * identifiers (`referencedIdentifiers`, the reparent guard's R1 machinery)
20
- * are classified by where they bind:
21
- *
22
- * - an IMPORT binding travels — the new module gets its own import
23
- * statement, with relative specifiers re-resolved from the new file's
24
- * location;
25
- * - a module-level declaration (a `const`/function in the world file)
26
- * REFUSES, naming the declaration and its line — chasing and copying
27
- * module locals is scope creep this planner deliberately does not have
28
- * (the fork records the same boundary), and a silent copy would fork the
29
- * value's identity;
30
- * - a binding inside an enclosing function (a hook result, a mapped
31
- * variable, a prop) REFUSES with the owning function's name — the same
32
- * sentence shape R1 gives a reparent that would break lexical capture;
33
- * - an unresolved name (a global — `Math`) is fine as-is.
34
- *
35
- * INSERTION SEMANTICS. When the extracted root is an INTRINSIC element
36
- * (`<mesh>`, `<group>`), its `name`/`position`/`rotation`/`scale` attributes
37
- * are LIFTED to the new callsite and the component's root spreads
38
- * `{...props}` (typed `ThreeElements['<tag>']`, the template HeroBox's own
39
- * shape) — placement belongs to the instance, so a second drop of the same
40
- * prefab does not land inside the first. A component-tag root is carried
41
- * verbatim with no lift and no props parameter: its prop surface is its own.
42
- *
43
- * REFUSALS ARE NAMED — every rejection returns a sentence the author can act
44
- * on, and the planner refuses before anything is written.
45
- */
1
+ /** Plan extraction into a component module, a portable CSF story, and a
2
+ * replacement callsite. Imported bindings travel with the subtree; references
3
+ * to module or enclosing-function locals return named refusals. Intrinsic
4
+ * roots lift placement props to the callsite and spread them on the new root.
5
+ * All edits are computed from supplied bytes before the caller writes files. */
46
6
 
47
7
  import ts from 'typescript';
48
8
  import { lineColToOffset } from './oid-transform';
@@ -72,14 +32,11 @@ export interface ExtractComponentRequest {
72
32
  /** Project-relative directory the new files land in (`src/prefabs`). */
73
33
  readonly prefabDir: string;
74
34
  /** File NAMES already in that directory — collision refusals + name fallback. */
75
- readonly siblingFiles?: readonly string[] | undefined;
35
+ readonly siblingFiles: readonly string[];
76
36
  }
77
37
 
78
38
  export interface ExtractComponentPlan {
79
39
  readonly newName: string;
80
- /** `Lantern.tsx` / `Lantern.stories.tsx` — caller joins onto `prefabDir`. */
81
- readonly componentFileName: string;
82
- readonly storyFileName: string;
83
40
  /** Project-relative paths, forward slashes. */
84
41
  readonly componentFile: string;
85
42
  readonly storyFile: string;
@@ -159,9 +116,9 @@ function pickName(
159
116
 
160
117
  // ------------------------------------------------------------ import carrying
161
118
 
162
- /** The import statement binding `name` in `sf`, rebuilt for a module living at
163
- * `newFile` — or `null` when `name` is not import-bound. Only the bindings the
164
- * subtree actually uses are carried, so an import of five names carries one. */
119
+ /** The specifier and binding kind for `name` in `sf`, resolved for a module at
120
+ * `newFile`, or `null` when `name` is not import-bound. The caller carries only
121
+ * bindings the subtree uses. */
165
122
  function carriedImportFor(
166
123
  sf: ts.SourceFile,
167
124
  name: string,
@@ -227,7 +184,7 @@ export function planComponentExtraction(
227
184
  const opening = openingTagOf(element);
228
185
  const tag = opening.tagName.getText(sf);
229
186
 
230
- // R1 over the whole subtree: classify every free identifier.
187
+ // Classify every free identifier in the subtree before producing edits.
231
188
  const carried = new Map<
232
189
  string,
233
190
  { specifier: string; piece: 'default' | 'namespace' | 'named' }
@@ -261,7 +218,7 @@ export function planComponentExtraction(
261
218
  }
262
219
 
263
220
  // Name + collision.
264
- const siblings = request.siblingFiles ?? [];
221
+ const siblings = request.siblingFiles;
265
222
  const attrs = opening.attributes.properties;
266
223
  const authoredName = attrs.find(
267
224
  (p): p is ts.JsxAttribute =>
@@ -296,7 +253,7 @@ export function planComponentExtraction(
296
253
  // Root-attribute lift (intrinsic roots only): placement belongs to the instance.
297
254
  const intrinsicRoot = /^[a-z]/.test(tag);
298
255
  const lifted: string[] = [];
299
- let rootTagWithoutLifted: { start: number; end: number; text: string } | null = null;
256
+ let rootTagWithoutLifted: { end: number; text: string } | null = null;
300
257
  if (intrinsicRoot) {
301
258
  const removals: Array<{ start: number; end: number }> = [];
302
259
  for (const property of attrs) {
@@ -324,14 +281,14 @@ export function planComponentExtraction(
324
281
  closer,
325
282
  ts.isJsxSelfClosingElement(opening) ? ' {...props} />' : ' {...props}>',
326
283
  );
327
- rootTagWithoutLifted = { start: tagStart, end: tagEnd, text };
284
+ rootTagWithoutLifted = { end: tagEnd, text };
328
285
  }
329
286
 
330
287
  // The component module.
331
288
  const elementStart = element.getStart(sf);
332
289
  const elementEnd = element.getEnd();
333
290
  const lineStart = source.lastIndexOf('\n', elementStart - 1) + 1;
334
- const indent = /^[ \t]*/.exec(source.slice(lineStart, elementStart))?.[0] ?? '';
291
+ const indent = /^[ \t]*/.exec(source.slice(lineStart, elementStart))![0];
335
292
  let subtree = source.slice(elementStart, elementEnd);
336
293
  if (rootTagWithoutLifted) {
337
294
  subtree = rootTagWithoutLifted.text + source.slice(rootTagWithoutLifted.end, elementEnd);
@@ -370,7 +327,7 @@ ${body}
370
327
  }
371
328
  `;
372
329
 
373
- // The story (the template HeroBox story, transcribed).
330
+ // A portable story for the extracted component.
374
331
  const title = newName.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
375
332
  const storySource = `import type { Meta, StoryObj } from '@storybook/react';
376
333
  import { ${newName} } from './${newName}';
@@ -404,8 +361,6 @@ export const Default: Story = {
404
361
  ok: true,
405
362
  plan: {
406
363
  newName,
407
- componentFileName,
408
- storyFileName,
409
364
  componentFile,
410
365
  storyFile,
411
366
  componentSource: componentSourceFinal,
@@ -1,36 +1,7 @@
1
- /**
2
- * `planComponentFork` — the PURE core of H7 "Fork Component…": two already-read
3
- * source strings in, two exact edits out. No I/O, no hashing, no transport — the
4
- * same split `plan-source-edit.ts` uses, and for the same reason (the dev server
5
- * reads with `node:fs`; this module stays dependency-free so the plan is
6
- * readable on its own).
7
- *
8
- * WHAT A FORK IS. H7's translation of Unity's Unpack into a source world:
9
- * customizing ONE instance beyond the component's prop surface means COPYING the
10
- * component and retargeting that callsite — never flattening the definition's
11
- * JSX into the parent (a refactor no React developer performs, and the thing
12
- * H7 rejects permanently). Mechanically: one new file plus one changed tag.
13
- *
14
- * WHY THE WHOLE MODULE IS COPIED. The copy has to COMPILE. A definition's
15
- * dependencies are module-level: imports, helper functions, constants, types,
16
- * the props interface. Copying the module wholesale carries all of them, and —
17
- * because the copy lands as a SIBLING of the original — every relative import
18
- * specifier inside it still resolves unchanged. Selective extraction (copy the
19
- * function, chase what it references) is the scope-analysis machinery the
20
- * reparent spec owns; this action deliberately does not have it. The price is
21
- * that non-component exports are duplicated across the two modules. That is
22
- * accepted for v1: nothing imports the new module except the one retargeted
23
- * callsite, so a duplicate export is inert.
24
- *
25
- * WHY IT IS NOT "for this instance". The menu item is `Fork Component…`, not
26
- * "Fork for this instance": a callsite that lives inside a component rendered
27
- * many times retargets EVERY render of that callsite. One callsite is the unit
28
- * of this edit, and one callsite is not always one on-screen object.
29
- *
30
- * REFUSALS ARE NAMED. Every rejection returns a sentence the author can act on
31
- * (doctrine rule 2 — "refusal is visible and named"), and the planner refuses
32
- * BEFORE anything is written; the caller writes only a plan it has in hand.
33
- */
1
+ /** Plan a component fork from definition and callsite source, without I/O.
2
+ * Copy the complete module to a sibling so relative imports keep resolving,
3
+ * rename the component, and retarget the callsite. A shared callsite changes
4
+ * every instance it renders. Return named refusals before any file is written. */
34
5
 
35
6
  import ts from 'typescript';
36
7
  import { lineColToOffset } from './oid-transform';
@@ -60,14 +31,8 @@ export interface ForkComponentRequest {
60
31
  readonly tag: string;
61
32
  /** The instance's `name` prop — the preferred seed for the fork's name. */
62
33
  readonly nameSeed?: string | undefined;
63
- /**
64
- * File NAMES (not paths) already in the definition file's directory. Two
65
- * jobs: the cheap "is this export name taken" probe H7 asks for (a module's
66
- * basename is a good enough proxy for the component it exports, and this
67
- * planner is not allowed to read the directory itself), and the hard
68
- * file-collision refusal.
69
- */
70
- readonly siblingFiles?: readonly string[] | undefined;
34
+ /** Sibling filenames used for component-name and file-collision checks. */
35
+ readonly siblingFiles: readonly string[];
71
36
  }
72
37
 
73
38
  export interface ForkComponentPlan {
@@ -75,8 +40,6 @@ export interface ForkComponentPlan {
75
40
  readonly newName: string;
76
41
  /** `EnemyBravo.tsx` — the caller joins it onto the definition's directory. */
77
42
  readonly newFileName: string;
78
- /** The new module's path, normalized to forward slashes. */
79
- readonly newFile: string;
80
43
  /** The complete bytes of the new module (the copy, renamed). */
81
44
  readonly newFileSource: string;
82
45
  readonly callsiteFile: string;
@@ -127,7 +90,7 @@ interface ComponentDeclaration {
127
90
 
128
91
  /**
129
92
  * The component's own declaration in its module — a function declaration or a
130
- * `const` bound to an arrow/function expression, the same two shapes
93
+ * variable bound to an arrow/function expression, the same two shapes
131
94
  * `oid-transform.ts`'s `componentDefinitions` recognizes as a component. A
132
95
  * `memo(...)`/`forwardRef(...)` wrapper is deliberately NOT matched: the fork
133
96
  * would have to guess where the name lives, and a named refusal beats a guess.
@@ -299,7 +262,7 @@ function defaultExportName(sf: ts.SourceFile): string | null {
299
262
 
300
263
  /** `patrol guard 2` → `PatrolGuard2`; `""`/`"2fast"` → `null` (not an
301
264
  * identifier, so the caller falls back to `<Tag>Fork`). */
302
- export function pascalCaseIdentifier(seed: string | undefined): string | null {
265
+ function pascalCaseIdentifier(seed: string | undefined): string | null {
303
266
  if (!seed) return null;
304
267
  const parts = seed.split(/[^A-Za-z0-9]+/).filter(Boolean);
305
268
  if (parts.length === 0) return null;
@@ -314,7 +277,7 @@ export function pascalCaseIdentifier(seed: string | undefined): string | null {
314
277
  * otherwise `<Tag>Fork`, `<Tag>Fork2`, … `null` when even the fallback series
315
278
  * is exhausted (99 forks of one component in one directory).
316
279
  */
317
- export function pickForkName(
280
+ function pickForkName(
318
281
  tag: string,
319
282
  seed: string | undefined,
320
283
  taken: ReadonlySet<string>,
@@ -344,27 +307,11 @@ function applyReplacements(source: string, replacements: readonly Replacement[])
344
307
  return out;
345
308
  }
346
309
 
347
- /**
348
- * Rename every reference to `name` in this module — AST-driven, never a regex.
349
- *
350
- * Renamed: the declaration, ordinary references, self-recursive JSX usage
351
- * (`<Enemy>` inside `Enemy`'s own body), `export { Enemy }`, `export default
352
- * Enemy`, type positions such as `typeof Enemy`. NOT renamed: anything that
353
- * merely SPELLS the same text in a non-reference position — a property access
354
- * (`registry.Enemy`), an object-literal key, a JSX attribute name, a
355
- * destructuring property name, a member of a type. The props type
356
- * (`EnemyProps`) is a different identifier and is deliberately untouched: H7
357
- * renames the component, nothing else.
358
- *
359
- * A shorthand property (`{ Enemy }`) is EXPANDED to `{ Enemy: EnemyBravo }`
360
- * rather than renamed, because renaming it silently changes the key.
361
- *
362
- * Known v1 limit, recorded rather than hidden: an inner scope that re-declares
363
- * the same name would have its references renamed too. Detecting that needs a
364
- * scope walk this action does not carry, and a component module shadowing its
365
- * own component name is not a shape we have seen.
366
- */
367
- export function renameComponentInModule(
310
+ /** Rename component references throughout the module, including JSX, exports
311
+ * and type positions. Property names remain unchanged; shorthand properties
312
+ * expand to preserve their keys. References shadowed by an inner declaration
313
+ * are also renamed because this walk does not resolve lexical bindings. */
314
+ function renameComponentInModule(
368
315
  file: string,
369
316
  source: string,
370
317
  name: string,
@@ -374,8 +321,8 @@ export function renameComponentInModule(
374
321
  const replacements: Replacement[] = [];
375
322
  const visit = (node: ts.Node): void => {
376
323
  if (ts.isIdentifier(node) && node.text === name) {
377
- const parent = node.parent as ts.Node | undefined;
378
- if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
324
+ const parent = node.parent;
325
+ if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
379
326
  replacements.push({
380
327
  start: node.getStart(sf),
381
328
  end: node.getEnd(),
@@ -403,12 +350,11 @@ const NAME_IS_A_KEY_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
403
350
  ts.SyntaxKind.EnumMember,
404
351
  ]);
405
352
 
406
- function isReferencePosition(node: ts.Identifier, parent: ts.Node | undefined): boolean {
407
- if (!parent) return true;
353
+ function isReferencePosition(node: ts.Identifier, parent: ts.Node): boolean {
408
354
  if (ts.isQualifiedName(parent)) return parent.right !== node;
409
355
  if (ts.isBindingElement(parent)) return parent.propertyName !== node;
410
356
  if (!NAME_IS_A_KEY_KINDS.has(parent.kind)) return true;
411
- return (parent as { name?: ts.Node }).name !== node;
357
+ return (parent as ts.Node & { readonly name: ts.Node }).name !== node;
412
358
  }
413
359
 
414
360
  // ------------------------------------------------------------ callsite edit
@@ -416,7 +362,6 @@ function isReferencePosition(node: ts.Identifier, parent: ts.Node | undefined):
416
362
  interface ImportStyle {
417
363
  readonly quote: string;
418
364
  readonly semi: boolean;
419
- readonly extension: string;
420
365
  /** Offset the new import statement is inserted at, and what follows it. */
421
366
  readonly insertAt: number;
422
367
  readonly leading: string;
@@ -430,26 +375,17 @@ function importStyleOf(sf: ts.SourceFile, source: string): ImportStyle {
430
375
  const last = imports[imports.length - 1];
431
376
  const first = imports[0];
432
377
  let quote = "'";
433
- let extension = '';
434
378
  if (first) {
435
379
  const specifierStart = first.moduleSpecifier.getStart(sf);
436
380
  if (source[specifierStart] === '"') quote = '"';
437
381
  }
438
- for (const declaration of imports) {
439
- if (!ts.isStringLiteral(declaration.moduleSpecifier)) continue;
440
- const text = declaration.moduleSpecifier.text;
441
- if (!text.startsWith('.')) continue;
442
- const match = SOURCE_EXTENSION_RE.exec(text);
443
- if (match) extension = match[0];
444
- break;
445
- }
446
382
  const semi = last ? source.slice(0, last.getEnd()).trimEnd().endsWith(';') : true;
447
383
  if (last) {
448
- return { quote, semi, extension, insertAt: last.getEnd(), leading: '\n', trailing: '' };
384
+ return { quote, semi, insertAt: last.getEnd(), leading: '\n', trailing: '' };
449
385
  }
450
386
  // No imports at all: the file's very top is the only position that cannot
451
387
  // detach a doc comment from the statement it documents.
452
- return { quote, semi, extension, insertAt: 0, leading: '', trailing: '\n' };
388
+ return { quote, semi, insertAt: 0, leading: '', trailing: '\n' };
453
389
  }
454
390
 
455
391
  function jsxAtOffset(
@@ -531,7 +467,7 @@ export function planComponentFork(request: ForkComponentRequest): ForkComponentP
531
467
  if (!declaredName || !declaration) {
532
468
  return refuse(
533
469
  `Could not find the declaration of <${tag}> in ${definitionFile}. Fork copies a component ` +
534
- 'written as a function declaration or a const arrow function; this one is neither.',
470
+ 'written as a function declaration or a variable initialized with a function; this one is neither.',
535
471
  );
536
472
  }
537
473
  const exportForm = exportFormOf(definitionSf, declaredName, declaration);
@@ -541,13 +477,11 @@ export function planComponentFork(request: ForkComponentRequest): ForkComponentP
541
477
  );
542
478
  }
543
479
 
544
- const siblings = request.siblingFiles ?? [];
480
+ const siblings = request.siblingFiles;
545
481
  const taken = new Set<string>([
546
482
  ...topLevelNames(callsiteSf),
547
483
  ...topLevelNames(definitionSf),
548
- // H7's "any export of the target directory's modules you can cheaply
549
- // check": a module's basename is the proxy — this planner may not read a
550
- // directory, and reading every sibling's exports is not cheap.
484
+ // Prefer a component name that does not collide with a sibling module.
551
485
  ...siblings.map((name) => name.replace(SOURCE_EXTENSION_RE, '')),
552
486
  tag,
553
487
  declaredName,
@@ -594,8 +528,8 @@ export function planComponentFork(request: ForkComponentRequest): ForkComponentP
594
528
  text: `${style.leading}${importText}${style.trailing}`,
595
529
  },
596
530
  ];
597
- const parent = opening.parent as ts.Node | undefined;
598
- if (parent && ts.isJsxElement(parent) && parent.openingElement === opening) {
531
+ if (ts.isJsxOpeningElement(opening)) {
532
+ const parent = opening.parent;
599
533
  replacements.push({
600
534
  start: parent.closingElement.tagName.getStart(callsiteSf),
601
535
  end: parent.closingElement.tagName.getEnd(),
@@ -608,7 +542,6 @@ export function planComponentFork(request: ForkComponentRequest): ForkComponentP
608
542
  plan: {
609
543
  newName,
610
544
  newFileName,
611
- newFile,
612
545
  newFileSource,
613
546
  callsiteFile,
614
547
  callsitePrevSource: callsiteSource,
@@ -1,20 +1,6 @@
1
- /**
2
- * NAMED STYLES — the planner behind "create a style from this selection"
3
- * (the design ledger's named-styles order; Webflow is the prior art). A
4
- * named style IS a CSS class in the project's own stylesheet — the web's
5
- * answer to a Figma style, and strictly stronger: it carries any property,
6
- * cascades, and responds to media/state. Until this, the editor could only
7
- * write inline styles and detected utility classes; nothing could mint a
8
- * reusable, project-owned class.
9
- *
10
- * Pure text surgery over the CSS source, in the writer idiom
11
- * (`plan-csf-story.ts` is the shape precedent): every refusal is a named
12
- * sentence, and the only grammar this file needs is "a class rule is
13
- * `.name { declarations }` appended at top level" — the dev server's
14
- * `@scope` wrapping happens at SERVE time, so source rules stay unwrapped.
15
- */
16
-
17
- /** One planned edit: the whole next CSS source, or the refusal. */
1
+ /** Plan a named CSS class from literal declarations. Rules append at the
2
+ * stylesheet top level; the dev server adds preview scoping at serve time.
3
+ * Return a named refusal for conflicting rules or unsupported declarations. */
18
4
  export type NamedStylePlan =
19
5
  | { ok: true; nextSource: string; summary: string }
20
6
  | { ok: false; reason: string };
@@ -1,20 +1,6 @@
1
- /**
2
- * `planSourceEdit` — the PURE core of a source edit: request + already-read file
3
- * bytes in, exact before/after bytes out. No I/O, no hashing, no transport.
4
- *
5
- * Why it exists: the same edit vocabulary (`SourceEditRequest`) has to be
6
- * executed by the dev server (`vite-plugin-ui-oid.ts`'s `handlePrepare`),
7
- * which reads with `node:fs` and hashes with `node:crypto`. The plan itself is
8
- * environment-free on purpose: resolving an oid to a source offset,
9
- * picking the writer function, deciding what `changed` means — is identical,
10
- * and a second hand-written copy of that switch is exactly the kind of fork
11
- * this repo has been bitten by. So the switch lives here once, over an
12
- * injected `sources` map the caller has already populated however it can.
13
- *
14
- * The caller learns WHICH files to read first from {@link sourceEditFiles},
15
- * which resolves the request's oids through the same `OidEntry` index without
16
- * touching any bytes.
17
- */
1
+ /** Plan one source edit from a request and already-read file bytes. The dev
2
+ * server owns I/O and hashing; this module resolves OIDs and runs the shared
3
+ * writer operations. sourceEditFiles identifies the files the caller must read. */
18
4
 
19
5
  import { ensureValueImport } from './ensure-import';
20
6
  import { lineColToOffset, type OidEntry } from './oid-transform';
@@ -27,7 +13,6 @@ import {
27
13
  deleteElements,
28
14
  duplicateElement,
29
15
  editTextContent,
30
- getEditableText,
31
16
  groupSiblingElements,
32
17
  insertChildElement,
33
18
  insertSiblingElement,
@@ -77,16 +62,8 @@ function planned(
77
62
  return { file, changed, prevSource, newSource, result: { ...result, changed } };
78
63
  }
79
64
 
80
- /**
81
- * D-A3: a `value: null` style request is the REMOVAL sentinel
82
- * routed to `removeInlineStyle`; anything else takes the normal
83
- * write-or-append path. Kept here (rather than in the vite plugin, where it
84
- * used to live alone) because BOTH tiers route the same sentinel.
85
- *
86
- * `support` is the caller's OBSERVATION about the target project's utility-class
87
- * pipeline — the class-vs-inline gate (`utility-class-support.ts`). Omitting it
88
- * means "unproven", which routes inline: the write that paints in every project.
89
- */
65
+ /** Route null style values to removal and other values to write-or-append.
66
+ * Utility-class support must be proved by the caller; unproven support writes inline. */
90
67
  export function applyStyleWriteRequest(
91
68
  code: string,
92
69
  elementStart: number,
@@ -150,9 +127,7 @@ export function planSourceEdit(
150
127
  if (request.kind === 'css') {
151
128
  const cleanFile = request.file.split('?')[0] ?? request.file;
152
129
  const src = sourceOf(cleanFile);
153
- // `media` scopes the edit to a breakpoint's `@media` block (design
154
- // ledger: breakpoints) — block and rule are minted on demand, so the
155
- // base path's generated-CSS probe does not apply.
130
+ // A media-scoped write creates the block and rule when absent.
156
131
  const edited =
157
132
  typeof request.media === 'string' && request.media.trim().length > 0
158
133
  ? surgicalCssEditInMedia(src, request.media, request.selector, request.prop, request.value)
@@ -171,10 +146,9 @@ export function planSourceEdit(
171
146
  if (missing) return failed(`unknown oid: ${missing.oid}`);
172
147
  const file = entries[0]!.entry!.file;
173
148
  if (entries.some((item) => item.entry!.file !== file)) {
174
- return failed('cannot batch-delete across files');
149
+ return failed('cannot batch-edit across files');
175
150
  }
176
- // ONE snapshot every offset resolves against — see `handleStructMany`'s
177
- // doc comment for why a per-oid re-read would be unsound here.
151
+ // Resolve every offset against the same source snapshot.
178
152
  const src = sourceOf(file);
179
153
  const offsets = entries.map((item) => lineColToOffset(src, item.entry!.line, item.entry!.col));
180
154
  const result =
@@ -220,7 +194,6 @@ export function planSourceEdit(
220
194
  changed: result.changed,
221
195
  dynamic: result.dynamic,
222
196
  route: result.route,
223
- appended: result.appended,
224
197
  // The `var()` guard's own channel — a refusal the caller must be able to
225
198
  // tell from a dynamic expression, because its sentence names a token.
226
199
  ...(result.tokenRef ? { tokenRef: result.tokenRef } : {}),
@@ -229,12 +202,10 @@ export function planSourceEdit(
229
202
  }
230
203
 
231
204
  if (request.kind === 'text') {
232
- const prevText = getEditableText(src, off);
233
205
  const result = editTextContent(src, off, request.text);
234
206
  return planned(entry.file, src, result.code, {
235
207
  changed: result.changed,
236
208
  dynamic: result.dynamic,
237
- prevText,
238
209
  });
239
210
  }
240
211
 
@@ -272,9 +243,7 @@ export function planSourceEdit(
272
243
  if (target === 'cross-file' || parent === 'cross-file') {
273
244
  return planned(entry.file, src, src, { changed: false, error: 'cannot move across files' });
274
245
  }
275
- // `reparent` is the ONE struct op with rules of its own (R1–R4): it is planned
276
- // whole — scope safety, destination legality, world-transform re-basing — before
277
- // any byte is written, and a refusal returns `src` unchanged with a named reason.
246
+ // Plan reparenting with scope, transform, and destination checks.
278
247
  if (request.op === 'reparent') {
279
248
  if (parent == null) {
280
249
  return planned(entry.file, src, src, {