@upstart.gg/vite-plugins 0.0.39 → 0.0.41
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 +79 -0
- package/dist/upstart-editor-api.d.ts.map +1 -0
- package/dist/upstart-editor-api.js +208 -0
- package/dist/upstart-editor-api.js.map +1 -0
- package/dist/vite-plugin-upstart-attrs.d.ts +3 -3
- package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-attrs.js +227 -25
- package/dist/vite-plugin-upstart-attrs.js.map +1 -1
- package/dist/vite-plugin-upstart-branding/plugin.d.ts +17 -0
- package/dist/vite-plugin-upstart-branding/plugin.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-branding/plugin.js +41 -0
- package/dist/vite-plugin-upstart-branding/plugin.js.map +1 -0
- package/dist/vite-plugin-upstart-branding/runtime.d.ts +10 -0
- package/dist/vite-plugin-upstart-branding/runtime.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-branding/runtime.js +118 -0
- package/dist/vite-plugin-upstart-branding/runtime.js.map +1 -0
- package/dist/vite-plugin-upstart-branding/types.d.ts +14 -0
- package/dist/vite-plugin-upstart-branding/types.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-branding/types.js +1 -0
- 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 +3 -16
- package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +25 -11
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts +5 -0
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.js +16 -0
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -0
- 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 +2 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.js +42 -7
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts +6 -1
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +423 -129
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +18 -10
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.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 +1 -3
- package/dist/vite-plugin-upstart-theme.js.map +1 -1
- package/package.json +12 -4
- package/src/tests/upstart-editor-api.test.ts +98 -174
- package/src/tests/vite-plugin-upstart-attrs.test.ts +408 -105
- package/src/tests/vite-plugin-upstart-branding.test.ts +90 -0
- package/src/tests/vite-plugin-upstart-editor.test.ts +1 -2
- package/src/upstart-editor-api.ts +90 -29
- package/src/vite-plugin-upstart-attrs.ts +376 -38
- package/src/vite-plugin-upstart-branding/plugin.ts +59 -0
- package/src/vite-plugin-upstart-branding/runtime.ts +128 -0
- package/src/vite-plugin-upstart-branding/types.ts +10 -0
- package/src/vite-plugin-upstart-editor/plugin.ts +4 -19
- package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +25 -12
- package/src/vite-plugin-upstart-editor/runtime/error-handler.ts +12 -0
- package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +1 -1
- package/src/vite-plugin-upstart-editor/runtime/index.ts +39 -5
- package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +518 -141
- package/src/vite-plugin-upstart-editor/runtime/types.ts +18 -4
- package/src/vite-plugin-upstart-theme.ts +0 -3
- package/src/vite-plugin-upstart-editor/PLAN.md +0 -1391
|
@@ -42,7 +42,6 @@ interface LoopContext {
|
|
|
42
42
|
arrayExpr: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
45
|
interface I18nKeyInfo {
|
|
47
46
|
/** The resolved key with namespace, e.g. "dashboard:features.title" */
|
|
48
47
|
fullKey: string;
|
|
@@ -50,6 +49,10 @@ interface I18nKeyInfo {
|
|
|
50
49
|
key: string;
|
|
51
50
|
/** The namespace, e.g. "dashboard" */
|
|
52
51
|
namespace: string;
|
|
52
|
+
/** Source expression for i18nKey when dynamic (e.g. "stat.labelKey"), null when static */
|
|
53
|
+
keyExpr: string | null;
|
|
54
|
+
/** Source expression for namespace when dynamic, null when static */
|
|
55
|
+
nsExpr: string | null;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
// Registry entry for editable elements (text and className)
|
|
@@ -87,6 +90,10 @@ interface TransformState {
|
|
|
87
90
|
code: string;
|
|
88
91
|
s: MagicString;
|
|
89
92
|
loopStack: LoopContext[];
|
|
93
|
+
/** Tracks const varName = "literal" declarations for resolving dynamic i18nKey expressions */
|
|
94
|
+
constants: Map<string, string>;
|
|
95
|
+
/** Maps t-function variable names to their default namespace, e.g. { "t": "dashboard" } */
|
|
96
|
+
tFunctions: Map<string, string>;
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
export const upstartEditor = createUnplugin<Options>((options) => {
|
|
@@ -173,6 +180,8 @@ export function transformWithOxc(code: string, filePath: string) {
|
|
|
173
180
|
code,
|
|
174
181
|
s,
|
|
175
182
|
loopStack: [],
|
|
183
|
+
constants: new Map(),
|
|
184
|
+
tFunctions: new Map(),
|
|
176
185
|
};
|
|
177
186
|
|
|
178
187
|
let modified = false;
|
|
@@ -180,9 +189,21 @@ export function transformWithOxc(code: string, filePath: string) {
|
|
|
180
189
|
// Use zimmerframe to walk and transform the AST
|
|
181
190
|
walk(ast.program as Program, state, {
|
|
182
191
|
_(node: Node, { state, next }: { state: TransformState; next: () => void }) {
|
|
183
|
-
//
|
|
192
|
+
// Track useTranslation() calls to resolve t() function namespaces
|
|
184
193
|
if (node.type === "VariableDeclaration") {
|
|
185
|
-
|
|
194
|
+
trackUseTranslation(node, state);
|
|
195
|
+
|
|
196
|
+
// Track const varName = "literal" for resolving dynamic i18nKey expressions
|
|
197
|
+
for (const decl of (node as any).declarations) {
|
|
198
|
+
if (
|
|
199
|
+
decl.type === "VariableDeclarator" &&
|
|
200
|
+
decl.id?.type === "Identifier" &&
|
|
201
|
+
decl.init?.type === "Literal" &&
|
|
202
|
+
typeof decl.init.value === "string"
|
|
203
|
+
) {
|
|
204
|
+
state.constants.set(decl.id.name, decl.init.value);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
186
207
|
}
|
|
187
208
|
|
|
188
209
|
// Handle .map() calls to track loops (must be before JSXElement)
|
|
@@ -207,6 +228,15 @@ export function transformWithOxc(code: string, filePath: string) {
|
|
|
207
228
|
const jsxNode = node as JSXElement;
|
|
208
229
|
const opening = jsxNode.openingElement;
|
|
209
230
|
const tagName = getJSXElementName(opening);
|
|
231
|
+
|
|
232
|
+
// Skip ALL attribute injection for <Trans> elements.
|
|
233
|
+
// <Trans> renders to a text node at runtime, so DOM attributes are lost.
|
|
234
|
+
// The i18n attributes are promoted to the parent element instead.
|
|
235
|
+
if (tagName === "Trans") {
|
|
236
|
+
next();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
210
240
|
const insertPos = getAttributeInsertPosition(opening, state.code);
|
|
211
241
|
|
|
212
242
|
// Track attributes to inject
|
|
@@ -217,13 +247,52 @@ export function transformWithOxc(code: string, filePath: string) {
|
|
|
217
247
|
if (hasRange(jsxNode)) {
|
|
218
248
|
const elementSource = state.code.slice(jsxNode.start, jsxNode.end);
|
|
219
249
|
const hash = hashContent(elementSource);
|
|
220
|
-
|
|
250
|
+
|
|
251
|
+
// Collect index variables from all enclosing loops so elements
|
|
252
|
+
// inside .map() get a unique hash per iteration at runtime.
|
|
253
|
+
const loopIndices = state.loopStack.map((l) => l.indexName).filter((n): n is string => n !== null);
|
|
254
|
+
|
|
255
|
+
if (loopIndices.length > 0) {
|
|
256
|
+
const suffix = loopIndices.map((n) => `\${${n}}`).join("-");
|
|
257
|
+
attributes.push(`data-upstart-hash={\`${hash}-${suffix}\`}`);
|
|
258
|
+
} else {
|
|
259
|
+
attributes.push(`data-upstart-hash="${hash}"`);
|
|
260
|
+
}
|
|
221
261
|
}
|
|
222
262
|
|
|
223
|
-
//
|
|
224
|
-
|
|
263
|
+
// --- Editable text detection ---
|
|
264
|
+
// Priority: i18n (true) > non-i18n text (false) > no text (no attribute)
|
|
265
|
+
|
|
266
|
+
// Step 1: Check for Trans children AND t() calls (editable via i18n, gets "true")
|
|
267
|
+
const transChildren = findTransInChildren(jsxNode, state.code, state.constants);
|
|
268
|
+
const tCallChildren = findTCallsInChildren(jsxNode, state.code, state);
|
|
269
|
+
const allI18nKeys = [...transChildren, ...tCallChildren];
|
|
270
|
+
const hasI18n = allI18nKeys.length > 0;
|
|
271
|
+
|
|
272
|
+
if (hasI18n) {
|
|
225
273
|
attributes.push('data-upstart-editable-text="true"');
|
|
226
274
|
|
|
275
|
+
const hasDynamic = allI18nKeys.some((t) => t.keyExpr || t.nsExpr);
|
|
276
|
+
if (hasDynamic) {
|
|
277
|
+
// Build a runtime JSX template expression for dynamic i18n keys
|
|
278
|
+
const parts = allI18nKeys.map((t) => {
|
|
279
|
+
const nsPart = t.nsExpr ? `\${${t.nsExpr}}` : t.namespace;
|
|
280
|
+
const keyPart = t.keyExpr ? `\${${t.keyExpr}}` : t.key;
|
|
281
|
+
return `${nsPart}:${keyPart}`;
|
|
282
|
+
});
|
|
283
|
+
attributes.push(`data-upstart-i18n={\`${parts.join(",")}\`}`);
|
|
284
|
+
} else {
|
|
285
|
+
const keys = allI18nKeys.map((t: I18nKeyInfo) => escapeProp(t.fullKey)).join(",");
|
|
286
|
+
attributes.push(`data-upstart-i18n="${keys}"`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Step 2: Text leaf elements — registry tracking + non-editable flag
|
|
291
|
+
if (isTextLeafElement(jsxNode)) {
|
|
292
|
+
if (!hasI18n) {
|
|
293
|
+
attributes.push('data-upstart-editable-text="false"');
|
|
294
|
+
}
|
|
295
|
+
|
|
227
296
|
// Find the actual JSXText node to track its location
|
|
228
297
|
const textChild = jsxNode.children.find((c) => c.type === "JSXText" && (c as any).value?.trim());
|
|
229
298
|
|
|
@@ -247,12 +316,9 @@ export function transformWithOxc(code: string, filePath: string) {
|
|
|
247
316
|
attributes.push(`data-upstart-id="${id}"`);
|
|
248
317
|
}
|
|
249
318
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (transInfo) {
|
|
254
|
-
attributes.push('data-upstart-editable-text="true"');
|
|
255
|
-
attributes.push(`data-i18n-key="${escapeProp(transInfo.fullKey)}"`);
|
|
319
|
+
// Step 3: Non-leaf elements with visible text content (expressions, mixed content)
|
|
320
|
+
else if (!hasI18n && hasVisibleTextContent(jsxNode)) {
|
|
321
|
+
attributes.push('data-upstart-editable-text="false"');
|
|
256
322
|
}
|
|
257
323
|
|
|
258
324
|
// Track className attribute if it's a string literal
|
|
@@ -542,15 +608,15 @@ function extractRecordId(expr: MemberExpression): string | undefined {
|
|
|
542
608
|
const obj = expr.object;
|
|
543
609
|
|
|
544
610
|
if (obj.type === "Identifier") {
|
|
545
|
-
// Assume the ID field is `${object}
|
|
546
|
-
return `${obj.name}
|
|
611
|
+
// Assume the ID field is `${object}.$id`
|
|
612
|
+
return `${obj.name}.$id`;
|
|
547
613
|
}
|
|
548
614
|
|
|
549
615
|
if (obj.type === "MemberExpression") {
|
|
550
616
|
// For nested objects like user.profile.id
|
|
551
617
|
const datasource = extractDatasource(obj);
|
|
552
618
|
if (datasource) {
|
|
553
|
-
return `${datasource}
|
|
619
|
+
return `${datasource}.$id`;
|
|
554
620
|
}
|
|
555
621
|
}
|
|
556
622
|
|
|
@@ -611,9 +677,8 @@ function escapeProp(value: string): string {
|
|
|
611
677
|
.replace(/>/g, ">");
|
|
612
678
|
}
|
|
613
679
|
|
|
614
|
-
// Helper:
|
|
615
|
-
|
|
616
|
-
function checkForbiddenUseTranslation(node: Node, filePath: string): void {
|
|
680
|
+
// Helper: Track useTranslation() calls to map t-function variable names to their namespace
|
|
681
|
+
function trackUseTranslation(node: Node, state: TransformState): void {
|
|
617
682
|
if (node.type !== "VariableDeclaration") return;
|
|
618
683
|
|
|
619
684
|
const decl = node as any;
|
|
@@ -631,56 +696,295 @@ function checkForbiddenUseTranslation(node: Node, filePath: string): void {
|
|
|
631
696
|
continue;
|
|
632
697
|
}
|
|
633
698
|
|
|
634
|
-
|
|
699
|
+
const namespace = extractUseTranslationNamespace(callExpr);
|
|
700
|
+
|
|
635
701
|
for (const prop of declarator.id.properties) {
|
|
636
702
|
if (prop.type !== "Property") continue;
|
|
703
|
+
if (prop.key?.type !== "Identifier" || prop.key.name !== "t") continue;
|
|
637
704
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
705
|
+
// Handle aliased destructuring: { t: translate } → maps "translate" to namespace
|
|
706
|
+
const localName = prop.value?.type === "Identifier" ? prop.value.name : "t";
|
|
707
|
+
state.tFunctions.set(localName, namespace);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Helper: Extract namespace from useTranslation() call arguments
|
|
713
|
+
function extractUseTranslationNamespace(callExpr: any): string {
|
|
714
|
+
const firstArg = callExpr.arguments?.[0];
|
|
715
|
+
if (!firstArg) return "translation";
|
|
716
|
+
|
|
717
|
+
// String literal: useTranslation("dashboard")
|
|
718
|
+
if (firstArg.type === "Literal" && typeof firstArg.value === "string") {
|
|
719
|
+
return firstArg.value;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Array expression: useTranslation(["dashboard", "common"])
|
|
723
|
+
if (firstArg.type === "ArrayExpression" && firstArg.elements?.length > 0) {
|
|
724
|
+
const first = firstArg.elements[0];
|
|
725
|
+
if (first?.type === "Literal" && typeof first.value === "string") {
|
|
726
|
+
return first.value;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return "translation";
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Helper: Resolve a JSX attribute value to a string.
|
|
734
|
+
// Handles string literals directly, and JSXExpressionContainer by resolving
|
|
735
|
+
// Identifier references via the constants map, or returning the expression source for dynamic values.
|
|
736
|
+
function resolveJSXAttrValue(
|
|
737
|
+
value: any,
|
|
738
|
+
code: string,
|
|
739
|
+
constants: Map<string, string>,
|
|
740
|
+
): { value: string; expr: string | null } | null {
|
|
741
|
+
if (!value) return null;
|
|
742
|
+
|
|
743
|
+
if (value.type === "Literal" && typeof value.value === "string") {
|
|
744
|
+
return { value: value.value, expr: null };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (value.type === "JSXExpressionContainer") {
|
|
748
|
+
const expression = value.expression;
|
|
749
|
+
if (!expression) return null;
|
|
750
|
+
|
|
751
|
+
// Resolve simple identifiers via constants map (e.g. i18nKey={labelKey} where const labelKey = "...")
|
|
752
|
+
if (expression.type === "Identifier" && constants.has(expression.name)) {
|
|
753
|
+
return { value: constants.get(expression.name)!, expr: null };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Dynamic expression — return source text so caller can emit a runtime JSX expression
|
|
757
|
+
if (hasRange(expression)) {
|
|
758
|
+
const src = code.slice(expression.start, expression.end);
|
|
759
|
+
return { value: src, expr: src };
|
|
645
760
|
}
|
|
646
761
|
}
|
|
762
|
+
|
|
763
|
+
return null;
|
|
647
764
|
}
|
|
648
765
|
|
|
649
766
|
// Helper: Detect <Trans i18nKey="..." /> component and extract i18n key info
|
|
650
|
-
function detectTransComponent(
|
|
767
|
+
function detectTransComponent(
|
|
768
|
+
jsxElement: JSXElement,
|
|
769
|
+
code: string,
|
|
770
|
+
constants: Map<string, string>,
|
|
771
|
+
): I18nKeyInfo | null {
|
|
651
772
|
const opening = jsxElement.openingElement;
|
|
652
773
|
const tagName = getJSXElementName(opening);
|
|
653
774
|
|
|
654
775
|
if (tagName !== "Trans") return null;
|
|
655
776
|
|
|
656
|
-
let
|
|
657
|
-
let
|
|
777
|
+
let keyResult: { value: string; expr: string | null } | null = null;
|
|
778
|
+
let nsResult: { value: string; expr: string | null } | null = null;
|
|
658
779
|
|
|
659
780
|
for (const attr of opening.attributes) {
|
|
660
781
|
if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
|
|
661
782
|
|
|
662
783
|
const attrName = attr.name.name;
|
|
663
784
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
i18nKey = (attr.value as any).value as string;
|
|
785
|
+
if (attrName === "i18nKey") {
|
|
786
|
+
keyResult = resolveJSXAttrValue(attr.value, code, constants);
|
|
667
787
|
}
|
|
668
788
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
namespace = (attr.value as any).value as string;
|
|
789
|
+
if (attrName === "ns") {
|
|
790
|
+
nsResult = resolveJSXAttrValue(attr.value, code, constants);
|
|
672
791
|
}
|
|
673
792
|
}
|
|
674
793
|
|
|
675
|
-
if (!
|
|
794
|
+
if (!keyResult) {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const key = keyResult.value;
|
|
799
|
+
const namespace = nsResult?.value ?? "translation";
|
|
800
|
+
const keyExpr = keyResult.expr;
|
|
801
|
+
const nsExpr = nsResult?.expr ?? null;
|
|
676
802
|
|
|
677
803
|
return {
|
|
678
|
-
fullKey: `${namespace}:${
|
|
679
|
-
key
|
|
804
|
+
fullKey: `${namespace}:${key}`,
|
|
805
|
+
key,
|
|
680
806
|
namespace,
|
|
807
|
+
keyExpr,
|
|
808
|
+
nsExpr,
|
|
681
809
|
};
|
|
682
810
|
}
|
|
683
811
|
|
|
812
|
+
// Helper: Scan an element's direct children for <Trans> components and extract i18n key info.
|
|
813
|
+
// Handles direct <Trans> children and <Trans> inside JSXExpressionContainer (e.g. {show && <Trans>}).
|
|
814
|
+
function findTransInChildren(
|
|
815
|
+
jsxElement: JSXElement,
|
|
816
|
+
code: string,
|
|
817
|
+
constants: Map<string, string>,
|
|
818
|
+
): I18nKeyInfo[] {
|
|
819
|
+
const results: I18nKeyInfo[] = [];
|
|
820
|
+
|
|
821
|
+
for (const child of jsxElement.children) {
|
|
822
|
+
if (child.type === "JSXElement") {
|
|
823
|
+
const info = detectTransComponent(child as JSXElement, code, constants);
|
|
824
|
+
if (info) results.push(info);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (child.type === "JSXExpressionContainer") {
|
|
828
|
+
const expr = (child as any).expression;
|
|
829
|
+
if (expr && expr.type !== "JSXEmptyExpression") {
|
|
830
|
+
findTransInExpression(expr, code, results, constants);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return results;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Helper: Recursively search an expression tree for <Trans> JSXElements.
|
|
839
|
+
// Handles LogicalExpression (&&, ||), ConditionalExpression (?:), CallExpression (.map()),
|
|
840
|
+
// ArrowFunctionExpression/FunctionExpression (callbacks), BlockStatement, and direct JSXElement.
|
|
841
|
+
function findTransInExpression(
|
|
842
|
+
expr: any,
|
|
843
|
+
code: string,
|
|
844
|
+
results: I18nKeyInfo[],
|
|
845
|
+
constants: Map<string, string>,
|
|
846
|
+
): void {
|
|
847
|
+
if (!expr || !expr.type) return;
|
|
848
|
+
|
|
849
|
+
if (expr.type === "JSXElement") {
|
|
850
|
+
const info = detectTransComponent(expr as JSXElement, code, constants);
|
|
851
|
+
if (info) results.push(info);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (expr.type === "LogicalExpression") {
|
|
856
|
+
findTransInExpression(expr.left, code, results, constants);
|
|
857
|
+
findTransInExpression(expr.right, code, results, constants);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (expr.type === "ConditionalExpression") {
|
|
862
|
+
findTransInExpression(expr.consequent, code, results, constants);
|
|
863
|
+
findTransInExpression(expr.alternate, code, results, constants);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Handle .map() and other call expressions — recurse into arguments
|
|
868
|
+
if (expr.type === "CallExpression") {
|
|
869
|
+
for (const arg of expr.arguments) {
|
|
870
|
+
findTransInExpression(arg, code, results, constants);
|
|
871
|
+
}
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Handle arrow/function expressions — recurse into body
|
|
876
|
+
if (expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") {
|
|
877
|
+
findTransInExpression(expr.body, code, results, constants);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Handle block bodies (arrow functions with braces)
|
|
882
|
+
if (expr.type === "BlockStatement") {
|
|
883
|
+
for (const stmt of expr.body) {
|
|
884
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
885
|
+
findTransInExpression(stmt.argument, code, results, constants);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Helper: Scan an element's direct children for t() function calls and extract i18n key info.
|
|
893
|
+
function findTCallsInChildren(jsxElement: JSXElement, code: string, state: TransformState): I18nKeyInfo[] {
|
|
894
|
+
if (state.tFunctions.size === 0) return [];
|
|
895
|
+
|
|
896
|
+
const results: I18nKeyInfo[] = [];
|
|
897
|
+
|
|
898
|
+
for (const child of jsxElement.children) {
|
|
899
|
+
if (child.type === "JSXExpressionContainer") {
|
|
900
|
+
const expr = (child as any).expression;
|
|
901
|
+
if (expr && expr.type !== "JSXEmptyExpression") {
|
|
902
|
+
findTCallInExpression(expr, code, results, state);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return results;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Helper: Recursively search an expression tree for t() calls.
|
|
911
|
+
// Handles LogicalExpression (&&, ||), ConditionalExpression (?:), CallExpression (.map()),
|
|
912
|
+
// ArrowFunctionExpression/FunctionExpression (callbacks), BlockStatement, and direct CallExpression.
|
|
913
|
+
function findTCallInExpression(expr: any, code: string, results: I18nKeyInfo[], state: TransformState): void {
|
|
914
|
+
if (!expr || !expr.type) return;
|
|
915
|
+
|
|
916
|
+
if (expr.type === "CallExpression") {
|
|
917
|
+
const info = detectTCall(expr, code, state);
|
|
918
|
+
if (info) {
|
|
919
|
+
results.push(info);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
// Not a t() call — recurse into arguments (for .map() callbacks etc.)
|
|
923
|
+
for (const arg of expr.arguments) {
|
|
924
|
+
findTCallInExpression(arg, code, results, state);
|
|
925
|
+
}
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (expr.type === "LogicalExpression") {
|
|
930
|
+
findTCallInExpression(expr.left, code, results, state);
|
|
931
|
+
findTCallInExpression(expr.right, code, results, state);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (expr.type === "ConditionalExpression") {
|
|
936
|
+
findTCallInExpression(expr.consequent, code, results, state);
|
|
937
|
+
findTCallInExpression(expr.alternate, code, results, state);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") {
|
|
942
|
+
findTCallInExpression(expr.body, code, results, state);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (expr.type === "BlockStatement") {
|
|
947
|
+
for (const stmt of expr.body) {
|
|
948
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
949
|
+
findTCallInExpression(stmt.argument, code, results, state);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Helper: Detect a t("key") call and extract i18n key info.
|
|
957
|
+
function detectTCall(callExpr: any, code: string, state: TransformState): I18nKeyInfo | null {
|
|
958
|
+
if (callExpr.callee?.type !== "Identifier") return null;
|
|
959
|
+
|
|
960
|
+
const calleeName = callExpr.callee.name;
|
|
961
|
+
const namespace = state.tFunctions.get(calleeName);
|
|
962
|
+
if (namespace === undefined) return null;
|
|
963
|
+
|
|
964
|
+
const firstArg = callExpr.arguments?.[0];
|
|
965
|
+
if (!firstArg) return null;
|
|
966
|
+
|
|
967
|
+
// Static string key: t("features.title")
|
|
968
|
+
if (firstArg.type === "Literal" && typeof firstArg.value === "string") {
|
|
969
|
+
const key = firstArg.value;
|
|
970
|
+
return { fullKey: `${namespace}:${key}`, key, namespace, keyExpr: null, nsExpr: null };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Dynamic key — try constant resolution first, then fall back to expression
|
|
974
|
+
if (hasRange(firstArg)) {
|
|
975
|
+
const exprSrc = code.slice(firstArg.start, firstArg.end);
|
|
976
|
+
|
|
977
|
+
if (firstArg.type === "Identifier" && state.constants.has(firstArg.name)) {
|
|
978
|
+
const key = state.constants.get(firstArg.name)!;
|
|
979
|
+
return { fullKey: `${namespace}:${key}`, key, namespace, keyExpr: null, nsExpr: null };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return { fullKey: `${namespace}:${exprSrc}`, key: exprSrc, namespace, keyExpr: exprSrc, nsExpr: null };
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
|
|
684
988
|
// Helper: Check if element is a "leaf" with only static text (no nested elements or expressions)
|
|
685
989
|
function isTextLeafElement(jsxElement: JSXElement): boolean {
|
|
686
990
|
let hasText = false;
|
|
@@ -705,4 +1009,38 @@ function isTextLeafElement(jsxElement: JSXElement): boolean {
|
|
|
705
1009
|
return hasText;
|
|
706
1010
|
}
|
|
707
1011
|
|
|
1012
|
+
// Helper: Check if element has any visible text content (static or dynamic).
|
|
1013
|
+
// Broader than isTextLeafElement — returns true even if the element has other child types.
|
|
1014
|
+
// Detects JSXText with non-whitespace content and expression containers with
|
|
1015
|
+
// text-producing expressions (Identifier, MemberExpression, Literal, TemplateLiteral).
|
|
1016
|
+
// Skips CallExpression, ConditionalExpression, LogicalExpression (these typically produce elements).
|
|
1017
|
+
function hasVisibleTextContent(jsxElement: JSXElement): boolean {
|
|
1018
|
+
for (const child of jsxElement.children) {
|
|
1019
|
+
if (child.type === "JSXText") {
|
|
1020
|
+
const textValue = (child as any).value;
|
|
1021
|
+
if (textValue?.trim()) {
|
|
1022
|
+
return true;
|
|
1023
|
+
}
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (child.type === "JSXExpressionContainer") {
|
|
1028
|
+
const expr = (child as any).expression;
|
|
1029
|
+
if (!expr || expr.type === "JSXEmptyExpression") {
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
if (
|
|
1033
|
+
expr.type === "Identifier" ||
|
|
1034
|
+
expr.type === "MemberExpression" ||
|
|
1035
|
+
expr.type === "Literal" ||
|
|
1036
|
+
expr.type === "TemplateLiteral"
|
|
1037
|
+
) {
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
708
1046
|
export default upstartEditor.vite;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { createUnplugin } from "unplugin";
|
|
4
|
+
import type { UpstartBrandingPluginOptions } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_OPTIONS: Required<UpstartBrandingPluginOptions> = {
|
|
7
|
+
enabled: false,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Upstart Branding Vite plugin (build-time)
|
|
12
|
+
*
|
|
13
|
+
* Injects a "Made with Upstart" badge into the app entry during build.
|
|
14
|
+
* The badge appears at the bottom of the page, then fades out after a
|
|
15
|
+
* few seconds or when the user scrolls.
|
|
16
|
+
*/
|
|
17
|
+
export const upstartBranding = createUnplugin<UpstartBrandingPluginOptions>((options = {}) => {
|
|
18
|
+
const { enabled } = { ...DEFAULT_OPTIONS, ...options };
|
|
19
|
+
|
|
20
|
+
if (!enabled) {
|
|
21
|
+
return { name: "upstart-branding-disabled" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const runtimePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "./runtime");
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
name: "upstart-branding",
|
|
28
|
+
enforce: "pre",
|
|
29
|
+
|
|
30
|
+
transform(code, id) {
|
|
31
|
+
const [cleanId] = id.split("?");
|
|
32
|
+
if (!cleanId || cleanId.includes("node_modules")) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!/\.(t|j)sx?$/.test(cleanId)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!cleanId.includes("entry.client.tsx")) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (code.includes("initUpstartBranding")) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const imports = `import { initUpstartBranding } from ${JSON.stringify(runtimePath)};`;
|
|
49
|
+
const injection = "requestIdleCallback(initUpstartBranding);";
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
code: `${imports}${code}${injection}`,
|
|
53
|
+
map: null,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export default upstartBranding.vite;
|