@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 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>window.__ALLEGRO_PREVIEW__ = true; localStorage.setItem('allegro_debug', 'true');</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>window.__ALLEGRO_PREVIEW__ = true; localStorage.setItem('allegro_debug', 'true');</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>window.__ALLEGRO_PREVIEW__ = true; localStorage.setItem('allegro_debug', 'true');</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>window.__ALLEGRO_PREVIEW__ = true; localStorage.setItem('allegro_debug', 'true');</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.10",
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
  }