meno-core 1.0.49 → 1.0.51
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/build-astro.ts +6 -2
- package/build-static.ts +8 -1
- package/dist/bin/cli.js +1 -1
- package/dist/build-static.js +5 -5
- package/dist/chunks/{chunk-KPU2XHOS.js → chunk-2MHDV5BF.js} +11 -1
- package/dist/chunks/chunk-2MHDV5BF.js.map +7 -0
- package/dist/chunks/{chunk-JER5NQVM.js → chunk-3KJ6SJZC.js} +5 -5
- package/dist/chunks/{chunk-JER5NQVM.js.map → chunk-3KJ6SJZC.js.map} +2 -2
- package/dist/chunks/{chunk-S2CX6HFM.js → chunk-7NIC4I3V.js} +42 -20
- package/dist/chunks/chunk-7NIC4I3V.js.map +7 -0
- package/dist/chunks/{chunk-EQYDSPBB.js → chunk-DM54NPEC.js} +114 -31
- package/dist/chunks/chunk-DM54NPEC.js.map +7 -0
- package/dist/chunks/{chunk-LKAGAQ3M.js → chunk-EDQSMAMP.js} +13 -2
- package/dist/chunks/{chunk-LKAGAQ3M.js.map → chunk-EDQSMAMP.js.map} +2 -2
- package/dist/chunks/{chunk-4OFZP5NQ.js → chunk-HNLUO36W.js} +15 -4
- package/dist/chunks/chunk-HNLUO36W.js.map +7 -0
- package/dist/chunks/{chunk-6IVUG7FY.js → chunk-LPVETICS.js} +19 -2
- package/dist/chunks/{chunk-6IVUG7FY.js.map → chunk-LPVETICS.js.map} +2 -2
- package/dist/chunks/{chunk-CHD5UCFF.js → chunk-V7CD7V7W.js} +149 -46
- package/dist/chunks/chunk-V7CD7V7W.js.map +7 -0
- package/dist/chunks/{configService-CCA6AIDI.js → configService-R3OGU2UD.js} +2 -2
- package/dist/entries/server-router.js +5 -5
- package/dist/lib/client/index.js +41 -15
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +12 -10
- package/dist/lib/server/index.js.map +2 -2
- package/dist/lib/shared/index.js +2 -2
- package/lib/client/core/ComponentBuilder.test.ts +34 -0
- package/lib/client/core/ComponentBuilder.ts +25 -3
- package/lib/client/core/builders/embedBuilder.ts +13 -5
- package/lib/client/core/builders/linkNodeBuilder.ts +13 -5
- package/lib/client/core/builders/localeListBuilder.ts +13 -5
- package/lib/client/templateEngine.ts +24 -0
- package/lib/server/fileWatcher.test.ts +134 -0
- package/lib/server/fileWatcher.ts +100 -32
- package/lib/server/jsonLoader.ts +1 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +46 -14
- package/lib/server/routes/pages.ts +37 -2
- package/lib/server/services/cmsService.ts +21 -0
- package/lib/server/services/configService.ts +21 -0
- package/lib/server/services/fileWatcherService.ts +17 -0
- package/lib/server/ssr/buildErrorOverlay.ts +22 -4
- package/lib/server/ssr/errorOverlay.ts +11 -3
- package/lib/server/ssr/htmlGenerator.nonce.test.ts +165 -0
- package/lib/server/ssr/htmlGenerator.ts +36 -9
- package/lib/server/ssr/liveReloadIntegration.test.ts +3 -1
- package/lib/server/ssr/metaTagGenerator.ts +35 -5
- package/lib/server/ssr/ssrRenderer.test.ts +258 -0
- package/lib/server/ssr/ssrRenderer.ts +47 -5
- package/lib/server/ssrRenderer.test.ts +87 -2
- package/lib/server/webflow/buildWebflow.ts +1 -1
- package/lib/server/websocketManager.test.ts +61 -6
- package/lib/server/websocketManager.ts +25 -1
- package/lib/shared/cssProperties.test.ts +28 -0
- package/lib/shared/cssProperties.ts +27 -1
- package/lib/shared/types/api.ts +10 -1
- package/lib/shared/types/cms.ts +18 -9
- package/lib/shared/validation/schemas.test.ts +93 -0
- package/lib/shared/validation/schemas.ts +56 -15
- package/package.json +1 -1
- package/dist/chunks/chunk-4OFZP5NQ.js.map +0 -7
- package/dist/chunks/chunk-CHD5UCFF.js.map +0 -7
- package/dist/chunks/chunk-EQYDSPBB.js.map +0 -7
- package/dist/chunks/chunk-KPU2XHOS.js.map +0 -7
- package/dist/chunks/chunk-S2CX6HFM.js.map +0 -7
- /package/dist/chunks/{configService-CCA6AIDI.js.map → configService-R3OGU2UD.js.map} +0 -0
|
@@ -137,15 +137,100 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
137
137
|
ogImage: "https://example.com/image.jpg",
|
|
138
138
|
ogType: "article"
|
|
139
139
|
};
|
|
140
|
-
|
|
140
|
+
|
|
141
141
|
const tags = generateMetaTags(meta);
|
|
142
|
-
|
|
142
|
+
|
|
143
143
|
expect(tags).toContain(`<meta property="og:title" content="OG Title" />`);
|
|
144
144
|
expect(tags).toContain(`<meta property="og:description" content="OG Description" />`);
|
|
145
145
|
expect(tags).toContain(`<meta property="og:image" content="https://example.com/image.jpg" />`);
|
|
146
146
|
expect(tags).toContain(`<meta property="og:type" content="article" />`);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
test("should emit twitter:card=summary_large_image when ogImage present", () => {
|
|
150
|
+
const meta = {
|
|
151
|
+
ogTitle: "OG Title",
|
|
152
|
+
ogDescription: "OG Description",
|
|
153
|
+
ogImage: "https://example.com/image.jpg",
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const tags = generateMetaTags(meta);
|
|
157
|
+
|
|
158
|
+
expect(tags).toContain(`<meta name="twitter:card" content="summary_large_image" />`);
|
|
159
|
+
expect(tags).toContain(`<meta name="twitter:title" content="OG Title" />`);
|
|
160
|
+
expect(tags).toContain(`<meta name="twitter:description" content="OG Description" />`);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("should emit twitter:card=summary when no ogImage", () => {
|
|
164
|
+
const meta = {
|
|
165
|
+
title: "Plain Title",
|
|
166
|
+
description: "Plain Description",
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const tags = generateMetaTags(meta);
|
|
170
|
+
|
|
171
|
+
expect(tags).toContain(`<meta name="twitter:card" content="summary" />`);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("twitter:title falls back to title when ogTitle is absent", () => {
|
|
175
|
+
const meta = {
|
|
176
|
+
title: "Plain Title",
|
|
177
|
+
ogImage: "https://example.com/image.jpg",
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const tags = generateMetaTags(meta);
|
|
181
|
+
|
|
182
|
+
expect(tags).toContain(`<meta name="twitter:title" content="Plain Title" />`);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("twitter:description falls back to description when ogDescription is absent", () => {
|
|
186
|
+
const meta = {
|
|
187
|
+
description: "Plain Description",
|
|
188
|
+
ogImage: "https://example.com/image.jpg",
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const tags = generateMetaTags(meta);
|
|
192
|
+
|
|
193
|
+
expect(tags).toContain(`<meta name="twitter:description" content="Plain Description" />`);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("should emit twitter:site and twitter:creator with @-normalized handle", () => {
|
|
197
|
+
const meta = { title: "Page" };
|
|
198
|
+
|
|
199
|
+
const tags = generateMetaTags(meta, '', 'en', undefined, {
|
|
200
|
+
social: { twitterHandle: 'meno' },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(tags).toContain(`<meta name="twitter:site" content="@meno" />`);
|
|
204
|
+
expect(tags).toContain(`<meta name="twitter:creator" content="@meno" />`);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("should not double-prefix handle that already starts with @", () => {
|
|
208
|
+
const meta = { title: "Page" };
|
|
209
|
+
|
|
210
|
+
const tags = generateMetaTags(meta, '', 'en', undefined, {
|
|
211
|
+
social: { twitterHandle: '@meno' },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(tags).toContain(`<meta name="twitter:site" content="@meno" />`);
|
|
215
|
+
expect(tags).not.toContain(`@@meno`);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("should emit no twitter tags when meta is empty", () => {
|
|
219
|
+
const tags = generateMetaTags({});
|
|
220
|
+
|
|
221
|
+
expect(tags).not.toContain('twitter:');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("should escape HTML in twitter content", () => {
|
|
225
|
+
const meta = {
|
|
226
|
+
ogTitle: "Title & <stuff>",
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const tags = generateMetaTags(meta);
|
|
230
|
+
|
|
231
|
+
expect(tags).toContain(`<meta name="twitter:title" content="Title & <stuff>" />`);
|
|
232
|
+
});
|
|
233
|
+
|
|
149
234
|
test("should generate canonical URL", () => {
|
|
150
235
|
const meta = {};
|
|
151
236
|
const url = "https://example.com/page";
|
|
@@ -456,7 +456,7 @@ export async function buildWebflowPayload(
|
|
|
456
456
|
// the slug, so a template like `templates/blog.json` lands at
|
|
457
457
|
// `/blog/<first-slug>` and doesn't collide with a sibling listing
|
|
458
458
|
// page like `pages/blog.json`.
|
|
459
|
-
let itemSlug = item[cmsSchema.slugField] ?? item._slug ?? item._id;
|
|
459
|
+
let itemSlug = item[cmsSchema.slugField] ?? item._slug ?? item._filename ?? item._id;
|
|
460
460
|
if (isI18nValue(itemSlug)) {
|
|
461
461
|
itemSlug = resolveI18nValue(itemSlug, locale, i18nConfig) as string;
|
|
462
462
|
}
|
|
@@ -1,9 +1,64 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { WebSocketManager } from './websocketManager';
|
|
3
|
+
import { WEBSOCKET_STATES } from '../shared/constants';
|
|
4
|
+
import type { RuntimeWSClient } from './runtime';
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
function makeMockClient(readyState: number = WEBSOCKET_STATES.OPEN): RuntimeWSClient & { sent: string[] } {
|
|
7
|
+
const sent: string[] = [];
|
|
8
|
+
return {
|
|
9
|
+
readyState,
|
|
10
|
+
send(data) {
|
|
11
|
+
sent.push(typeof data === 'string' ? data : '<binary>');
|
|
12
|
+
},
|
|
13
|
+
close() {},
|
|
14
|
+
sent,
|
|
15
|
+
} as RuntimeWSClient & { sent: string[] };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('WebSocketManager', () => {
|
|
19
|
+
let manager: WebSocketManager;
|
|
20
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
manager = new WebSocketManager();
|
|
24
|
+
client = makeMockClient();
|
|
25
|
+
manager.addClient(client);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('broadcastCollectionsUpdate', () => {
|
|
29
|
+
test('sends hmr:cms-collections-update to OPEN clients', () => {
|
|
30
|
+
manager.broadcastCollectionsUpdate();
|
|
31
|
+
expect(client.sent).toEqual([JSON.stringify({ type: 'hmr:cms-collections-update' })]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('skips clients that are not OPEN', () => {
|
|
35
|
+
const closing = makeMockClient(WEBSOCKET_STATES.CLOSING);
|
|
36
|
+
manager.addClient(closing);
|
|
37
|
+
manager.broadcastCollectionsUpdate();
|
|
38
|
+
expect(client.sent.length).toBe(1);
|
|
39
|
+
expect(closing.sent.length).toBe(0);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('broadcastCMSUpdate', () => {
|
|
44
|
+
test('includes collection in payload', () => {
|
|
45
|
+
manager.broadcastCMSUpdate('blog');
|
|
46
|
+
expect(client.sent).toEqual([JSON.stringify({ type: 'hmr:cms-update', collection: 'blog' })]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('broadcastConfigUpdate', () => {
|
|
51
|
+
test('sends hmr:config-update to OPEN clients', () => {
|
|
52
|
+
manager.broadcastConfigUpdate();
|
|
53
|
+
expect(client.sent).toEqual([JSON.stringify({ type: 'hmr:config-update' })]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('client lifecycle', () => {
|
|
58
|
+
test('removed clients no longer receive broadcasts', () => {
|
|
59
|
+
manager.removeClient(client);
|
|
60
|
+
manager.broadcastCollectionsUpdate();
|
|
61
|
+
expect(client.sent.length).toBe(0);
|
|
62
|
+
});
|
|
8
63
|
});
|
|
9
64
|
});
|
|
@@ -103,7 +103,31 @@ export class WebSocketManager {
|
|
|
103
103
|
collection
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Broadcast CMS collections-list update notification.
|
|
109
|
+
* Emitted when a template file is added, removed, or its schema changes —
|
|
110
|
+
* tells connected clients to re-fetch the collections list.
|
|
111
|
+
*/
|
|
112
|
+
broadcastCollectionsUpdate(): void {
|
|
113
|
+
this.broadcast({
|
|
114
|
+
type: 'hmr:cms-collections-update'
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Broadcast project.config.json update notification.
|
|
120
|
+
* Emitted when project.config.json changes (e.g. an AI tool adds a new
|
|
121
|
+
* locale) — tells connected clients to re-fetch config-derived state
|
|
122
|
+
* such as the i18n locale list.
|
|
123
|
+
*/
|
|
124
|
+
broadcastConfigUpdate(): void {
|
|
125
|
+
this.broadcast({
|
|
126
|
+
type: 'hmr:config-update'
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
107
131
|
/**
|
|
108
132
|
* Get number of connected clients
|
|
109
133
|
*/
|
|
@@ -128,6 +128,34 @@ describe('cssProperties', () => {
|
|
|
128
128
|
const result = filterCSSProperties('fd');
|
|
129
129
|
expect(result).toContain('flexDirection');
|
|
130
130
|
});
|
|
131
|
+
|
|
132
|
+
test('promotes paddingLeft to top for "pl"', () => {
|
|
133
|
+
const result = filterCSSProperties('pl');
|
|
134
|
+
expect(result[0]).toBe('paddingLeft');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('promotes paddingRight to top for "pr"', () => {
|
|
138
|
+
const result = filterCSSProperties('pr');
|
|
139
|
+
expect(result[0]).toBe('paddingRight');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('promotes paddingTop to top for "pt" and "pu"', () => {
|
|
143
|
+
expect(filterCSSProperties('pt')[0]).toBe('paddingTop');
|
|
144
|
+
expect(filterCSSProperties('pu')[0]).toBe('paddingTop');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('promotes paddingBottom to top for "pb"', () => {
|
|
148
|
+
const result = filterCSSProperties('pb');
|
|
149
|
+
expect(result[0]).toBe('paddingBottom');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('promotes marginLeft/Right/Top/Bottom for "ml"/"mr"/"mt"/"mu"/"mb"', () => {
|
|
153
|
+
expect(filterCSSProperties('ml')[0]).toBe('marginLeft');
|
|
154
|
+
expect(filterCSSProperties('mr')[0]).toBe('marginRight');
|
|
155
|
+
expect(filterCSSProperties('mt')[0]).toBe('marginTop');
|
|
156
|
+
expect(filterCSSProperties('mu')[0]).toBe('marginTop');
|
|
157
|
+
expect(filterCSSProperties('mb')[0]).toBe('marginBottom');
|
|
158
|
+
});
|
|
131
159
|
});
|
|
132
160
|
|
|
133
161
|
describe('getPropertyValues', () => {
|
|
@@ -650,6 +650,25 @@ function matchesAbbreviation(property: string, input: string): boolean {
|
|
|
650
650
|
return true;
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
+
/**
|
|
654
|
+
* Direction-abbreviation shortcuts. When the user types one of these exact
|
|
655
|
+
* inputs, the mapped property is forced to the top of the suggestions list
|
|
656
|
+
* (ahead of startsWith matches like `placeContent` for "pl"). "u" stands for
|
|
657
|
+
* "up" — a natural mnemonic for `Top`.
|
|
658
|
+
*/
|
|
659
|
+
const PROPERTY_SHORTCUTS: Record<string, string> = {
|
|
660
|
+
pl: 'paddingLeft',
|
|
661
|
+
pr: 'paddingRight',
|
|
662
|
+
pt: 'paddingTop',
|
|
663
|
+
pu: 'paddingTop',
|
|
664
|
+
pb: 'paddingBottom',
|
|
665
|
+
ml: 'marginLeft',
|
|
666
|
+
mr: 'marginRight',
|
|
667
|
+
mt: 'marginTop',
|
|
668
|
+
mu: 'marginTop',
|
|
669
|
+
mb: 'marginBottom',
|
|
670
|
+
};
|
|
671
|
+
|
|
653
672
|
/**
|
|
654
673
|
* Filter CSS properties based on input value
|
|
655
674
|
* Supports both startsWith matching and camelCase abbreviation matching
|
|
@@ -675,10 +694,17 @@ export function filterCSSProperties(input: string): string[] {
|
|
|
675
694
|
const byPriority = (a: string, b: string) =>
|
|
676
695
|
getPropertyPriority(a) - getPropertyPriority(b);
|
|
677
696
|
|
|
678
|
-
|
|
697
|
+
const combined = [
|
|
679
698
|
...startsWithMatches.sort(byPriority),
|
|
680
699
|
...abbreviationMatches.sort(byPriority),
|
|
681
700
|
];
|
|
701
|
+
|
|
702
|
+
const shortcut = PROPERTY_SHORTCUTS[normalizedInput.toLowerCase()];
|
|
703
|
+
if (shortcut) {
|
|
704
|
+
return [shortcut, ...combined.filter(prop => prop !== shortcut)];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return combined;
|
|
682
708
|
}
|
|
683
709
|
|
|
684
710
|
/**
|
package/lib/shared/types/api.ts
CHANGED
|
@@ -7,7 +7,16 @@ import type { CMSFieldDefinition } from './cms';
|
|
|
7
7
|
import type { PageLibrariesConfig } from './libraries';
|
|
8
8
|
|
|
9
9
|
export interface HMRMessage {
|
|
10
|
-
type:
|
|
10
|
+
type:
|
|
11
|
+
| 'hmr:update'
|
|
12
|
+
| 'hmr:colors-update'
|
|
13
|
+
| 'hmr:variables-update'
|
|
14
|
+
| 'hmr:enums-update'
|
|
15
|
+
| 'hmr:fonts-update'
|
|
16
|
+
| 'hmr:libraries-update'
|
|
17
|
+
| 'hmr:cms-update'
|
|
18
|
+
| 'hmr:cms-collections-update'
|
|
19
|
+
| 'hmr:config-update';
|
|
11
20
|
path?: string;
|
|
12
21
|
collection?: string; // For CMS updates
|
|
13
22
|
}
|
package/lib/shared/types/cms.ts
CHANGED
|
@@ -73,24 +73,33 @@ export interface CMSSchema {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* CMS Item - actual content entry (stored as individual JSON file)
|
|
76
|
+
* CMS Item - actual content entry (stored as individual JSON file).
|
|
77
77
|
*
|
|
78
|
-
*
|
|
79
|
-
* -
|
|
80
|
-
* -
|
|
78
|
+
* Identity model:
|
|
79
|
+
* - `_id` is the canonical identifier. For new items written by the editor, it
|
|
80
|
+
* equals the on-disk filename's stem (slug-shaped, human-readable). For
|
|
81
|
+
* legacy items it may be a custom value (e.g. `"post-001"`) that differs
|
|
82
|
+
* from the filename — those continue to work; reference resolution accepts
|
|
83
|
+
* either `_id` or the on-disk filename as a key.
|
|
84
|
+
* - `_filename` is a legacy alias kept for back-compat. The provider's
|
|
85
|
+
* `normalizeItem` always sets it on read to the on-disk filename's stem, so
|
|
86
|
+
* internal file-routing code can rely on it. New items do not need to
|
|
87
|
+
* persist `_filename` to disk — write only `_id`.
|
|
88
|
+
* - `_slug` is the oldest identifier alias, fully deprecated. Use `_id`.
|
|
81
89
|
*/
|
|
82
90
|
export interface CMSItem {
|
|
83
|
-
/**
|
|
91
|
+
/** Canonical identifier. For new items, equals the on-disk filename's stem. */
|
|
84
92
|
_id: string;
|
|
85
93
|
/**
|
|
86
|
-
* @deprecated Use
|
|
94
|
+
* @deprecated Use _id. Kept for backward compatibility.
|
|
87
95
|
* Derived from the filename on disk.
|
|
88
96
|
*/
|
|
89
97
|
_slug?: string;
|
|
90
98
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
99
|
+
* @deprecated Legacy alias for _id. Always equals the on-disk filename's
|
|
100
|
+
* stem; populated automatically by the file-system provider on read.
|
|
101
|
+
* Internal file-routing code reads this for legacy items where the on-disk
|
|
102
|
+
* filename differs from `_id`. New items do not need to write this to disk.
|
|
94
103
|
*/
|
|
95
104
|
_filename?: string;
|
|
96
105
|
/** ISO timestamp when item was created */
|
|
@@ -28,6 +28,99 @@ describe('Schema Validation', () => {
|
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
test('prop default accepts an _i18n value object', () => {
|
|
32
|
+
// Regression: the SSR pipeline resolves `_i18n` objects in prop defaults
|
|
33
|
+
// (see resolveI18nInProps) and CLAUDE.md tells AI to write them. The
|
|
34
|
+
// schema must accept this shape — otherwise the editor's component-load
|
|
35
|
+
// route rejects the file with a misleading JSONPage error.
|
|
36
|
+
const propDef = {
|
|
37
|
+
type: 'string',
|
|
38
|
+
default: { _i18n: true, en: 'Hello', pl: 'Cześć' },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const result = PropDefinitionSchema.safeParse(propDef);
|
|
42
|
+
expect(result.success).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('list prop default accepts _i18n value objects on item fields', () => {
|
|
46
|
+
const propDef = {
|
|
47
|
+
type: 'list',
|
|
48
|
+
itemSchema: { title: { type: 'string' } },
|
|
49
|
+
default: [
|
|
50
|
+
{ title: { _i18n: true, en: 'Hello', pl: 'Cześć' } },
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const result = PropDefinitionSchema.safeParse(propDef);
|
|
55
|
+
expect(result.success).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('HTML node accepts an _i18n object as direct children', () => {
|
|
59
|
+
// Regression: previously the validator rejected `{type: "node", tag:
|
|
60
|
+
// "h1", children: {_i18n: true, en: "Blog", pl: "Blog"}}` with a
|
|
61
|
+
// misleading JSONPage error. After widening ComponentNodeSchema this
|
|
62
|
+
// shape validates and the runtime resolves it to a locale-string.
|
|
63
|
+
const page = {
|
|
64
|
+
root: {
|
|
65
|
+
type: 'node',
|
|
66
|
+
tag: 'h1',
|
|
67
|
+
children: { _i18n: true, en: 'Blog', pl: 'Blog' },
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
const result = PageDataSchema.safeParse(page);
|
|
71
|
+
expect(result.success).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('HTML node accepts an _i18n object inside attribute values', () => {
|
|
75
|
+
const page = {
|
|
76
|
+
root: {
|
|
77
|
+
type: 'node',
|
|
78
|
+
tag: 'img',
|
|
79
|
+
attributes: {
|
|
80
|
+
src: '/x.png',
|
|
81
|
+
alt: { _i18n: true, en: 'Photo', pl: 'Zdjęcie' },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const result = PageDataSchema.safeParse(page);
|
|
86
|
+
expect(result.success).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('HTML node accepts an _i18n object inside a children array', () => {
|
|
90
|
+
const page = {
|
|
91
|
+
root: {
|
|
92
|
+
type: 'node',
|
|
93
|
+
tag: 'p',
|
|
94
|
+
children: [
|
|
95
|
+
{ _i18n: true, en: 'Hello', pl: 'Cześć' },
|
|
96
|
+
' literal ',
|
|
97
|
+
{ type: 'node', tag: 'span', children: 'static' },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const result = PageDataSchema.safeParse(page);
|
|
102
|
+
expect(result.success).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('component file with localized prop defaults validates as PageData', () => {
|
|
106
|
+
// End-to-end regression: a real component shape written by AI with
|
|
107
|
+
// localized defaults must pass PageDataSchema so the editor can open it.
|
|
108
|
+
const componentFile = {
|
|
109
|
+
component: {
|
|
110
|
+
interface: {
|
|
111
|
+
title: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
default: { _i18n: true, en: 'From the blog', pl: 'Z bloga' },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
structure: { type: 'node', tag: 'section', children: [] },
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const result = PageDataSchema.safeParse(componentFile);
|
|
121
|
+
expect(result.success).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
31
124
|
test('valid select prop definition', () => {
|
|
32
125
|
const propDef = {
|
|
33
126
|
type: 'select',
|
|
@@ -23,11 +23,39 @@ export const BasePropTypeSchema = z.enum(['string', 'select', 'boolean', 'number
|
|
|
23
23
|
export const PropTypeSchema = z.enum(['string', 'select', 'boolean', 'number', 'link', 'file', 'rich-text', 'embed', 'list']);
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* `_i18n` value object schema. Used wherever a localizable string can appear:
|
|
27
|
+
* - Prop defaults (`BasePropDefinitionSchema.default`)
|
|
28
|
+
* - Node `children` (text content on `type: "node"`, `"link"`, `"component"`,
|
|
29
|
+
* and basic list nodes)
|
|
30
|
+
* - Attribute values (`attributes` records on every node type)
|
|
31
|
+
*
|
|
32
|
+
* The runtime resolves these objects to the active locale's string at the
|
|
33
|
+
* earliest dispatch point in both the SSR renderer (`renderNode`) and the
|
|
34
|
+
* client builder (`ComponentBuilder.buildComponent`). The schema accepts the
|
|
35
|
+
* shape so the editor's load path doesn't reject files that contain it.
|
|
36
|
+
*/
|
|
37
|
+
export const I18nValueObjectSchema = z.object({
|
|
38
|
+
_i18n: z.literal(true),
|
|
39
|
+
}).passthrough();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Base prop definition schema (for non-list props).
|
|
43
|
+
* `default` accepts a literal value (string/number/boolean/link) OR an
|
|
44
|
+
* `_i18n` value object — the SSR pipeline resolves the latter to the active
|
|
45
|
+
* locale's string via `resolveI18nInProps` at render time, so localized prop
|
|
46
|
+
* defaults are first-class. The `I18nOrStringSchema` proper is defined later
|
|
47
|
+
* in this file; we inline the matching shape here to keep the validator
|
|
48
|
+
* tolerant of localized defaults.
|
|
27
49
|
*/
|
|
28
50
|
export const BasePropDefinitionSchema = z.object({
|
|
29
51
|
type: BasePropTypeSchema,
|
|
30
|
-
default: z.union([
|
|
52
|
+
default: z.union([
|
|
53
|
+
z.string(),
|
|
54
|
+
z.number(),
|
|
55
|
+
z.boolean(),
|
|
56
|
+
z.object({ href: z.string(), target: z.string().optional() }),
|
|
57
|
+
I18nValueObjectSchema,
|
|
58
|
+
]).optional(),
|
|
31
59
|
options: z.array(z.string()).readonly().optional(),
|
|
32
60
|
enumName: z.string().optional(), // For 'select' type: reference to project-level enum
|
|
33
61
|
accept: z.string().optional(), // For 'file' type: MIME pattern like "image/*", "video/*"
|
|
@@ -51,7 +79,16 @@ export const ListItemSchemaSchema = z.record(z.string(), BasePropDefinitionSchem
|
|
|
51
79
|
export const ListPropDefinitionSchema = z.object({
|
|
52
80
|
type: z.literal('list'),
|
|
53
81
|
itemSchema: ListItemSchemaSchema,
|
|
54
|
-
|
|
82
|
+
// List-item field values can be any primitive, a link object, an `_i18n`
|
|
83
|
+
// object (resolved by the SSR pipeline), or null.
|
|
84
|
+
default: z.array(z.record(z.string(), z.union([
|
|
85
|
+
z.string(),
|
|
86
|
+
z.number(),
|
|
87
|
+
z.boolean(),
|
|
88
|
+
z.object({ href: z.string(), target: z.string().optional() }),
|
|
89
|
+
I18nValueObjectSchema,
|
|
90
|
+
z.null(),
|
|
91
|
+
]))).optional(),
|
|
55
92
|
}).passthrough();
|
|
56
93
|
|
|
57
94
|
/**
|
|
@@ -175,8 +212,9 @@ export const InteractiveStylesSchema = z.array(InteractiveStyleRuleSchema);
|
|
|
175
212
|
export const SlotMarkerSchema: z.ZodType<any> = z.lazy(() => z.object({
|
|
176
213
|
type: z.literal(NODE_TYPE.SLOT),
|
|
177
214
|
default: z.union([
|
|
178
|
-
z.array(z.union([ComponentNodeSchema, z.string()])),
|
|
215
|
+
z.array(z.union([ComponentNodeSchema, z.string(), I18nValueObjectSchema])),
|
|
179
216
|
z.string(),
|
|
217
|
+
I18nValueObjectSchema,
|
|
180
218
|
]).optional(),
|
|
181
219
|
}).passthrough());
|
|
182
220
|
|
|
@@ -197,11 +235,12 @@ export const HtmlNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
|
|
|
197
235
|
style: StyleValueSchema.optional(),
|
|
198
236
|
interactiveStyles: InteractiveStylesSchema.optional(),
|
|
199
237
|
generateElementClass: z.boolean().optional(),
|
|
200
|
-
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
238
|
+
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), I18nValueObjectSchema])).optional(),
|
|
201
239
|
props: z.record(z.string(), z.any()).optional(), // Allow props for backward compatibility
|
|
202
240
|
children: z.union([
|
|
203
|
-
z.array(z.union([ComponentNodeSchema, z.string()])),
|
|
241
|
+
z.array(z.union([ComponentNodeSchema, z.string(), I18nValueObjectSchema])),
|
|
204
242
|
z.string(),
|
|
243
|
+
I18nValueObjectSchema,
|
|
205
244
|
]).optional(),
|
|
206
245
|
}).passthrough());
|
|
207
246
|
|
|
@@ -217,10 +256,11 @@ export const ComponentInstanceNodeSchema: z.ZodType<any> = z.lazy(() => z.object
|
|
|
217
256
|
style: StyleValueSchema.optional(),
|
|
218
257
|
interactiveStyles: InteractiveStylesSchema.optional(),
|
|
219
258
|
generateElementClass: z.boolean().optional(),
|
|
220
|
-
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
259
|
+
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), I18nValueObjectSchema])).optional(),
|
|
221
260
|
children: z.union([
|
|
222
|
-
z.array(z.union([ComponentNodeSchema, z.string()])),
|
|
261
|
+
z.array(z.union([ComponentNodeSchema, z.string(), I18nValueObjectSchema])),
|
|
223
262
|
z.string(),
|
|
263
|
+
I18nValueObjectSchema,
|
|
224
264
|
]).optional(),
|
|
225
265
|
}).passthrough());
|
|
226
266
|
|
|
@@ -229,13 +269,13 @@ export const ComponentInstanceNodeSchema: z.ZodType<any> = z.lazy(() => z.object
|
|
|
229
269
|
*/
|
|
230
270
|
export const EmbedNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
|
|
231
271
|
type: z.literal(NODE_TYPE.EMBED),
|
|
232
|
-
html: z.union([z.string(), HtmlMappingSchema]),
|
|
272
|
+
html: z.union([z.string(), HtmlMappingSchema, I18nValueObjectSchema]),
|
|
233
273
|
label: z.string().optional(),
|
|
234
274
|
if: IfConditionSchema.optional(),
|
|
235
275
|
style: StyleValueSchema.optional(),
|
|
236
276
|
interactiveStyles: InteractiveStylesSchema.optional(),
|
|
237
277
|
generateElementClass: z.boolean().optional(),
|
|
238
|
-
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
278
|
+
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), I18nValueObjectSchema])).optional(),
|
|
239
279
|
}).passthrough());
|
|
240
280
|
|
|
241
281
|
/**
|
|
@@ -243,16 +283,17 @@ export const EmbedNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
|
|
|
243
283
|
*/
|
|
244
284
|
export const LinkNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
|
|
245
285
|
type: z.literal(NODE_TYPE.LINK),
|
|
246
|
-
href: z.union([z.string(), LinkMappingSchema]),
|
|
286
|
+
href: z.union([z.string(), LinkMappingSchema, I18nValueObjectSchema]),
|
|
247
287
|
label: z.string().optional(),
|
|
248
288
|
if: IfConditionSchema.optional(),
|
|
249
289
|
style: StyleValueSchema.optional(),
|
|
250
290
|
interactiveStyles: InteractiveStylesSchema.optional(),
|
|
251
291
|
generateElementClass: z.boolean().optional(),
|
|
252
|
-
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
292
|
+
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), I18nValueObjectSchema])).optional(),
|
|
253
293
|
children: z.union([
|
|
254
|
-
z.array(z.union([ComponentNodeSchema, z.string()])),
|
|
294
|
+
z.array(z.union([ComponentNodeSchema, z.string(), I18nValueObjectSchema])),
|
|
255
295
|
z.string(),
|
|
296
|
+
I18nValueObjectSchema,
|
|
256
297
|
]).optional(),
|
|
257
298
|
}).passthrough());
|
|
258
299
|
|
|
@@ -273,7 +314,7 @@ export const LocaleListNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
|
|
|
273
314
|
showSeparator: z.boolean().optional(),
|
|
274
315
|
showFlag: z.boolean().optional(),
|
|
275
316
|
flagStyle: StyleValueSchema.optional(),
|
|
276
|
-
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
317
|
+
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), I18nValueObjectSchema])).optional(),
|
|
277
318
|
}).passthrough());
|
|
278
319
|
|
|
279
320
|
/**
|
|
@@ -298,7 +339,7 @@ export const ListNodeSchemaBasic: z.ZodType<any> = z.lazy(() => z.object({
|
|
|
298
339
|
offset: z.number().optional(),
|
|
299
340
|
excludeCurrentItem: z.boolean().optional(),
|
|
300
341
|
emitTemplate: z.boolean().optional(),
|
|
301
|
-
children: z.array(z.union([ComponentNodeSchema, z.string()])).optional(),
|
|
342
|
+
children: z.array(z.union([ComponentNodeSchema, z.string(), I18nValueObjectSchema])).optional(),
|
|
302
343
|
}).passthrough());
|
|
303
344
|
|
|
304
345
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../lib/server/ssr/buildErrorOverlay.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Build Error Overlay Generator\n * Generates an HTML page showing build errors when the static server detects _errors.json\n */\n\nexport interface BuildError {\n file: string; // e.g., \"pages/posts.json\"\n message: string; // Error message\n type: string; // 'minify' | 'render' | 'parse' | 'cms'\n}\n\nexport interface BuildErrorsData {\n errors: BuildError[];\n timestamp: number;\n}\n\n/**\n * Escape HTML to prevent XSS in error messages\n */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Safely encode data for embedding in a script tag\n */\nfunction safeJsonForScript(data: unknown): string {\n return JSON.stringify(data)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/&/g, '\\\\u0026');\n}\n\n/**\n * Generate HTML page showing build errors\n */\nexport function generateBuildErrorPage(data: BuildErrorsData): string {\n const { errors, timestamp } = data;\n const timeStr = new Date(timestamp).toLocaleTimeString();\n\n // Generate plain text for copying to AI\n const plainTextErrors = errors.map(err =>\n `[${err.type.toUpperCase()}] ${err.file}\\n${err.message}`\n ).join('\\n\\n');\n const copyText = `Build failed with ${errors.length} error(s):\\n\\n${plainTextErrors}`;\n\n const errorListHtml = errors.map((err) => `\n <div class=\"error-item\">\n <div class=\"error-item-header\">\n <div class=\"error-type\">${escapeHtml(err.type)}</div>\n <div class=\"error-file\">${escapeHtml(err.file)}</div>\n </div>\n <div class=\"error-message\">${escapeHtml(err.message)}</div>\n </div>\n `).join('');\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Build Failed</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);\n color: #e2e8f0;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n }\n\n .container {\n max-width: 720px;\n width: 100%;\n }\n\n .card {\n background: rgba(30, 30, 50, 0.8);\n backdrop-filter: blur(20px);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 16px;\n overflow: hidden;\n box-shadow:\n 0 4px 6px rgba(0, 0, 0, 0.1),\n 0 20px 50px rgba(0, 0, 0, 0.3),\n inset 0 1px 0 rgba(255, 255, 255, 0.05);\n }\n\n .header {\n background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);\n padding: 20px 24px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n\n .header-left {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n .header-icon {\n width: 32px;\n height: 32px;\n background: rgba(255, 255, 255, 0.2);\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .header-icon svg {\n width: 18px;\n height: 18px;\n stroke: white;\n }\n\n .header-title {\n font-size: 16px;\n font-weight: 600;\n letter-spacing: -0.01em;\n }\n\n .error-count {\n background: rgba(0, 0, 0, 0.25);\n padding: 6px 14px;\n border-radius: 100px;\n font-size: 13px;\n font-weight: 500;\n }\n\n .body {\n padding: 20px;\n max-height: 50vh;\n overflow-y: auto;\n }\n\n .body::-webkit-scrollbar {\n width: 6px;\n }\n\n .body::-webkit-scrollbar-track {\n background: transparent;\n }\n\n .body::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 3px;\n }\n\n .error-item {\n background: rgba(0, 0, 0, 0.3);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 10px;\n padding: 14px 16px;\n margin-bottom: 10px;\n transition: border-color 0.15s;\n }\n\n .error-item:last-child { margin-bottom: 0; }\n\n .error-item:hover {\n border-color: rgba(255, 255, 255, 0.12);\n }\n\n .error-item-header {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 10px;\n }\n\n .error-type {\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n padding: 4px 8px;\n border-radius: 4px;\n background: rgba(239, 68, 68, 0.2);\n color: #fca5a5;\n }\n\n .error-file {\n font-family: 'SF Mono', 'Fira Code', Menlo, Monaco, monospace;\n font-size: 12px;\n color: #94a3b8;\n }\n\n .error-message {\n font-family: 'SF Mono', 'Fira Code', Menlo, Monaco, monospace;\n font-size: 12px;\n line-height: 1.7;\n color: #f87171;\n white-space: pre-wrap;\n word-break: break-word;\n background: rgba(0, 0, 0, 0.2);\n padding: 10px 12px;\n border-radius: 6px;\n border-left: 2px solid #dc2626;\n }\n\n .footer {\n padding: 16px 20px;\n background: rgba(0, 0, 0, 0.2);\n border-top: 1px solid rgba(255, 255, 255, 0.06);\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n }\n\n .footer-info {\n font-size: 12px;\n color: #64748b;\n }\n\n .footer-actions {\n display: flex;\n gap: 8px;\n }\n\n .btn {\n border: none;\n padding: 10px 16px;\n border-radius: 8px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 6px;\n transition: all 0.15s;\n }\n\n .btn svg {\n width: 14px;\n height: 14px;\n }\n\n .btn-secondary {\n background: rgba(255, 255, 255, 0.08);\n color: #e2e8f0;\n border: 1px solid rgba(255, 255, 255, 0.1);\n }\n\n .btn-secondary:hover {\n background: rgba(255, 255, 255, 0.12);\n border-color: rgba(255, 255, 255, 0.15);\n }\n\n .btn-primary {\n background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);\n color: white;\n }\n\n .btn-primary:hover {\n background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);\n }\n\n .btn-success {\n background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"card\">\n <div class=\"header\">\n <div class=\"header-left\">\n <div class=\"header-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"></line>\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"></line>\n </svg>\n </div>\n <span class=\"header-title\">Build Failed</span>\n </div>\n <span class=\"error-count\">${errors.length} error${errors.length === 1 ? '' : 's'}</span>\n </div>\n <div class=\"body\">\n ${errorListHtml}\n </div>\n <div class=\"footer\">\n <div class=\"footer-info\">Failed at ${timeStr}</div>\n <div class=\"footer-actions\">\n <button class=\"btn btn-secondary\" id=\"copyBtn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect>\n <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path>\n </svg>\n <span>Copy</span>\n </button>\n <button class=\"btn btn-primary\" onclick=\"location.reload()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"23 4 23 10 17 10\"></polyline>\n <path d=\"M20.49 15a9 9 0 1 1-2.12-9.36L23 10\"></path>\n </svg>\n Refresh\n </button>\n </div>\n </div>\n </div>\n </div>\n <script>\n (function() {\n var copyBtn = document.getElementById('copyBtn');\n var copyText = ${safeJsonForScript(copyText)};\n\n copyBtn.addEventListener('click', function() {\n navigator.clipboard.writeText(copyText).then(function() {\n var span = copyBtn.querySelector('span');\n var original = span.textContent;\n span.textContent = 'Copied!';\n copyBtn.classList.add('btn-success');\n setTimeout(function() {\n span.textContent = original;\n copyBtn.classList.remove('btn-success');\n }, 2000);\n }).catch(function(err) {\n console.error('Failed to copy:', err);\n });\n });\n })();\n </script>\n</body>\n</html>`;\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|