lovable-ssr 0.1.23 → 0.1.24

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.
@@ -1 +1 @@
1
- {"version":3,"file":"buildHeadHtml.d.ts","sourceRoot":"","sources":["../../src/ssr/buildHeadHtml.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAW5D,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,GAAG;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAwBA"}
1
+ {"version":3,"file":"buildHeadHtml.d.ts","sourceRoot":"","sources":["../../src/ssr/buildHeadHtml.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAG5D,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,GAAG;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAwBA"}
@@ -1,11 +1,4 @@
1
- function escapeHtml(s) {
2
- return s
3
- .replace(/&/g, '&')
4
- .replace(/</g, '&lt;')
5
- .replace(/>/g, '&gt;')
6
- .replace(/"/g, '&quot;')
7
- .replace(/'/g, '&#39;');
8
- }
1
+ import { escapeHtml } from '../utils/escapeHtml.js';
9
2
  export function buildHeadHtmlFromSEO(props) {
10
3
  const title = `<title>${escapeHtml(props.title)}</title>`;
11
4
  const metaTags = [
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/ssr/server.ts"],"names":[],"mappings":"AAGA,OAAgB,EAAE,KAAK,OAAO,EAAkD,MAAM,SAAS,CAAC;AAIhG,OAAO,KAAK,EAAqB,YAAY,EAAsC,MAAM,aAAa,CAAC;AAGvG,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wGAAwG;IACxG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,uGAAuG;IACvG,OAAO,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,4GAA4G;IAC5G,UAAU,CAAC,EAAE,YAAY,CAAC;CAC3B;AAwXD,wBAAsB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC;IACtE,MAAM,EAAE,MAAM,OAAO,CAAC;IACtB,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CACxD,CAAC,CAOD;AAED;qDACqD;AACrD,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,QAO7D"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/ssr/server.ts"],"names":[],"mappings":"AAGA,OAAgB,EAAE,KAAK,OAAO,EAAkD,MAAM,SAAS,CAAC;AAKhG,OAAO,KAAK,EAAqB,YAAY,EAAoD,MAAM,aAAa,CAAC;AAGrH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wGAAwG;IACxG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,uGAAuG;IACvG,OAAO,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,4GAA4G;IAC5G,UAAU,CAAC,EAAE,YAAY,CAAC;CAC3B;AA2TD,wBAAsB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC;IACtE,MAAM,EAAE,MAAM,OAAO,CAAC;IACtB,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CACxD,CAAC,CAOD;AAED;qDACqD;AACrD,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,QAO7D"}
@@ -5,6 +5,8 @@ import express from 'express';
5
5
  import { createServer as createViteServer } from 'vite';
6
6
  import { getMiddleware, getRoutes } from '../registry.js';
7
7
  import RouterService from '../router/RouterService.js';
8
+ import { buildRequestContext } from '../types.js';
9
+ import { escapeHtml } from '../utils/escapeHtml.js';
8
10
  function defaultDistEntryPath(entryPath) {
9
11
  return entryPath
10
12
  .replace(/^src\//, 'dist/')
@@ -16,15 +18,6 @@ class SsrServer {
16
18
  config;
17
19
  isProd;
18
20
  _rendererCache;
19
- /** On-demand SSR cache: key = pathname + normalized search params, value = render result. Only used in production. */
20
- _ssrCache = new Map();
21
- normalizeCacheKey(url) {
22
- const u = new URL(url, 'http://localhost');
23
- const search = new URLSearchParams(u.search);
24
- const sorted = new URLSearchParams([...search.entries()].sort((a, b) => a[0].localeCompare(b[0])));
25
- const q = sorted.toString();
26
- return (u.pathname || '/') + (q ? `?${q}` : '');
27
- }
28
21
  constructor(config) {
29
22
  this.config = {
30
23
  root: path.resolve(config.root),
@@ -123,20 +116,12 @@ Sitemap: ${baseUrl}/sitemap.xml`);
123
116
  });
124
117
  }
125
118
  buildSitemapXml(entries) {
126
- const lines = entries.map((e) => ` <url><loc>${this.escapeXml(e.loc)}</loc><lastmod>${e.lastmod ?? ''}</lastmod><changefreq>${e.changefreq ?? 'weekly'}</changefreq><priority>${e.priority ?? 0.5}</priority></url>`);
119
+ const lines = entries.map((e) => ` <url><loc>${escapeHtml(e.loc)}</loc><lastmod>${e.lastmod ?? ''}</lastmod><changefreq>${e.changefreq ?? 'weekly'}</changefreq><priority>${e.priority ?? 0.5}</priority></url>`);
127
120
  return `<?xml version="1.0" encoding="UTF-8"?>
128
121
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
129
122
  ${lines.join('\n')}
130
123
  </urlset>`;
131
124
  }
132
- escapeXml(str) {
133
- return str
134
- .replace(/&/g, '&amp;')
135
- .replace(/</g, '&lt;')
136
- .replace(/>/g, '&gt;')
137
- .replace(/"/g, '&quot;')
138
- .replace(/'/g, '&apos;');
139
- }
140
125
  async runMiddleware(ctx) {
141
126
  const mw = this.config.middleware ?? getMiddleware();
142
127
  if (!mw)
@@ -163,27 +148,7 @@ ${lines.join('\n')}
163
148
  const params = matchedRoute
164
149
  ? RouterService.routeParams(matchedRoute.path, pathname)
165
150
  : { routeParams: {}, searchParams: {} };
166
- const cookiesRaw = req.headers.cookie ?? '';
167
- const cookies = {};
168
- if (cookiesRaw) {
169
- cookiesRaw.split(';').forEach((part) => {
170
- const [k, ...rest] = part.split('=');
171
- if (!k)
172
- return;
173
- const key = k.trim();
174
- if (!key)
175
- return;
176
- const value = rest.join('=').trim();
177
- cookies[key] = decodeURIComponent(value);
178
- });
179
- }
180
- const requestContext = {
181
- cookiesRaw,
182
- cookies,
183
- headers: req.headers,
184
- method: req.method,
185
- url: req.originalUrl,
186
- };
151
+ const requestContext = buildRequestContext(req);
187
152
  const mwResult = await this.runMiddleware({
188
153
  request: requestContext,
189
154
  route: matchedRoute,
@@ -215,47 +180,20 @@ ${lines.join('\n')}
215
180
  const template = this.readTemplate(path.join(this.config.root, 'dist', 'index.html'));
216
181
  return res.status(200).set({ 'Content-Type': 'text/html' }).send(template);
217
182
  }
183
+ async resolveRenderResult(url, render, requestContext) {
184
+ const result = await render(url, { requestContext });
185
+ return {
186
+ appHtml: typeof result.html === 'string' ? result.html : '',
187
+ preloadedData: result.preloadedData ?? {},
188
+ helmet: result.helmet,
189
+ };
190
+ }
218
191
  async renderSsr(url, res, requestContext) {
219
192
  const { template, render } = await this.getSsrRenderer();
220
193
  if (process.env.NODE_ENV !== 'production' && process.env.LOVABLE_SSR_DEBUG) {
221
194
  console.log('[lovable-ssr] render(url)', url);
222
195
  }
223
- let appHtml;
224
- let preloadedData;
225
- let helmet;
226
- const cookiesRaw = requestContext.cookiesRaw;
227
- if (this.isProd) {
228
- const cacheKey = this.normalizeCacheKey(url);
229
- const hasCookies = !!cookiesRaw;
230
- // Evita cachear respostas personalizadas por cookies (ex.: auth).
231
- if (!hasCookies) {
232
- const cached = this._ssrCache.get(cacheKey);
233
- if (cached) {
234
- appHtml = cached.html;
235
- preloadedData = cached.preloadedData;
236
- helmet = cached.helmet;
237
- }
238
- else {
239
- const result = await render(url, { requestContext });
240
- appHtml = typeof result.html === 'string' ? result.html : '';
241
- preloadedData = result.preloadedData ?? {};
242
- helmet = result.helmet;
243
- this._ssrCache.set(cacheKey, { html: appHtml, preloadedData, helmet });
244
- }
245
- }
246
- else {
247
- const result = await render(url, { requestContext });
248
- appHtml = typeof result.html === 'string' ? result.html : '';
249
- preloadedData = result.preloadedData ?? {};
250
- helmet = result.helmet;
251
- }
252
- }
253
- else {
254
- const result = await render(url, { requestContext });
255
- appHtml = typeof result.html === 'string' ? result.html : '';
256
- preloadedData = result.preloadedData ?? {};
257
- helmet = result.helmet;
258
- }
196
+ const { appHtml, preloadedData, helmet } = await this.resolveRenderResult(url, render, requestContext);
259
197
  let html = template.replace('<div id="root"></div>', `<div id="root">${appHtml}</div>`);
260
198
  html = this.injectHelmet(html, helmet);
261
199
  html = this.injectPreloadedData(html, preloadedData);
package/dist/types.d.ts CHANGED
@@ -32,6 +32,12 @@ export type RequestContext = {
32
32
  method: string;
33
33
  url: string;
34
34
  };
35
+ export declare function parseCookies(raw: string): Record<string, string>;
36
+ export declare function buildRequestContext(req: {
37
+ headers: IncomingHttpHeaders;
38
+ method: string;
39
+ originalUrl: string;
40
+ }): RequestContext;
35
41
  export type RouteDataParams = {
36
42
  routeParams: Record<string, string>;
37
43
  searchParams: Record<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG;IAC5D,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC1E,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,OAAO,CAAC;AAEZ,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CACpE,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,oBAAoB,CAAC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,kBAAkB,CAAC;CAC9B,CAAC;AACF,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,CACzB,GAAG,EAAE,iBAAiB,KACnB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,GAAG,kBAAkB,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG;IAC5D,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC1E,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,OAAO,CAAC;AAEZ,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CACpE,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,oBAAoB,CAAC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,kBAAkB,CAAC;CAC9B,CAAC;AACF,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWhE;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE;IACvC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,cAAc,CASjB;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,CACzB,GAAG,EAAE,iBAAiB,KACnB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,GAAG,kBAAkB,GAAG,IAAI,CAAC"}
package/dist/types.js CHANGED
@@ -1 +1,25 @@
1
- export {};
1
+ export function parseCookies(raw) {
2
+ const cookies = {};
3
+ if (!raw)
4
+ return cookies;
5
+ raw.split(';').forEach((part) => {
6
+ const [k, ...rest] = part.split('=');
7
+ if (!k)
8
+ return;
9
+ const key = k.trim();
10
+ if (!key)
11
+ return;
12
+ cookies[key] = decodeURIComponent(rest.join('=').trim());
13
+ });
14
+ return cookies;
15
+ }
16
+ export function buildRequestContext(req) {
17
+ const cookiesRaw = req.headers.cookie ?? '';
18
+ return {
19
+ cookiesRaw,
20
+ cookies: parseCookies(cookiesRaw),
21
+ headers: req.headers,
22
+ method: req.method,
23
+ url: req.originalUrl,
24
+ };
25
+ }
@@ -0,0 +1,2 @@
1
+ export declare function escapeHtml(s: string): string;
2
+ //# sourceMappingURL=escapeHtml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escapeHtml.d.ts","sourceRoot":"","sources":["../../src/utils/escapeHtml.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAO5C"}
@@ -0,0 +1,8 @@
1
+ export function escapeHtml(s) {
2
+ return s
3
+ .replace(/&/g, '&amp;')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;')
7
+ .replace(/'/g, '&#39;');
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lovable-ssr",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "SSR and route data engine for Lovable projects",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -38,6 +38,9 @@
38
38
  "changelog:patch": "npm run changelog -- --release-as patch --prerelease rc",
39
39
  "changelog:minor": "npm run changelog -- --release-as minor --prerelease rc",
40
40
  "changelog:major": "npm run changelog -- --release-as major --prerelease rc",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "test:e2e": "playwright test",
41
44
  "build:docs": "cd doc && npm run docs:build"
42
45
  },
43
46
  "peerDependencies": {
@@ -49,16 +52,19 @@
49
52
  "express": "^4.21.0"
50
53
  },
51
54
  "devDependencies": {
55
+ "@playwright/test": "^1.59.1",
52
56
  "@types/express": "^4.17.21",
53
57
  "@types/node": "^22.16.5",
54
58
  "@types/react": "^18.3.23",
55
59
  "@types/react-dom": "^18.3.7",
60
+ "happy-dom": "^20.8.9",
56
61
  "react": "^18.3.1",
57
62
  "react-dom": "^18.3.1",
58
63
  "react-router-dom": "^6.30.1",
59
64
  "standard-version": "^9.5.0",
60
65
  "typescript": "^5.8.3",
61
- "vite": "^5.4.19"
66
+ "vite": "^5.4.19",
67
+ "vitest": "^4.1.4"
62
68
  },
63
69
  "engines": {
64
70
  "node": ">=18"