@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.
- package/contributions/component-contracts.service.ts +1 -1
- package/dist-node/serving.mjs +14770 -1629
- package/package.json +20 -9
- package/serving/component-prop-worker.ts +3 -19
- package/serving/index.ts +20 -8
- package/serving/source-diagnostics.ts +311 -0
- package/serving/ui-oid-plugin.ts +154 -775
- package/src/source/component-prop-types.ts +6 -62
- package/src/source/drei-detailed-names.ts +24 -0
- package/src/source/oid-transform.ts +166 -376
- package/src/source/plan-csf-story.ts +3 -26
- package/src/source/plan-extract-component.ts +15 -60
- package/src/source/plan-fork-component.ts +25 -92
- package/src/source/plan-named-style.ts +3 -17
- package/src/source/plan-source-edit.ts +9 -40
- package/src/source/r3f-contract-resolver.ts +9 -103
- package/src/source/r3f-physics-binding.ts +28 -186
- package/src/source/r3f-project-contracts.ts +13 -103
- package/src/source/relative-import-specifier.ts +3 -18
- package/src/source/reparent-guard.ts +16 -61
- package/src/source/source-edit-request.ts +2 -20
- package/src/source/ts-ast.ts +5 -95
- package/src/source/utility-class-support.ts +4 -32
- package/src/source/writer.ts +74 -326
- package/src/source/r3f-diagnostic-index.ts +0 -165
- package/src/source/r3f-environment-binding.ts +0 -106
- package/src/source/r3f-joint-binding.ts +0 -228
- package/src/source/r3f-lod-binding.ts +0 -81
- package/src/source/r3f-particle-binding.ts +0 -213
- package/src/source/syntactic-prop-specs.ts +0 -296
|
@@ -1,29 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
|
163
|
-
* `newFile
|
|
164
|
-
* subtree
|
|
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
|
-
//
|
|
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: {
|
|
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 = {
|
|
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))
|
|
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
|
-
//
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
|
|
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
|
|
378
|
-
if (
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
598
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
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
|
-
*
|
|
3
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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-
|
|
149
|
+
return failed('cannot batch-edit across files');
|
|
175
150
|
}
|
|
176
|
-
//
|
|
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
|
-
//
|
|
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, {
|