@allegrocdp/preview 0.1.0-dev.10 → 0.1.0-dev.12
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/cli.js +91 -4
- package/dist/server.js +92 -4
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -7,9 +7,27 @@ import { Command } from "commander";
|
|
|
7
7
|
import chokidar from "chokidar";
|
|
8
8
|
import express from "express";
|
|
9
9
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import { get as httpGet } from "http";
|
|
11
|
+
import { get as httpsGet } from "https";
|
|
10
12
|
import open from "open";
|
|
11
13
|
import { basename, isAbsolute, join, relative, resolve } from "path";
|
|
12
14
|
import { fileURLToPath } from "url";
|
|
15
|
+
|
|
16
|
+
// src/preview-bootstrap.ts
|
|
17
|
+
var PREVIEW_BOOTSTRAP_SCRIPT = `window.__ALLEGRO_PREVIEW__ = true;
|
|
18
|
+
localStorage.setItem('allegro_debug', 'true');
|
|
19
|
+
window.__ALLEGRO_TEMPLATE_RESOLVER__ = async function (slug) {
|
|
20
|
+
try {
|
|
21
|
+
var r = await fetch('/api/template?slug=' + encodeURIComponent(slug));
|
|
22
|
+
if (!r.ok) return null;
|
|
23
|
+
var t = await r.json();
|
|
24
|
+
return { html: t.html, css: t.css, js: t.js, external_css_urls: [] };
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
};`;
|
|
29
|
+
|
|
30
|
+
// src/server.ts
|
|
13
31
|
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
14
32
|
var BOILERPLATE_HTML = `<div class="signup">
|
|
15
33
|
<div class="signup__eyebrow">Newsletter</div>
|
|
@@ -142,6 +160,60 @@ function normalizeTenantSdkUrl(raw) {
|
|
|
142
160
|
const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
143
161
|
return withProtocol.replace(/\/+$/, "") + "/client.js";
|
|
144
162
|
}
|
|
163
|
+
var previewCssCache = /* @__PURE__ */ new Map();
|
|
164
|
+
var PREVIEW_CSS_TTL_MS = 6e4;
|
|
165
|
+
function escapeStyle(css) {
|
|
166
|
+
return css.replace(/<\/(style)/gi, "<\\/$1");
|
|
167
|
+
}
|
|
168
|
+
function isLocalHost(hostname) {
|
|
169
|
+
const name = hostname.toLowerCase();
|
|
170
|
+
return name === "localhost" || name.endsWith(".localhost") || name.endsWith(".test") || name === "127.0.0.1" || name === "::1";
|
|
171
|
+
}
|
|
172
|
+
function getJson(target) {
|
|
173
|
+
return new Promise((resolve2, reject) => {
|
|
174
|
+
const handleResponse = (res) => {
|
|
175
|
+
const status = res.statusCode ?? 0;
|
|
176
|
+
if (status < 200 || status >= 300) {
|
|
177
|
+
res.resume();
|
|
178
|
+
reject(new Error(`HTTP ${status}`));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
let body = "";
|
|
182
|
+
res.setEncoding("utf-8");
|
|
183
|
+
res.on("data", (chunk) => body += chunk);
|
|
184
|
+
res.on("end", () => {
|
|
185
|
+
try {
|
|
186
|
+
resolve2(JSON.parse(body));
|
|
187
|
+
} catch (err) {
|
|
188
|
+
reject(err);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
const req = target.protocol === "https:" ? httpsGet(target, { rejectUnauthorized: !isLocalHost(target.hostname) }, handleResponse) : httpGet(target, handleResponse);
|
|
193
|
+
req.on("error", reject);
|
|
194
|
+
req.setTimeout(5e3, () => req.destroy(new Error("Request timed out")));
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async function fetchPreviewCss(sdkUrl) {
|
|
198
|
+
let endpoint;
|
|
199
|
+
try {
|
|
200
|
+
endpoint = new URL("/api/preview-css", sdkUrl);
|
|
201
|
+
} catch {
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
const cached = previewCssCache.get(endpoint.origin);
|
|
205
|
+
if (cached && Date.now() - cached.fetchedAt < PREVIEW_CSS_TTL_MS) {
|
|
206
|
+
return cached.css;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const data = await getJson(endpoint);
|
|
210
|
+
const css = typeof data.css === "string" ? data.css : "";
|
|
211
|
+
previewCssCache.set(endpoint.origin, { css, fetchedAt: Date.now() });
|
|
212
|
+
return css;
|
|
213
|
+
} catch {
|
|
214
|
+
return cached?.css ?? "";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
145
217
|
var COOKIE_NOOP_SCRIPT = `(function(){var _jar={};Object.defineProperty(document,'cookie',{configurable:true,get:function(){return Object.keys(_jar).map(function(k){return k+'='+_jar[k];}).join('; ');},set:function(str){var parts=str.split(';');var kv=parts[0].trim();var eq=kv.indexOf('=');var name=kv.slice(0,eq).trim();var val=kv.slice(eq+1).trim();var maxAge=null;for(var i=1;i<parts.length;i++){var p=parts[i].trim().toLowerCase();if(p.indexOf('max-age=')===0){maxAge=parseInt(p.split('=')[1],10);}}if(maxAge!==null&&maxAge<=0){delete _jar[name];}else{_jar[name]=val;}}});})();`;
|
|
146
218
|
function buildPreviewData(options) {
|
|
147
219
|
const json = JSON.stringify({
|
|
@@ -154,6 +226,9 @@ function buildPreviewData(options) {
|
|
|
154
226
|
});
|
|
155
227
|
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
156
228
|
}
|
|
229
|
+
function previewCssTag(css) {
|
|
230
|
+
return css ? `<style data-allegro-preview-css>${escapeStyle(css)}</style>` : "";
|
|
231
|
+
}
|
|
157
232
|
function standaloneShell(options) {
|
|
158
233
|
const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
|
|
159
234
|
return `<!DOCTYPE html>
|
|
@@ -161,13 +236,14 @@ function standaloneShell(options) {
|
|
|
161
236
|
<head>
|
|
162
237
|
<meta charset="UTF-8">
|
|
163
238
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
164
|
-
<script
|
|
239
|
+
<script>${PREVIEW_BOOTSTRAP_SCRIPT}</script>
|
|
165
240
|
${cookieScript}
|
|
166
241
|
<style>
|
|
167
242
|
* { box-sizing: border-box; }
|
|
168
243
|
body { margin: 0; }
|
|
169
244
|
[data-allegro-interaction] { display: block; }
|
|
170
245
|
</style>
|
|
246
|
+
${previewCssTag(options.previewCss)}
|
|
171
247
|
</head>
|
|
172
248
|
<body>
|
|
173
249
|
<script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
|
|
@@ -184,7 +260,7 @@ function articleShell(options) {
|
|
|
184
260
|
<head>
|
|
185
261
|
<meta charset="UTF-8">
|
|
186
262
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
187
|
-
<script
|
|
263
|
+
<script>${PREVIEW_BOOTSTRAP_SCRIPT}</script>
|
|
188
264
|
${cookieScript}
|
|
189
265
|
<style>
|
|
190
266
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -196,6 +272,7 @@ function articleShell(options) {
|
|
|
196
272
|
p { margin: 0 0 1.1em; font-size: 1rem; overflow-wrap: break-word; }
|
|
197
273
|
[data-allegro-interaction] { display: block; margin: 24px 0; }
|
|
198
274
|
</style>
|
|
275
|
+
${previewCssTag(options.previewCss)}
|
|
199
276
|
</head>
|
|
200
277
|
<body>
|
|
201
278
|
<script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
|
|
@@ -373,7 +450,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
373
450
|
sseClients.delete(res);
|
|
374
451
|
});
|
|
375
452
|
});
|
|
376
|
-
app.get("/preview", (req, res) => {
|
|
453
|
+
app.get("/preview", async (req, res) => {
|
|
377
454
|
const slug = typeof req.query.slug === "string" ? req.query.slug : "";
|
|
378
455
|
const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
|
|
379
456
|
const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
|
|
@@ -381,6 +458,14 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
381
458
|
res.status(400).send("Missing slug");
|
|
382
459
|
return;
|
|
383
460
|
}
|
|
461
|
+
if (sdkParam !== void 0) {
|
|
462
|
+
try {
|
|
463
|
+
new URL(sdkParam);
|
|
464
|
+
} catch {
|
|
465
|
+
res.status(400).send("Invalid sdk URL");
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
384
469
|
if (slug !== "." && !templates.some((t) => t.slug === slug)) {
|
|
385
470
|
res.status(404).send("Template not found");
|
|
386
471
|
return;
|
|
@@ -406,6 +491,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
406
491
|
}
|
|
407
492
|
const effectiveSdkUrl = sdkParam ?? (tenant ? normalizeTenantSdkUrl(tenant) : void 0);
|
|
408
493
|
const blockCookies = req.query.blockCookies !== "false";
|
|
494
|
+
const previewCss = effectiveSdkUrl ? await fetchPreviewCss(effectiveSdkUrl) : "";
|
|
409
495
|
const shellOptions = {
|
|
410
496
|
slug,
|
|
411
497
|
html: templateFiles.html,
|
|
@@ -414,7 +500,8 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
414
500
|
fieldValues,
|
|
415
501
|
tab,
|
|
416
502
|
sdkUrl: effectiveSdkUrl,
|
|
417
|
-
blockCookies
|
|
503
|
+
blockCookies,
|
|
504
|
+
previewCss
|
|
418
505
|
};
|
|
419
506
|
const rendered = tab === "standalone" ? standaloneShell(shellOptions) : articleShell(shellOptions);
|
|
420
507
|
res.setHeader("Cache-Control", "no-store");
|
package/dist/server.js
CHANGED
|
@@ -4,9 +4,27 @@
|
|
|
4
4
|
import chokidar from "chokidar";
|
|
5
5
|
import express from "express";
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import { get as httpGet } from "http";
|
|
8
|
+
import { get as httpsGet } from "https";
|
|
7
9
|
import open from "open";
|
|
8
10
|
import { basename, isAbsolute, join, relative, resolve } from "path";
|
|
9
11
|
import { fileURLToPath } from "url";
|
|
12
|
+
|
|
13
|
+
// src/preview-bootstrap.ts
|
|
14
|
+
var PREVIEW_BOOTSTRAP_SCRIPT = `window.__ALLEGRO_PREVIEW__ = true;
|
|
15
|
+
localStorage.setItem('allegro_debug', 'true');
|
|
16
|
+
window.__ALLEGRO_TEMPLATE_RESOLVER__ = async function (slug) {
|
|
17
|
+
try {
|
|
18
|
+
var r = await fetch('/api/template?slug=' + encodeURIComponent(slug));
|
|
19
|
+
if (!r.ok) return null;
|
|
20
|
+
var t = await r.json();
|
|
21
|
+
return { html: t.html, css: t.css, js: t.js, external_css_urls: [] };
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};`;
|
|
26
|
+
|
|
27
|
+
// src/server.ts
|
|
10
28
|
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
11
29
|
var BOILERPLATE_HTML = `<div class="signup">
|
|
12
30
|
<div class="signup__eyebrow">Newsletter</div>
|
|
@@ -139,6 +157,60 @@ function normalizeTenantSdkUrl(raw) {
|
|
|
139
157
|
const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
140
158
|
return withProtocol.replace(/\/+$/, "") + "/client.js";
|
|
141
159
|
}
|
|
160
|
+
var previewCssCache = /* @__PURE__ */ new Map();
|
|
161
|
+
var PREVIEW_CSS_TTL_MS = 6e4;
|
|
162
|
+
function escapeStyle(css) {
|
|
163
|
+
return css.replace(/<\/(style)/gi, "<\\/$1");
|
|
164
|
+
}
|
|
165
|
+
function isLocalHost(hostname) {
|
|
166
|
+
const name = hostname.toLowerCase();
|
|
167
|
+
return name === "localhost" || name.endsWith(".localhost") || name.endsWith(".test") || name === "127.0.0.1" || name === "::1";
|
|
168
|
+
}
|
|
169
|
+
function getJson(target) {
|
|
170
|
+
return new Promise((resolve2, reject) => {
|
|
171
|
+
const handleResponse = (res) => {
|
|
172
|
+
const status = res.statusCode ?? 0;
|
|
173
|
+
if (status < 200 || status >= 300) {
|
|
174
|
+
res.resume();
|
|
175
|
+
reject(new Error(`HTTP ${status}`));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
let body = "";
|
|
179
|
+
res.setEncoding("utf-8");
|
|
180
|
+
res.on("data", (chunk) => body += chunk);
|
|
181
|
+
res.on("end", () => {
|
|
182
|
+
try {
|
|
183
|
+
resolve2(JSON.parse(body));
|
|
184
|
+
} catch (err) {
|
|
185
|
+
reject(err);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
const req = target.protocol === "https:" ? httpsGet(target, { rejectUnauthorized: !isLocalHost(target.hostname) }, handleResponse) : httpGet(target, handleResponse);
|
|
190
|
+
req.on("error", reject);
|
|
191
|
+
req.setTimeout(5e3, () => req.destroy(new Error("Request timed out")));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async function fetchPreviewCss(sdkUrl) {
|
|
195
|
+
let endpoint;
|
|
196
|
+
try {
|
|
197
|
+
endpoint = new URL("/api/preview-css", sdkUrl);
|
|
198
|
+
} catch {
|
|
199
|
+
return "";
|
|
200
|
+
}
|
|
201
|
+
const cached = previewCssCache.get(endpoint.origin);
|
|
202
|
+
if (cached && Date.now() - cached.fetchedAt < PREVIEW_CSS_TTL_MS) {
|
|
203
|
+
return cached.css;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const data = await getJson(endpoint);
|
|
207
|
+
const css = typeof data.css === "string" ? data.css : "";
|
|
208
|
+
previewCssCache.set(endpoint.origin, { css, fetchedAt: Date.now() });
|
|
209
|
+
return css;
|
|
210
|
+
} catch {
|
|
211
|
+
return cached?.css ?? "";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
142
214
|
var COOKIE_NOOP_SCRIPT = `(function(){var _jar={};Object.defineProperty(document,'cookie',{configurable:true,get:function(){return Object.keys(_jar).map(function(k){return k+'='+_jar[k];}).join('; ');},set:function(str){var parts=str.split(';');var kv=parts[0].trim();var eq=kv.indexOf('=');var name=kv.slice(0,eq).trim();var val=kv.slice(eq+1).trim();var maxAge=null;for(var i=1;i<parts.length;i++){var p=parts[i].trim().toLowerCase();if(p.indexOf('max-age=')===0){maxAge=parseInt(p.split('=')[1],10);}}if(maxAge!==null&&maxAge<=0){delete _jar[name];}else{_jar[name]=val;}}});})();`;
|
|
143
215
|
function buildPreviewData(options) {
|
|
144
216
|
const json = JSON.stringify({
|
|
@@ -151,6 +223,9 @@ function buildPreviewData(options) {
|
|
|
151
223
|
});
|
|
152
224
|
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
153
225
|
}
|
|
226
|
+
function previewCssTag(css) {
|
|
227
|
+
return css ? `<style data-allegro-preview-css>${escapeStyle(css)}</style>` : "";
|
|
228
|
+
}
|
|
154
229
|
function standaloneShell(options) {
|
|
155
230
|
const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
|
|
156
231
|
return `<!DOCTYPE html>
|
|
@@ -158,13 +233,14 @@ function standaloneShell(options) {
|
|
|
158
233
|
<head>
|
|
159
234
|
<meta charset="UTF-8">
|
|
160
235
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
161
|
-
<script
|
|
236
|
+
<script>${PREVIEW_BOOTSTRAP_SCRIPT}</script>
|
|
162
237
|
${cookieScript}
|
|
163
238
|
<style>
|
|
164
239
|
* { box-sizing: border-box; }
|
|
165
240
|
body { margin: 0; }
|
|
166
241
|
[data-allegro-interaction] { display: block; }
|
|
167
242
|
</style>
|
|
243
|
+
${previewCssTag(options.previewCss)}
|
|
168
244
|
</head>
|
|
169
245
|
<body>
|
|
170
246
|
<script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
|
|
@@ -181,7 +257,7 @@ function articleShell(options) {
|
|
|
181
257
|
<head>
|
|
182
258
|
<meta charset="UTF-8">
|
|
183
259
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
184
|
-
<script
|
|
260
|
+
<script>${PREVIEW_BOOTSTRAP_SCRIPT}</script>
|
|
185
261
|
${cookieScript}
|
|
186
262
|
<style>
|
|
187
263
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -193,6 +269,7 @@ function articleShell(options) {
|
|
|
193
269
|
p { margin: 0 0 1.1em; font-size: 1rem; overflow-wrap: break-word; }
|
|
194
270
|
[data-allegro-interaction] { display: block; margin: 24px 0; }
|
|
195
271
|
</style>
|
|
272
|
+
${previewCssTag(options.previewCss)}
|
|
196
273
|
</head>
|
|
197
274
|
<body>
|
|
198
275
|
<script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
|
|
@@ -370,7 +447,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
370
447
|
sseClients.delete(res);
|
|
371
448
|
});
|
|
372
449
|
});
|
|
373
|
-
app.get("/preview", (req, res) => {
|
|
450
|
+
app.get("/preview", async (req, res) => {
|
|
374
451
|
const slug = typeof req.query.slug === "string" ? req.query.slug : "";
|
|
375
452
|
const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
|
|
376
453
|
const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
|
|
@@ -378,6 +455,14 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
378
455
|
res.status(400).send("Missing slug");
|
|
379
456
|
return;
|
|
380
457
|
}
|
|
458
|
+
if (sdkParam !== void 0) {
|
|
459
|
+
try {
|
|
460
|
+
new URL(sdkParam);
|
|
461
|
+
} catch {
|
|
462
|
+
res.status(400).send("Invalid sdk URL");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
381
466
|
if (slug !== "." && !templates.some((t) => t.slug === slug)) {
|
|
382
467
|
res.status(404).send("Template not found");
|
|
383
468
|
return;
|
|
@@ -403,6 +488,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
403
488
|
}
|
|
404
489
|
const effectiveSdkUrl = sdkParam ?? (tenant ? normalizeTenantSdkUrl(tenant) : void 0);
|
|
405
490
|
const blockCookies = req.query.blockCookies !== "false";
|
|
491
|
+
const previewCss = effectiveSdkUrl ? await fetchPreviewCss(effectiveSdkUrl) : "";
|
|
406
492
|
const shellOptions = {
|
|
407
493
|
slug,
|
|
408
494
|
html: templateFiles.html,
|
|
@@ -411,7 +497,8 @@ async function startServer(templatePath, port = 0, tenant = "") {
|
|
|
411
497
|
fieldValues,
|
|
412
498
|
tab,
|
|
413
499
|
sdkUrl: effectiveSdkUrl,
|
|
414
|
-
blockCookies
|
|
500
|
+
blockCookies,
|
|
501
|
+
previewCss
|
|
415
502
|
};
|
|
416
503
|
const rendered = tab === "standalone" ? standaloneShell(shellOptions) : articleShell(shellOptions);
|
|
417
504
|
res.setHeader("Cache-Control", "no-store");
|
|
@@ -506,5 +593,6 @@ allegro-preview \u2192 ${url}
|
|
|
506
593
|
void open(url);
|
|
507
594
|
}
|
|
508
595
|
export {
|
|
596
|
+
escapeStyle,
|
|
509
597
|
startServer
|
|
510
598
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@allegrocdp/preview",
|
|
4
|
-
"version": "0.1.0-dev.
|
|
4
|
+
"version": "0.1.0-dev.12",
|
|
5
5
|
"description": "Local dev server for previewing Allegro templates",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"build:client": "vite build",
|
|
16
16
|
"build:cli": "tsup",
|
|
17
17
|
"dev": "npm run build:client -- --watch & tsup --watch",
|
|
18
|
+
"test": "vitest run",
|
|
18
19
|
"types": "tsc --noEmit",
|
|
19
20
|
"format": "prettier --write src/",
|
|
20
21
|
"format:check": "prettier --check src/"
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"prettier-plugin-organize-imports": "^4.1.0",
|
|
34
35
|
"tsup": "^8.4.0",
|
|
35
36
|
"typescript": "^5.7.2",
|
|
36
|
-
"vite": "^7.0.4"
|
|
37
|
+
"vite": "^7.0.4",
|
|
38
|
+
"vitest": "^4.0.18"
|
|
37
39
|
}
|
|
38
40
|
}
|