@allegrocdp/preview 0.1.0-dev.11 → 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,6 +7,8 @@ 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";
@@ -158,6 +160,60 @@ function normalizeTenantSdkUrl(raw) {
158
160
  const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
159
161
  return withProtocol.replace(/\/+$/, "") + "/client.js";
160
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
+ }
161
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;}}});})();`;
162
218
  function buildPreviewData(options) {
163
219
  const json = JSON.stringify({
@@ -170,6 +226,9 @@ function buildPreviewData(options) {
170
226
  });
171
227
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
172
228
  }
229
+ function previewCssTag(css) {
230
+ return css ? `<style data-allegro-preview-css>${escapeStyle(css)}</style>` : "";
231
+ }
173
232
  function standaloneShell(options) {
174
233
  const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
175
234
  return `<!DOCTYPE html>
@@ -184,6 +243,7 @@ function standaloneShell(options) {
184
243
  body { margin: 0; }
185
244
  [data-allegro-interaction] { display: block; }
186
245
  </style>
246
+ ${previewCssTag(options.previewCss)}
187
247
  </head>
188
248
  <body>
189
249
  <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
@@ -212,6 +272,7 @@ function articleShell(options) {
212
272
  p { margin: 0 0 1.1em; font-size: 1rem; overflow-wrap: break-word; }
213
273
  [data-allegro-interaction] { display: block; margin: 24px 0; }
214
274
  </style>
275
+ ${previewCssTag(options.previewCss)}
215
276
  </head>
216
277
  <body>
217
278
  <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
@@ -389,7 +450,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
389
450
  sseClients.delete(res);
390
451
  });
391
452
  });
392
- app.get("/preview", (req, res) => {
453
+ app.get("/preview", async (req, res) => {
393
454
  const slug = typeof req.query.slug === "string" ? req.query.slug : "";
394
455
  const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
395
456
  const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
@@ -397,6 +458,14 @@ async function startServer(templatePath, port = 0, tenant = "") {
397
458
  res.status(400).send("Missing slug");
398
459
  return;
399
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
+ }
400
469
  if (slug !== "." && !templates.some((t) => t.slug === slug)) {
401
470
  res.status(404).send("Template not found");
402
471
  return;
@@ -422,6 +491,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
422
491
  }
423
492
  const effectiveSdkUrl = sdkParam ?? (tenant ? normalizeTenantSdkUrl(tenant) : void 0);
424
493
  const blockCookies = req.query.blockCookies !== "false";
494
+ const previewCss = effectiveSdkUrl ? await fetchPreviewCss(effectiveSdkUrl) : "";
425
495
  const shellOptions = {
426
496
  slug,
427
497
  html: templateFiles.html,
@@ -430,7 +500,8 @@ async function startServer(templatePath, port = 0, tenant = "") {
430
500
  fieldValues,
431
501
  tab,
432
502
  sdkUrl: effectiveSdkUrl,
433
- blockCookies
503
+ blockCookies,
504
+ previewCss
434
505
  };
435
506
  const rendered = tab === "standalone" ? standaloneShell(shellOptions) : articleShell(shellOptions);
436
507
  res.setHeader("Cache-Control", "no-store");
package/dist/server.js CHANGED
@@ -4,6 +4,8 @@
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";
@@ -155,6 +157,60 @@ function normalizeTenantSdkUrl(raw) {
155
157
  const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
156
158
  return withProtocol.replace(/\/+$/, "") + "/client.js";
157
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
+ }
158
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;}}});})();`;
159
215
  function buildPreviewData(options) {
160
216
  const json = JSON.stringify({
@@ -167,6 +223,9 @@ function buildPreviewData(options) {
167
223
  });
168
224
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
169
225
  }
226
+ function previewCssTag(css) {
227
+ return css ? `<style data-allegro-preview-css>${escapeStyle(css)}</style>` : "";
228
+ }
170
229
  function standaloneShell(options) {
171
230
  const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
172
231
  return `<!DOCTYPE html>
@@ -181,6 +240,7 @@ function standaloneShell(options) {
181
240
  body { margin: 0; }
182
241
  [data-allegro-interaction] { display: block; }
183
242
  </style>
243
+ ${previewCssTag(options.previewCss)}
184
244
  </head>
185
245
  <body>
186
246
  <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
@@ -209,6 +269,7 @@ function articleShell(options) {
209
269
  p { margin: 0 0 1.1em; font-size: 1rem; overflow-wrap: break-word; }
210
270
  [data-allegro-interaction] { display: block; margin: 24px 0; }
211
271
  </style>
272
+ ${previewCssTag(options.previewCss)}
212
273
  </head>
213
274
  <body>
214
275
  <script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
@@ -386,7 +447,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
386
447
  sseClients.delete(res);
387
448
  });
388
449
  });
389
- app.get("/preview", (req, res) => {
450
+ app.get("/preview", async (req, res) => {
390
451
  const slug = typeof req.query.slug === "string" ? req.query.slug : "";
391
452
  const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
392
453
  const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
@@ -394,6 +455,14 @@ async function startServer(templatePath, port = 0, tenant = "") {
394
455
  res.status(400).send("Missing slug");
395
456
  return;
396
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
+ }
397
466
  if (slug !== "." && !templates.some((t) => t.slug === slug)) {
398
467
  res.status(404).send("Template not found");
399
468
  return;
@@ -419,6 +488,7 @@ async function startServer(templatePath, port = 0, tenant = "") {
419
488
  }
420
489
  const effectiveSdkUrl = sdkParam ?? (tenant ? normalizeTenantSdkUrl(tenant) : void 0);
421
490
  const blockCookies = req.query.blockCookies !== "false";
491
+ const previewCss = effectiveSdkUrl ? await fetchPreviewCss(effectiveSdkUrl) : "";
422
492
  const shellOptions = {
423
493
  slug,
424
494
  html: templateFiles.html,
@@ -427,7 +497,8 @@ async function startServer(templatePath, port = 0, tenant = "") {
427
497
  fieldValues,
428
498
  tab,
429
499
  sdkUrl: effectiveSdkUrl,
430
- blockCookies
500
+ blockCookies,
501
+ previewCss
431
502
  };
432
503
  const rendered = tab === "standalone" ? standaloneShell(shellOptions) : articleShell(shellOptions);
433
504
  res.setHeader("Cache-Control", "no-store");
@@ -522,5 +593,6 @@ allegro-preview \u2192 ${url}
522
593
  void open(url);
523
594
  }
524
595
  export {
596
+ escapeStyle,
525
597
  startServer
526
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.11",
4
+ "version": "0.1.0-dev.12",
5
5
  "description": "Local dev server for previewing Allegro templates",
6
6
  "type": "module",
7
7
  "bin": {