@upstart.gg/vite-plugins 0.1.29 → 0.1.31
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/upstart-editor-api.d.ts +13 -1
- package/dist/upstart-editor-api.d.ts.map +1 -1
- package/dist/upstart-editor-api.js +59 -1
- package/dist/upstart-editor-api.js.map +1 -1
- package/dist/vite-plugin-upstart-attrs.d.ts +12 -7
- package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-attrs.js +195 -3
- package/dist/vite-plugin-upstart-attrs.js.map +1 -1
- package/dist/vite-plugin-upstart-branding/plugin.d.ts +3 -3
- package/dist/vite-plugin-upstart-branding/plugin.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-branding/plugin.js.map +1 -1
- package/dist/vite-plugin-upstart-branding/runtime.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/plugin.d.ts +3 -3
- package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/plugin.js +4 -1
- package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +27 -16
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.js +14 -2
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
- package/dist/{src/vite-plugin-upstart-editor → vite-plugin-upstart-editor}/runtime/state.d.ts +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/state.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/state.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +212 -28
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +16 -3
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/utils.js.map +1 -1
- package/dist/vite-plugin-upstart-theme.d.ts +3 -3
- package/dist/vite-plugin-upstart-theme.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-theme.js +2 -2
- package/dist/vite-plugin-upstart-theme.js.map +1 -1
- package/package.json +7 -7
- package/src/tests/vite-plugin-upstart-attrs.test.ts +298 -37
- package/src/upstart-editor-api.ts +71 -0
- package/src/vite-plugin-upstart-attrs.ts +293 -5
- package/src/vite-plugin-upstart-editor/plugin.ts +11 -1
- package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +35 -21
- package/src/vite-plugin-upstart-editor/runtime/index.ts +21 -1
- package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +260 -41
- package/src/vite-plugin-upstart-editor/runtime/types.ts +17 -4
- package/src/vite-plugin-upstart-theme.ts +4 -1
- package/dist/src/vite-plugin-upstart-editor/runtime/state.d.ts.map +0 -1
- package/dist/src/vite-plugin-upstart-editor/runtime/state.js.map +0 -1
- /package/dist/{src/vite-plugin-upstart-editor → vite-plugin-upstart-editor}/runtime/state.js +0 -0
|
@@ -10,6 +10,12 @@ declare const payloadEditText: z.ZodObject<{
|
|
|
10
10
|
content: z.ZodString;
|
|
11
11
|
}, z.core.$strip>;
|
|
12
12
|
type PayloadEditText = z.infer<typeof payloadEditText>;
|
|
13
|
+
declare const payloadEditTextDirect: z.ZodObject<{
|
|
14
|
+
action: z.ZodLiteral<"editTextDirect">;
|
|
15
|
+
id: z.ZodString;
|
|
16
|
+
content: z.ZodString;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
type PayloadEditTextDirect = z.infer<typeof payloadEditTextDirect>;
|
|
13
19
|
declare const payloadEditClassName: z.ZodObject<{
|
|
14
20
|
action: z.ZodLiteral<"editClassName">;
|
|
15
21
|
id: z.ZodString;
|
|
@@ -53,11 +59,17 @@ declare class UpstartEditorAPI {
|
|
|
53
59
|
* Only updates existing keys — returns an error if the key is not found.
|
|
54
60
|
*/
|
|
55
61
|
editText(params: PayloadEditText): Promise<EditResult>;
|
|
62
|
+
/**
|
|
63
|
+
* Edit a plain-text or rich-text JSX node directly in the source TSX file.
|
|
64
|
+
* For rich-text entries, `content` should be the inner JSX/HTML of the element's children.
|
|
65
|
+
*/
|
|
66
|
+
editTextDirect(params: PayloadEditTextDirect): Promise<EditResult>;
|
|
56
67
|
/**
|
|
57
68
|
* Edit the className of an element
|
|
58
69
|
*/
|
|
59
70
|
editClassName(params: PayloadEditClassName): Promise<EditResult>;
|
|
60
71
|
private applyEdit;
|
|
72
|
+
private applyMixedTextEdit;
|
|
61
73
|
/**
|
|
62
74
|
* Get element info by ID
|
|
63
75
|
*/
|
|
@@ -72,5 +84,5 @@ declare class UpstartEditorAPI {
|
|
|
72
84
|
getElementsByFile(file: string): Record<string, EditableEntry>;
|
|
73
85
|
}
|
|
74
86
|
//#endregion
|
|
75
|
-
export { EditResult, EditableRegistry, PayloadEditClassName, PayloadEditText, UpstartEditorAPI, payloadEditClassName, payloadEditText };
|
|
87
|
+
export { EditResult, EditableRegistry, PayloadEditClassName, PayloadEditText, PayloadEditTextDirect, UpstartEditorAPI, payloadEditClassName, payloadEditText, payloadEditTextDirect };
|
|
76
88
|
//# sourceMappingURL=upstart-editor-api.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upstart-editor-api.d.ts","names":[],"sources":["../src/upstart-editor-api.ts"],"mappings":";;;;cAMa,eAAA,EAAe,CAAA,CAAA,SAAA;;;;;;;KAWhB,eAAA,GAAkB,CAAA,CAAE,KAAA,QAAa,eAAA;AAAA,cAEhC,oBAAA,EAAoB,CAAA,CAAA,SAAA;;;;;KAMrB,oBAAA,GAAuB,CAAA,CAAE,KAAA,QAAa,oBAAA;AAAA,UAEjC,gBAAA;EACf,OAAA;EACA,WAAA;EACA,QAAA,EAAU,MAAA,SAAe,aAAA;AAAA;AAAA,KAGf,UAAA;EAEN,OAAA;EACA,KAAA;EACA,QAAA;AAAA;EAGA,OAAA;EACA,KAAA;EACA,QAAA;AAAA;AAAA,cAGO,gBAAA;EAAA,QACH,QAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;EAER,WAAA,CAAY,WAAA,UAAqB,YAAA
|
|
1
|
+
{"version":3,"file":"upstart-editor-api.d.ts","names":[],"sources":["../src/upstart-editor-api.ts"],"mappings":";;;;cAMa,eAAA,EAAe,CAAA,CAAA,SAAA;;;;;;;KAWhB,eAAA,GAAkB,CAAA,CAAE,KAAA,QAAa,eAAA;AAAA,cAEhC,qBAAA,EAAqB,CAAA,CAAA,SAAA;;;;;KAMtB,qBAAA,GAAwB,CAAA,CAAE,KAAA,QAAa,qBAAA;AAAA,cAEtC,oBAAA,EAAoB,CAAA,CAAA,SAAA;;;;;KAMrB,oBAAA,GAAuB,CAAA,CAAE,KAAA,QAAa,oBAAA;AAAA,UAEjC,gBAAA;EACf,OAAA;EACA,WAAA;EACA,QAAA,EAAU,MAAA,SAAe,aAAA;AAAA;AAAA,KAGf,UAAA;EAEN,OAAA;EACA,KAAA;EACA,QAAA;AAAA;EAGA,OAAA;EACA,KAAA;EACA,QAAA;AAAA;AAAA,cAGO,gBAAA;EAAA,QACH,QAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;EAER,WAAA,CAAY,WAAA,UAAqB,YAAA;EAzCE;;;EAiD7B,YAAA,CAAA,GAAgB,OAAA;EAjDoC;;AAE5D;EAuDE,WAAA,CAAA,GAAe,gBAAA;;;;EAOf,WAAA,CAAY,QAAA,EAAU,gBAAA;;;;;;EAShB,QAAA,CAAS,MAAA,EAAQ,eAAA,GAAkB,OAAA,CAAQ,UAAA;;;;;EAyD3C,cAAA,CAAe,MAAA,EAAQ,qBAAA,GAAwB,OAAA,CAAQ,UAAA;;;;EA6BvD,aAAA,CAAc,MAAA,EAAQ,oBAAA,GAAuB,OAAA,CAAQ,UAAA;EAAA,QA6B7C,SAAA;EAAA,QAiEA,kBAAA;;;;EA4Bd,UAAA,CAAW,EAAA,WAAa,aAAA;EAjRO;;;EAwR/B,iBAAA,CAAkB,IAAA,yBAA6B,MAAA,SAAe,aAAA;EAxR1B;;;EAySpC,iBAAA,CAAkB,IAAA,WAAe,MAAA,SAAe,aAAA;AAAA"}
|
|
@@ -10,6 +10,11 @@ const payloadEditText = z.object({
|
|
|
10
10
|
key: z.string().regex(/^[a-zA-Z0-9_.-]+$/),
|
|
11
11
|
content: z.string()
|
|
12
12
|
});
|
|
13
|
+
const payloadEditTextDirect = z.object({
|
|
14
|
+
action: z.literal("editTextDirect"),
|
|
15
|
+
id: z.string().min(1),
|
|
16
|
+
content: z.string()
|
|
17
|
+
});
|
|
13
18
|
const payloadEditClassName = z.object({
|
|
14
19
|
action: z.literal("editClassName"),
|
|
15
20
|
id: z.string().min(1),
|
|
@@ -106,6 +111,37 @@ var UpstartEditorAPI = class {
|
|
|
106
111
|
};
|
|
107
112
|
}
|
|
108
113
|
/**
|
|
114
|
+
* Edit a plain-text or rich-text JSX node directly in the source TSX file.
|
|
115
|
+
* For rich-text entries, `content` should be the inner JSX/HTML of the element's children.
|
|
116
|
+
*/
|
|
117
|
+
async editTextDirect(params) {
|
|
118
|
+
const parsed = payloadEditTextDirect.safeParse(params);
|
|
119
|
+
if (!parsed.success) return {
|
|
120
|
+
success: false,
|
|
121
|
+
error: `Invalid payload: ${parsed.error.message}`
|
|
122
|
+
};
|
|
123
|
+
const { id, content } = parsed.data;
|
|
124
|
+
if (!this.registry) try {
|
|
125
|
+
await this.loadRegistry();
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
error: `Failed to load registry: ${err}`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const entry = this.registry.elements[id];
|
|
133
|
+
if (!entry) return {
|
|
134
|
+
success: false,
|
|
135
|
+
error: `Element ${id} not found in registry`
|
|
136
|
+
};
|
|
137
|
+
if (entry.type !== "text" && entry.type !== "rich-text" && entry.type !== "mixed-text") return {
|
|
138
|
+
success: false,
|
|
139
|
+
error: `Element ${id} is not a text element (type: ${entry.type})`
|
|
140
|
+
};
|
|
141
|
+
if (entry.type === "mixed-text") return this.applyMixedTextEdit(id, entry, content);
|
|
142
|
+
return this.applyEdit(id, entry, content);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
109
145
|
* Edit the className of an element
|
|
110
146
|
*/
|
|
111
147
|
async editClassName(params) {
|
|
@@ -177,6 +213,28 @@ var UpstartEditorAPI = class {
|
|
|
177
213
|
}
|
|
178
214
|
}
|
|
179
215
|
/**
|
|
216
|
+
* Reconstruct JSX children from a user-edited template and the original expression segments,
|
|
217
|
+
* then write the result back to the TSX source file.
|
|
218
|
+
*
|
|
219
|
+
* `template` uses `{{N}}` placeholders for expressions, e.g.:
|
|
220
|
+
* "© {{0}} Alex's Kitchen. All rights reserved."
|
|
221
|
+
* The server replaces each placeholder with the original expression source from the registry.
|
|
222
|
+
*/
|
|
223
|
+
async applyMixedTextEdit(id, entry, template) {
|
|
224
|
+
const exprSegments = (entry.segments ?? []).filter((s) => s.type === "expr");
|
|
225
|
+
const parts = template.split(/\{\{(\d+)\}\}/);
|
|
226
|
+
let inner = "";
|
|
227
|
+
for (let i = 0; i < parts.length; i++) if (i % 2 === 0) inner += parts[i];
|
|
228
|
+
else {
|
|
229
|
+
const exprSeg = exprSegments[parseInt(parts[i])];
|
|
230
|
+
inner += exprSeg ? exprSeg.raw : `{{${parts[i]}}}`;
|
|
231
|
+
}
|
|
232
|
+
const leadingWS = entry.originalContent.match(/^(\s*)/)?.[1] ?? "";
|
|
233
|
+
const trailingWS = entry.originalContent.match(/(\s*)$/)?.[1] ?? "";
|
|
234
|
+
const newContent = leadingWS + inner + trailingWS;
|
|
235
|
+
return this.applyEdit(id, entry, newContent);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
180
238
|
* Get element info by ID
|
|
181
239
|
*/
|
|
182
240
|
getElement(id) {
|
|
@@ -202,6 +260,6 @@ var UpstartEditorAPI = class {
|
|
|
202
260
|
}
|
|
203
261
|
};
|
|
204
262
|
//#endregion
|
|
205
|
-
export { UpstartEditorAPI, payloadEditClassName, payloadEditText };
|
|
263
|
+
export { UpstartEditorAPI, payloadEditClassName, payloadEditText, payloadEditTextDirect };
|
|
206
264
|
|
|
207
265
|
//# sourceMappingURL=upstart-editor-api.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upstart-editor-api.js","names":[],"sources":["../src/upstart-editor-api.ts"],"sourcesContent":["import MagicString from \"magic-string\";\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport z from \"zod\";\nimport type { EditableEntry } from \"./vite-plugin-upstart-attrs\";\n\nexport const payloadEditText = z.object({\n action: z.literal(\"editText\"),\n language: z\n .string()\n .length(2)\n .regex(/^[a-z]{2}$/),\n namespace: z.string().regex(/^[a-z0-9_-]+$/),\n key: z.string().regex(/^[a-zA-Z0-9_.-]+$/),\n content: z.string(),\n});\n\nexport type PayloadEditText = z.infer<typeof payloadEditText>;\n\nexport const payloadEditClassName = z.object({\n action: z.literal(\"editClassName\"),\n id: z.string().min(1),\n className: z.string(),\n});\n\nexport type PayloadEditClassName = z.infer<typeof payloadEditClassName>;\n\nexport interface EditableRegistry {\n version: number;\n generatedAt: string;\n elements: Record<string, EditableEntry>;\n}\n\nexport type EditResult =\n | {\n success: true;\n error?: never;\n filePath: string;\n }\n | {\n success: false;\n error: string;\n filePath?: never;\n };\n\nexport class UpstartEditorAPI {\n private registry: EditableRegistry | null = null;\n private projectRoot: string;\n private registryPath: string;\n\n constructor(projectRoot: string, registryPath: string) {\n this.projectRoot = projectRoot;\n this.registryPath = registryPath;\n }\n\n /**\n * Load the registry from disk\n */\n async loadRegistry(): Promise<void> {\n const content = await fs.readFile(this.registryPath, \"utf-8\");\n this.registry = JSON.parse(content);\n }\n\n /**\n * Get the current registry (for testing/debugging)\n */\n getRegistry(): EditableRegistry | null {\n return this.registry;\n }\n\n /**\n * Set the registry directly (for testing)\n */\n setRegistry(registry: EditableRegistry): void {\n this.registry = registry;\n }\n\n /**\n * Edit a translation value in an i18next locale file.\n * Auto-detects flat keys (e.g. \"nav.home\" as literal key) vs nested keys (e.g. nav -> home).\n * Only updates existing keys — returns an error if the key is not found.\n */\n async editText(params: PayloadEditText): Promise<EditResult> {\n const parsed = payloadEditText.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { language, namespace, key, content: newContent } = parsed.data;\n const filePath = path.join(this.projectRoot, \"app\", \"locales\", language, `${namespace}.json`);\n\n let raw: string;\n try {\n raw = await fs.readFile(filePath, \"utf-8\");\n } catch (err) {\n return { success: false, error: `Failed to read locale file: ${filePath}` };\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw);\n } catch (err) {\n return { success: false, error: `Failed to parse locale file: ${filePath}` };\n }\n\n // Strategy 1: check for flat/literal key at top level\n if (key in data && typeof data[key] === \"string\") {\n data[key] = newContent;\n } else {\n // Strategy 2: nested traversal via dot notation\n const parts = key.split(\".\");\n let current: Record<string, unknown> = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n if (current[part] == null || typeof current[part] !== \"object\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current = current[part] as Record<string, unknown>;\n }\n\n const leafKey = parts[parts.length - 1];\n if (!(leafKey in current) || typeof current[leafKey] !== \"string\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current[leafKey] = newContent;\n }\n\n try {\n await fs.writeFile(filePath, JSON.stringify(data, null, 2) + \"\\n\");\n } catch (err) {\n return { success: false, error: `Failed to write locale file: ${filePath}` };\n }\n\n return { success: true, filePath };\n }\n\n /**\n * Edit the className of an element\n */\n async editClassName(params: PayloadEditClassName): Promise<EditResult> {\n const parsed = payloadEditClassName.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, className: newClassName } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n\n if (entry.type !== \"className\") {\n return { success: false, error: `Element ${id} is not a className element (type: ${entry.type})` };\n }\n\n return this.applyEdit(id, entry, newClassName);\n }\n\n /**\n * Apply an edit to a source file\n */\n private async applyEdit(id: string, entry: EditableEntry, newContent: string): Promise<EditResult> {\n const filePath = path.join(this.projectRoot, entry.file);\n\n try {\n const code = await fs.readFile(filePath, \"utf-8\");\n\n // Verify content at expected location\n const currentContent = code.slice(entry.startOffset, entry.endOffset);\n\n let actualStart = entry.startOffset;\n let actualEnd = entry.endOffset;\n\n if (currentContent !== entry.originalContent) {\n // Content has shifted - try to find it by searching\n const searchIndex = code.indexOf(entry.originalContent);\n if (searchIndex === -1) {\n return {\n success: false,\n error: `Original content \"${entry.originalContent}\" not found in file ${entry.file}. The file may have been modified.`,\n };\n }\n actualStart = searchIndex;\n actualEnd = searchIndex + entry.originalContent.length;\n }\n\n // Apply the edit using MagicString\n const s = new MagicString(code);\n s.overwrite(actualStart, actualEnd, newContent);\n\n // Write the modified file\n await fs.writeFile(filePath, s.toString());\n\n // Calculate the length difference for offset adjustments\n const lengthDiff = newContent.length - entry.originalContent.length;\n\n // Update the registry entry\n entry.startOffset = actualStart;\n entry.endOffset = actualStart + newContent.length;\n entry.originalContent = newContent;\n\n // Shift all subsequent entries in the same file\n for (const [otherId, otherEntry] of Object.entries(this.registry!.elements)) {\n if (otherId !== id && otherEntry.file === entry.file && otherEntry.startOffset > actualStart) {\n otherEntry.startOffset += lengthDiff;\n otherEntry.endOffset += lengthDiff;\n }\n }\n\n // Save the updated registry\n await fs.writeFile(this.registryPath, JSON.stringify(this.registry, null, 2));\n\n return { success: true, filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Get element info by ID\n */\n getElement(id: string): EditableEntry | undefined {\n return this.registry?.elements[id];\n }\n\n /**\n * Get all elements of a specific type\n */\n getElementsByType(type: \"text\" | \"className\"): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.type === type) {\n result[id] = entry;\n }\n }\n return result;\n }\n\n /**\n * Get all elements in a specific file\n */\n getElementsByFile(file: string): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.file === file) {\n result[id] = entry;\n }\n }\n return result;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"upstart-editor-api.js","names":[],"sources":["../src/upstart-editor-api.ts"],"sourcesContent":["import MagicString from \"magic-string\";\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport z from \"zod\";\nimport type { EditableEntry } from \"./vite-plugin-upstart-attrs\";\n\nexport const payloadEditText = z.object({\n action: z.literal(\"editText\"),\n language: z\n .string()\n .length(2)\n .regex(/^[a-z]{2}$/),\n namespace: z.string().regex(/^[a-z0-9_-]+$/),\n key: z.string().regex(/^[a-zA-Z0-9_.-]+$/),\n content: z.string(),\n});\n\nexport type PayloadEditText = z.infer<typeof payloadEditText>;\n\nexport const payloadEditTextDirect = z.object({\n action: z.literal(\"editTextDirect\"),\n id: z.string().min(1),\n content: z.string(),\n});\n\nexport type PayloadEditTextDirect = z.infer<typeof payloadEditTextDirect>;\n\nexport const payloadEditClassName = z.object({\n action: z.literal(\"editClassName\"),\n id: z.string().min(1),\n className: z.string(),\n});\n\nexport type PayloadEditClassName = z.infer<typeof payloadEditClassName>;\n\nexport interface EditableRegistry {\n version: number;\n generatedAt: string;\n elements: Record<string, EditableEntry>;\n}\n\nexport type EditResult =\n | {\n success: true;\n error?: never;\n filePath: string;\n }\n | {\n success: false;\n error: string;\n filePath?: never;\n };\n\nexport class UpstartEditorAPI {\n private registry: EditableRegistry | null = null;\n private projectRoot: string;\n private registryPath: string;\n\n constructor(projectRoot: string, registryPath: string) {\n this.projectRoot = projectRoot;\n this.registryPath = registryPath;\n }\n\n /**\n * Load the registry from disk\n */\n async loadRegistry(): Promise<void> {\n const content = await fs.readFile(this.registryPath, \"utf-8\");\n this.registry = JSON.parse(content);\n }\n\n /**\n * Get the current registry (for testing/debugging)\n */\n getRegistry(): EditableRegistry | null {\n return this.registry;\n }\n\n /**\n * Set the registry directly (for testing)\n */\n setRegistry(registry: EditableRegistry): void {\n this.registry = registry;\n }\n\n /**\n * Edit a translation value in an i18next locale file.\n * Auto-detects flat keys (e.g. \"nav.home\" as literal key) vs nested keys (e.g. nav -> home).\n * Only updates existing keys — returns an error if the key is not found.\n */\n async editText(params: PayloadEditText): Promise<EditResult> {\n const parsed = payloadEditText.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { language, namespace, key, content: newContent } = parsed.data;\n const filePath = path.join(this.projectRoot, \"app\", \"locales\", language, `${namespace}.json`);\n\n let raw: string;\n try {\n raw = await fs.readFile(filePath, \"utf-8\");\n } catch (err) {\n return { success: false, error: `Failed to read locale file: ${filePath}` };\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw);\n } catch (err) {\n return { success: false, error: `Failed to parse locale file: ${filePath}` };\n }\n\n // Strategy 1: check for flat/literal key at top level\n if (key in data && typeof data[key] === \"string\") {\n data[key] = newContent;\n } else {\n // Strategy 2: nested traversal via dot notation\n const parts = key.split(\".\");\n let current: Record<string, unknown> = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n if (current[part] == null || typeof current[part] !== \"object\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current = current[part] as Record<string, unknown>;\n }\n\n const leafKey = parts[parts.length - 1];\n if (!(leafKey in current) || typeof current[leafKey] !== \"string\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current[leafKey] = newContent;\n }\n\n try {\n await fs.writeFile(filePath, JSON.stringify(data, null, 2) + \"\\n\");\n } catch (err) {\n return { success: false, error: `Failed to write locale file: ${filePath}` };\n }\n\n return { success: true, filePath };\n }\n\n /**\n * Edit a plain-text or rich-text JSX node directly in the source TSX file.\n * For rich-text entries, `content` should be the inner JSX/HTML of the element's children.\n */\n async editTextDirect(params: PayloadEditTextDirect): Promise<EditResult> {\n const parsed = payloadEditTextDirect.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, content } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n if (entry.type !== \"text\" && entry.type !== \"rich-text\" && entry.type !== \"mixed-text\") {\n return { success: false, error: `Element ${id} is not a text element (type: ${entry.type})` };\n }\n if (entry.type === \"mixed-text\") {\n return this.applyMixedTextEdit(id, entry, content);\n }\n return this.applyEdit(id, entry, content);\n }\n\n /**\n * Edit the className of an element\n */\n async editClassName(params: PayloadEditClassName): Promise<EditResult> {\n const parsed = payloadEditClassName.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, className: newClassName } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n\n if (entry.type !== \"className\") {\n return { success: false, error: `Element ${id} is not a className element (type: ${entry.type})` };\n }\n\n return this.applyEdit(id, entry, newClassName);\n }\n\n /**\n * Apply an edit to a source file\n */\n private async applyEdit(id: string, entry: EditableEntry, newContent: string): Promise<EditResult> {\n const filePath = path.join(this.projectRoot, entry.file);\n\n try {\n const code = await fs.readFile(filePath, \"utf-8\");\n\n // Verify content at expected location\n const currentContent = code.slice(entry.startOffset, entry.endOffset);\n\n let actualStart = entry.startOffset;\n let actualEnd = entry.endOffset;\n\n if (currentContent !== entry.originalContent) {\n // Content has shifted - try to find it by searching\n const searchIndex = code.indexOf(entry.originalContent);\n if (searchIndex === -1) {\n return {\n success: false,\n error: `Original content \"${entry.originalContent}\" not found in file ${entry.file}. The file may have been modified.`,\n };\n }\n actualStart = searchIndex;\n actualEnd = searchIndex + entry.originalContent.length;\n }\n\n // Apply the edit using MagicString\n const s = new MagicString(code);\n s.overwrite(actualStart, actualEnd, newContent);\n\n // Write the modified file\n await fs.writeFile(filePath, s.toString());\n\n // Calculate the length difference for offset adjustments\n const lengthDiff = newContent.length - entry.originalContent.length;\n\n // Update the registry entry\n entry.startOffset = actualStart;\n entry.endOffset = actualStart + newContent.length;\n entry.originalContent = newContent;\n\n // Shift all subsequent entries in the same file\n for (const [otherId, otherEntry] of Object.entries(this.registry!.elements)) {\n if (otherId !== id && otherEntry.file === entry.file && otherEntry.startOffset > actualStart) {\n otherEntry.startOffset += lengthDiff;\n otherEntry.endOffset += lengthDiff;\n }\n }\n\n // Save the updated registry\n await fs.writeFile(this.registryPath, JSON.stringify(this.registry, null, 2));\n\n return { success: true, filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Reconstruct JSX children from a user-edited template and the original expression segments,\n * then write the result back to the TSX source file.\n *\n * `template` uses `{{N}}` placeholders for expressions, e.g.:\n * \"© {{0}} Alex's Kitchen. All rights reserved.\"\n * The server replaces each placeholder with the original expression source from the registry.\n */\n private async applyMixedTextEdit(id: string, entry: EditableEntry, template: string): Promise<EditResult> {\n const exprSegments = (entry.segments ?? []).filter((s) => s.type === \"expr\");\n\n // Split template by {{N}} placeholders — odd indices are expression indices\n const parts = template.split(/\\{\\{(\\d+)\\}\\}/);\n\n // Reconstruct inner JSX: text parts interleaved with original expression sources\n let inner = \"\";\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n inner += parts[i];\n } else {\n const exprSeg = exprSegments[parseInt(parts[i])];\n inner += exprSeg ? exprSeg.raw : `{{${parts[i]}}}`;\n }\n }\n\n // Preserve leading/trailing JSX whitespace (indentation) from the original source\n const leadingWS = entry.originalContent.match(/^(\\s*)/)?.[1] ?? \"\";\n const trailingWS = entry.originalContent.match(/(\\s*)$/)?.[1] ?? \"\";\n const newContent = leadingWS + inner + trailingWS;\n\n return this.applyEdit(id, entry, newContent);\n }\n\n /**\n * Get element info by ID\n */\n getElement(id: string): EditableEntry | undefined {\n return this.registry?.elements[id];\n }\n\n /**\n * Get all elements of a specific type\n */\n getElementsByType(type: \"text\" | \"className\"): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.type === type) {\n result[id] = entry;\n }\n }\n return result;\n }\n\n /**\n * Get all elements in a specific file\n */\n getElementsByFile(file: string): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.file === file) {\n result[id] = entry;\n }\n }\n return result;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,29 +1,34 @@
|
|
|
1
|
-
import * as magic_string0 from "magic-string";
|
|
2
|
-
import * as unplugin from "unplugin";
|
|
1
|
+
import * as _$magic_string0 from "magic-string";
|
|
2
|
+
import * as _$unplugin from "unplugin";
|
|
3
3
|
|
|
4
4
|
//#region src/vite-plugin-upstart-attrs.d.ts
|
|
5
5
|
interface Options {
|
|
6
6
|
enabled: boolean;
|
|
7
7
|
emitRegistry?: boolean;
|
|
8
8
|
}
|
|
9
|
+
interface EditableSegment {
|
|
10
|
+
type: "text" | "expr";
|
|
11
|
+
raw: string;
|
|
12
|
+
}
|
|
9
13
|
interface EditableEntry {
|
|
10
14
|
file: string;
|
|
11
|
-
type: "text" | "className";
|
|
15
|
+
type: "text" | "rich-text" | "className" | "mixed-text";
|
|
12
16
|
startOffset: number;
|
|
13
17
|
endOffset: number;
|
|
14
18
|
originalContent: string;
|
|
19
|
+
segments?: EditableSegment[];
|
|
15
20
|
context: {
|
|
16
21
|
parentTag: string;
|
|
17
22
|
};
|
|
18
23
|
}
|
|
19
24
|
declare function getRegistry(): Record<string, EditableEntry>;
|
|
20
25
|
declare function clearRegistry(): void;
|
|
21
|
-
declare const upstartEditor: unplugin.UnpluginInstance<Options, boolean>;
|
|
26
|
+
declare const upstartEditor: _$unplugin.UnpluginInstance<Options, boolean>;
|
|
22
27
|
declare function transformWithOxc(code: string, filePath: string): {
|
|
23
28
|
code: string;
|
|
24
|
-
map: magic_string0.SourceMap;
|
|
29
|
+
map: _$magic_string0.SourceMap;
|
|
25
30
|
} | null;
|
|
26
|
-
declare const _default: (options: Options) => unplugin.VitePlugin<any>[] | unplugin.VitePlugin<any>;
|
|
31
|
+
declare const _default: (options: Options) => _$unplugin.VitePlugin<any>[] | _$unplugin.VitePlugin<any>;
|
|
27
32
|
//#endregion
|
|
28
|
-
export { EditableEntry, clearRegistry, _default as default, getRegistry, transformWithOxc, upstartEditor };
|
|
33
|
+
export { EditableEntry, EditableSegment, clearRegistry, _default as default, getRegistry, transformWithOxc, upstartEditor };
|
|
29
34
|
//# sourceMappingURL=vite-plugin-upstart-attrs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-plugin-upstart-attrs.d.ts","names":[],"sources":["../src/vite-plugin-upstart-attrs.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"vite-plugin-upstart-attrs.d.ts","names":[],"sources":["../src/vite-plugin-upstart-attrs.ts"],"mappings":";;;;UAgBU,OAAA;EACR,OAAA;EACA,YAAA;AAAA;AAAA,UA0Ce,eAAA;EACf,IAAA;EAGA,GAAA;AAAA;AAAA,UAIe,aAAA;EACf,IAAA;EACA,IAAA;EACA,WAAA;EACA,SAAA;EACA,eAAA;EAEA,QAAA,GAAW,eAAA;EACX,OAAA;IAAW,SAAA;EAAA;AAAA;AAAA,iBAcG,WAAA,CAAA,GAAe,MAAA,SAAe,aAAA;AAAA,iBAK9B,aAAA,CAAA;AAAA,cAeH,aAAA,EAAa,UAAA,CAAA,gBAAA,CAAA,OAAA;AAAA,iBA8FV,gBAAA,CAAiB,IAAA,UAAc,QAAA;;OAAf,eAAA,CAAA,SAAA;AAAA;AAAA,cAiU/B,QAAA"}
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { createUnplugin } from "unplugin";
|
|
4
4
|
import { parseSync } from "oxc-parser";
|
|
5
5
|
import { walk } from "zimmerframe";
|
|
6
|
+
import fs from "node:fs";
|
|
6
7
|
//#region src/vite-plugin-upstart-attrs.ts
|
|
7
8
|
function hasRange(node) {
|
|
8
9
|
return node && typeof node.start === "number" && typeof node.end === "number";
|
|
@@ -26,12 +27,35 @@ const upstartEditor = createUnplugin((options) => {
|
|
|
26
27
|
if (!options.enabled) return { name: "upstart-editor-disabled" };
|
|
27
28
|
const emitRegistry = options.emitRegistry ?? true;
|
|
28
29
|
let root = process.cwd();
|
|
30
|
+
let isDevMode = false;
|
|
31
|
+
let devWriteTimer = null;
|
|
32
|
+
const flushDevRegistry = () => {
|
|
33
|
+
if (!isDevMode || editableRegistry.size === 0) return;
|
|
34
|
+
const registryDir = path.join(root, "build", "server");
|
|
35
|
+
const registryPath = path.join(registryDir, "upstart-registry.json");
|
|
36
|
+
const registry = {
|
|
37
|
+
version: 1,
|
|
38
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
39
|
+
elements: Object.fromEntries(editableRegistry)
|
|
40
|
+
};
|
|
41
|
+
fs.mkdirSync(registryDir, { recursive: true });
|
|
42
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
43
|
+
};
|
|
44
|
+
const scheduleDevRegistryWrite = () => {
|
|
45
|
+
if (!isDevMode) return;
|
|
46
|
+
if (devWriteTimer) clearTimeout(devWriteTimer);
|
|
47
|
+
devWriteTimer = setTimeout(flushDevRegistry, 150);
|
|
48
|
+
};
|
|
29
49
|
return {
|
|
30
50
|
name: "upstart-editor",
|
|
31
51
|
enforce: "pre",
|
|
32
52
|
vite: {
|
|
33
53
|
configResolved(config) {
|
|
34
54
|
root = config.root;
|
|
55
|
+
isDevMode = config.command === "serve";
|
|
56
|
+
},
|
|
57
|
+
configureServer(_server) {
|
|
58
|
+
isDevMode = true;
|
|
35
59
|
},
|
|
36
60
|
generateBundle() {
|
|
37
61
|
if (!emitRegistry || editableRegistry.size === 0) return;
|
|
@@ -56,6 +80,7 @@ const upstartEditor = createUnplugin((options) => {
|
|
|
56
80
|
try {
|
|
57
81
|
const result = transformWithOxc(code, path.relative(root, id));
|
|
58
82
|
if (!result) return null;
|
|
83
|
+
scheduleDevRegistryWrite();
|
|
59
84
|
return {
|
|
60
85
|
code: result.code,
|
|
61
86
|
map: result.map
|
|
@@ -116,7 +141,7 @@ function transformWithOxc(code, filePath) {
|
|
|
116
141
|
const tCallChildren = findTCallsInChildren(jsxNode, state.code, state);
|
|
117
142
|
const allI18nKeys = [...transChildren, ...tCallChildren];
|
|
118
143
|
const hasI18n = allI18nKeys.length > 0;
|
|
119
|
-
if (hasI18n) {
|
|
144
|
+
if (hasI18n && !hasMixedNonI18nContent(jsxNode, state.code, state, state.constants)) {
|
|
120
145
|
attributes.push("data-upstart-editable-text=\"true\"");
|
|
121
146
|
attributes.push("data-upstart-editable-text-mode=\"plain\"");
|
|
122
147
|
if (allI18nKeys.some((t) => t.keyExpr || t.nsExpr)) {
|
|
@@ -128,9 +153,14 @@ function transformWithOxc(code, filePath) {
|
|
|
128
153
|
const keys = allI18nKeys.map((t) => escapeProp(t.fullKey)).join(",");
|
|
129
154
|
attributes.push(`data-upstart-i18n="${keys}"`);
|
|
130
155
|
}
|
|
156
|
+
const allValueKeys = [...new Set(allI18nKeys.flatMap((t) => t.valueKeys ?? []))];
|
|
157
|
+
if (allValueKeys.length > 0) attributes.push(`data-i18n-values="${allValueKeys.join(",")}"`);
|
|
131
158
|
}
|
|
132
159
|
if (isTextLeafElement(jsxNode)) {
|
|
133
|
-
if (!hasI18n)
|
|
160
|
+
if (!hasI18n) {
|
|
161
|
+
attributes.push("data-upstart-editable-text=\"true\"");
|
|
162
|
+
attributes.push("data-upstart-editable-text-mode=\"direct\"");
|
|
163
|
+
}
|
|
134
164
|
const textChild = jsxNode.children.find((c) => c.type === "JSXText" && c.value?.trim());
|
|
135
165
|
if (textChild && hasRange(textChild)) {
|
|
136
166
|
const id = generateId(state.filePath, textChild);
|
|
@@ -147,6 +177,66 @@ function transformWithOxc(code, filePath) {
|
|
|
147
177
|
});
|
|
148
178
|
attributes.push(`data-upstart-id="${id}"`);
|
|
149
179
|
}
|
|
180
|
+
} else if (!hasI18n && isAlmostLeafElement(jsxNode)) {
|
|
181
|
+
attributes.push("data-upstart-editable-text=\"true\"");
|
|
182
|
+
attributes.push("data-upstart-editable-text-mode=\"rich-panel\"");
|
|
183
|
+
const children = jsxNode.children.filter((c) => hasRange(c));
|
|
184
|
+
const firstChild = children[0];
|
|
185
|
+
const lastChild = children[children.length - 1];
|
|
186
|
+
if (firstChild && lastChild && hasRange(firstChild) && hasRange(lastChild)) {
|
|
187
|
+
const id = generateId(state.filePath, firstChild);
|
|
188
|
+
const originalContent = state.code.slice(firstChild.start, lastChild.end);
|
|
189
|
+
editableRegistry.set(id, {
|
|
190
|
+
file: state.filePath,
|
|
191
|
+
type: "rich-text",
|
|
192
|
+
startOffset: firstChild.start,
|
|
193
|
+
endOffset: lastChild.end,
|
|
194
|
+
originalContent,
|
|
195
|
+
context: { parentTag: tagName || "unknown" }
|
|
196
|
+
});
|
|
197
|
+
attributes.push(`data-upstart-id="${id}"`);
|
|
198
|
+
}
|
|
199
|
+
} else if (!hasI18n && isMixedTextLeafElement(jsxNode)) {
|
|
200
|
+
attributes.push("data-upstart-editable-text=\"true\"");
|
|
201
|
+
attributes.push("data-upstart-editable-text-mode=\"plain\"");
|
|
202
|
+
const rangedChildren = jsxNode.children.filter((c) => hasRange(c));
|
|
203
|
+
const firstChild = rangedChildren[0];
|
|
204
|
+
const lastChild = rangedChildren[rangedChildren.length - 1];
|
|
205
|
+
if (firstChild && lastChild && hasRange(firstChild) && hasRange(lastChild)) {
|
|
206
|
+
const segments = [];
|
|
207
|
+
let exprIndex = 0;
|
|
208
|
+
let templateStr = "";
|
|
209
|
+
for (const child of jsxNode.children) if (child.type === "JSXText") {
|
|
210
|
+
const normalized = normalizeJSXText(child.value);
|
|
211
|
+
segments.push({
|
|
212
|
+
type: "text",
|
|
213
|
+
raw: child.value
|
|
214
|
+
});
|
|
215
|
+
templateStr += normalized;
|
|
216
|
+
} else if (child.type === "JSXExpressionContainer") {
|
|
217
|
+
const expr = child.expression;
|
|
218
|
+
if (!expr || expr.type === "JSXEmptyExpression") continue;
|
|
219
|
+
const raw = hasRange(child) ? state.code.slice(child.start, child.end) : "";
|
|
220
|
+
segments.push({
|
|
221
|
+
type: "expr",
|
|
222
|
+
raw
|
|
223
|
+
});
|
|
224
|
+
templateStr += `{{${exprIndex++}}}`;
|
|
225
|
+
}
|
|
226
|
+
const id = generateId(state.filePath, firstChild);
|
|
227
|
+
const originalContent = state.code.slice(firstChild.start, lastChild.end);
|
|
228
|
+
editableRegistry.set(id, {
|
|
229
|
+
file: state.filePath,
|
|
230
|
+
type: "mixed-text",
|
|
231
|
+
startOffset: firstChild.start,
|
|
232
|
+
endOffset: lastChild.end,
|
|
233
|
+
originalContent,
|
|
234
|
+
segments,
|
|
235
|
+
context: { parentTag: tagName || "unknown" }
|
|
236
|
+
});
|
|
237
|
+
attributes.push(`data-upstart-id="${id}"`);
|
|
238
|
+
attributes.push(`data-upstart-mixed-template="${escapeProp(templateStr.trim())}"`);
|
|
239
|
+
}
|
|
150
240
|
} else if (!hasI18n && hasVisibleTextContent(jsxNode)) attributes.push("data-upstart-editable-text=\"false\"");
|
|
151
241
|
const classNameAttr = opening.attributes.find((attr) => attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && attr.name.name === "className" && attr.value?.type === "Literal" && typeof attr.value.value === "string");
|
|
152
242
|
if (classNameAttr && classNameAttr.value && hasRange(classNameAttr.value)) {
|
|
@@ -355,11 +445,26 @@ function detectTransComponent(jsxElement, code, constants) {
|
|
|
355
445
|
if (getJSXElementName(opening) !== "Trans") return null;
|
|
356
446
|
let keyResult = null;
|
|
357
447
|
let nsResult = null;
|
|
448
|
+
let valueKeys;
|
|
358
449
|
for (const attr of opening.attributes) {
|
|
359
450
|
if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
|
|
360
451
|
const attrName = attr.name.name;
|
|
361
452
|
if (attrName === "i18nKey") keyResult = resolveJSXAttrValue(attr.value, code, constants);
|
|
362
453
|
if (attrName === "ns") nsResult = resolveJSXAttrValue(attr.value, code, constants);
|
|
454
|
+
if (attrName === "values") {
|
|
455
|
+
const container = attr.value;
|
|
456
|
+
if (container?.type === "JSXExpressionContainer") {
|
|
457
|
+
const expr = container.expression;
|
|
458
|
+
if (expr?.type === "ObjectExpression") {
|
|
459
|
+
const keys = [];
|
|
460
|
+
for (const prop of expr.properties) if ((prop.type === "Property" || prop.type === "ObjectProperty") && prop.key) {
|
|
461
|
+
if (prop.key.type === "Identifier") keys.push(prop.key.name);
|
|
462
|
+
else if (prop.key.type === "Literal" || prop.key.type === "StringLiteral") keys.push(prop.key.value);
|
|
463
|
+
}
|
|
464
|
+
if (keys.length > 0) valueKeys = keys;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
363
468
|
}
|
|
364
469
|
if (!keyResult) return null;
|
|
365
470
|
const key = keyResult.value;
|
|
@@ -371,7 +476,8 @@ function detectTransComponent(jsxElement, code, constants) {
|
|
|
371
476
|
key,
|
|
372
477
|
namespace,
|
|
373
478
|
keyExpr,
|
|
374
|
-
nsExpr
|
|
479
|
+
nsExpr,
|
|
480
|
+
valueKeys
|
|
375
481
|
};
|
|
376
482
|
}
|
|
377
483
|
function findTransInChildren(jsxElement, code, constants) {
|
|
@@ -427,6 +533,24 @@ function findTCallsInChildren(jsxElement, code, state) {
|
|
|
427
533
|
}
|
|
428
534
|
return results;
|
|
429
535
|
}
|
|
536
|
+
function hasMixedNonI18nContent(jsxNode, code, state, constants) {
|
|
537
|
+
for (const child of jsxNode.children) if (child.type === "JSXText") {
|
|
538
|
+
if (child.value.trim().length > 0) return true;
|
|
539
|
+
} else if (child.type === "JSXExpressionContainer") {
|
|
540
|
+
const expr = child.expression;
|
|
541
|
+
if (!expr || expr.type === "JSXEmptyExpression") continue;
|
|
542
|
+
if (expr.type === "Literal" && typeof expr.value === "string" && expr.value.trim() === "") continue;
|
|
543
|
+
const tResults = [];
|
|
544
|
+
findTCallInExpression(expr, code, tResults, state);
|
|
545
|
+
if (tResults.length > 0) continue;
|
|
546
|
+
const transResults = [];
|
|
547
|
+
findTransInExpression(expr, code, transResults, constants);
|
|
548
|
+
if (transResults.length > 0) continue;
|
|
549
|
+
const type = expr.type;
|
|
550
|
+
if (type === "Identifier" || type === "MemberExpression" || type === "Literal" || type === "TemplateLiteral") return true;
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
430
554
|
function findTCallInExpression(expr, code, results, state) {
|
|
431
555
|
if (!expr || !expr.type) return;
|
|
432
556
|
if (expr.type === "CallExpression") {
|
|
@@ -503,6 +627,74 @@ function isTextLeafElement(jsxElement) {
|
|
|
503
627
|
} else if (child.type === "JSXElement" || child.type === "JSXFragment" || child.type === "JSXExpressionContainer" || child.type === "JSXSpreadChild") return false;
|
|
504
628
|
return hasText;
|
|
505
629
|
}
|
|
630
|
+
const INLINE_FORMATTING_TAGS = new Set([
|
|
631
|
+
"b",
|
|
632
|
+
"i",
|
|
633
|
+
"em",
|
|
634
|
+
"strong",
|
|
635
|
+
"u",
|
|
636
|
+
"s",
|
|
637
|
+
"del",
|
|
638
|
+
"ins",
|
|
639
|
+
"mark",
|
|
640
|
+
"small",
|
|
641
|
+
"sub",
|
|
642
|
+
"sup",
|
|
643
|
+
"code",
|
|
644
|
+
"kbd",
|
|
645
|
+
"abbr",
|
|
646
|
+
"cite",
|
|
647
|
+
"time",
|
|
648
|
+
"span",
|
|
649
|
+
"a"
|
|
650
|
+
]);
|
|
651
|
+
function isAlmostLeafElement(jsxElement) {
|
|
652
|
+
let hasInlineElement = false;
|
|
653
|
+
for (const child of jsxElement.children) {
|
|
654
|
+
if (child.type === "JSXText") continue;
|
|
655
|
+
if (child.type === "JSXElement") {
|
|
656
|
+
const childEl = child;
|
|
657
|
+
const tagName = getJSXElementName(childEl.openingElement);
|
|
658
|
+
if (!tagName || !INLINE_FORMATTING_TAGS.has(tagName.toLowerCase())) return false;
|
|
659
|
+
if (childEl.openingElement.attributes.length > 0) return false;
|
|
660
|
+
for (const grandchild of childEl.children) if (grandchild.type !== "JSXText") return false;
|
|
661
|
+
hasInlineElement = true;
|
|
662
|
+
} else return false;
|
|
663
|
+
}
|
|
664
|
+
return hasInlineElement;
|
|
665
|
+
}
|
|
666
|
+
function normalizeJSXText(raw) {
|
|
667
|
+
const lines = raw.split(/\r\n|\n|\r/);
|
|
668
|
+
const lastNonEmptyIdx = lines.reduce((acc, line, i) => /[^ \t]/.test(line) ? i : acc, -1);
|
|
669
|
+
let result = "";
|
|
670
|
+
for (let i = 0; i < lines.length; i++) {
|
|
671
|
+
let line = lines[i].replace(/\t/g, " ");
|
|
672
|
+
if (i > 0) line = line.replace(/^[ ]+/, "");
|
|
673
|
+
if (i < lines.length - 1) line = line.replace(/[ ]+$/, "");
|
|
674
|
+
if (!line) continue;
|
|
675
|
+
if (i !== lastNonEmptyIdx) line += " ";
|
|
676
|
+
result += line;
|
|
677
|
+
}
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
function isMixedTextLeafElement(jsxElement) {
|
|
681
|
+
let hasText = false;
|
|
682
|
+
let hasExpr = false;
|
|
683
|
+
for (const child of jsxElement.children) {
|
|
684
|
+
if (child.type === "JSXText") {
|
|
685
|
+
if (normalizeJSXText(child.value).length > 0) hasText = true;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (child.type === "JSXExpressionContainer") {
|
|
689
|
+
const expr = child.expression;
|
|
690
|
+
if (!expr || expr.type === "JSXEmptyExpression") continue;
|
|
691
|
+
hasExpr = true;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
return hasText && hasExpr;
|
|
697
|
+
}
|
|
506
698
|
function hasVisibleTextContent(jsxElement) {
|
|
507
699
|
for (const child of jsxElement.children) {
|
|
508
700
|
if (child.type === "JSXText") {
|