@allegrocdp/preview 0.1.0-dev.10
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/README.md +89 -0
- package/dist/cli.js +519 -0
- package/dist/client/assets/app-D1XS_DDX.js +14 -0
- package/dist/client/index.html +1272 -0
- package/dist/client/preview-client.js +1 -0
- package/dist/server.js +510 -0
- package/package.json +38 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function r(){const e=document.getElementById("allegro-preview-data");if(!e||!e.textContent)throw new Error("[allegro-preview] Missing #allegro-preview-data element");return JSON.parse(e.textContent)}function o(e){const t=e.tab==="cut"||e.tab==="append";return{type:"template",template_id:e.slug,target_selector:t?"#allegro-preview-article":null,field_values:e.fieldValues,placement_method:e.tab==="cut"?"truncate":e.tab==="append"?"append":null,truncation_unit:e.tab==="cut"?"paragraphs":null,truncation_count:e.tab==="cut"?3:null,truncation_style:e.tab==="cut"?"cut":null,template:{id:e.slug,title:e.slug,slug:e.slug,html:e.html,css:e.css,js:e.js,external_css_urls:[]}}}window.allegro=window.allegro||[];window.allegro.push(e=>{const t=r(),l=o(t);e.interaction.renderActions([l],{skipDelays:!0}).catch(n=>{console.error("[allegro-preview] renderActions failed:",n)})});
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server.ts
|
|
4
|
+
import chokidar from "chokidar";
|
|
5
|
+
import express from "express";
|
|
6
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import open from "open";
|
|
8
|
+
import { basename, isAbsolute, join, relative, resolve } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
11
|
+
var BOILERPLATE_HTML = `<div class="signup">
|
|
12
|
+
<div class="signup__eyebrow">Newsletter</div>
|
|
13
|
+
<h2 class="signup__headline">{% headline %}</h2>
|
|
14
|
+
<p class="signup__sub">{% subheadline %}</p>
|
|
15
|
+
<form class="signup__form" onsubmit="return false">
|
|
16
|
+
<div class="signup__row">
|
|
17
|
+
<input class="signup__input" type="email" placeholder="{% input_placeholder %}" />
|
|
18
|
+
<button class="signup__btn" type="submit">{% button_text %}</button>
|
|
19
|
+
</div>
|
|
20
|
+
<p class="signup__note">{% privacy_note %}</p>
|
|
21
|
+
</form>
|
|
22
|
+
</div>
|
|
23
|
+
`;
|
|
24
|
+
var BOILERPLATE_CSS = `*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
25
|
+
|
|
26
|
+
.signup {
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
|
|
28
|
+
-webkit-font-smoothing: antialiased;
|
|
29
|
+
padding: 16px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.signup__eyebrow {
|
|
33
|
+
font-size: 11px;
|
|
34
|
+
font-weight: 700;
|
|
35
|
+
letter-spacing: 0.1em;
|
|
36
|
+
text-transform: uppercase;
|
|
37
|
+
color: #6366f1;
|
|
38
|
+
margin-bottom: 8px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.signup__headline {
|
|
42
|
+
font-size: clamp(20px, 4vw, 26px);
|
|
43
|
+
font-weight: 800;
|
|
44
|
+
color: #0f172a;
|
|
45
|
+
line-height: 1.2;
|
|
46
|
+
letter-spacing: -0.02em;
|
|
47
|
+
margin-bottom: 8px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.signup__sub {
|
|
51
|
+
font-size: 14px;
|
|
52
|
+
color: #64748b;
|
|
53
|
+
line-height: 1.6;
|
|
54
|
+
margin-bottom: 20px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.signup__row {
|
|
58
|
+
display: flex;
|
|
59
|
+
gap: 8px;
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.signup__input {
|
|
64
|
+
flex: 1;
|
|
65
|
+
min-width: 180px;
|
|
66
|
+
height: 42px;
|
|
67
|
+
padding: 0 12px;
|
|
68
|
+
border: 1.5px solid #e2e8f0;
|
|
69
|
+
border-radius: 8px;
|
|
70
|
+
font-size: 14px;
|
|
71
|
+
color: #0f172a;
|
|
72
|
+
background: #f8fafc;
|
|
73
|
+
outline: none;
|
|
74
|
+
transition: border-color 0.15s, background 0.15s;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.signup__input:focus { border-color: #6366f1; background: #fff; }
|
|
78
|
+
.signup__input::placeholder { color: #94a3b8; }
|
|
79
|
+
|
|
80
|
+
.signup__btn {
|
|
81
|
+
height: 42px;
|
|
82
|
+
padding: 0 20px;
|
|
83
|
+
background: #6366f1;
|
|
84
|
+
color: #fff;
|
|
85
|
+
border: none;
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
font-size: 14px;
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
font-family: inherit;
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
white-space: nowrap;
|
|
92
|
+
transition: background 0.15s;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.signup__btn:hover { background: #4f46e5; }
|
|
96
|
+
.signup__btn:active { opacity: 0.9; }
|
|
97
|
+
|
|
98
|
+
.signup__note {
|
|
99
|
+
margin-top: 10px;
|
|
100
|
+
font-size: 12px;
|
|
101
|
+
color: #94a3b8;
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
var BOILERPLATE_FIELDS = [
|
|
105
|
+
{ slug: "headline", label: "Headline", type: "text", default_value: "Stay in the know" },
|
|
106
|
+
{
|
|
107
|
+
slug: "subheadline",
|
|
108
|
+
label: "Subheadline",
|
|
109
|
+
type: "textarea",
|
|
110
|
+
default_value: "Get the latest stories delivered to your inbox."
|
|
111
|
+
},
|
|
112
|
+
{ slug: "input_placeholder", label: "Input placeholder", type: "text", default_value: "Enter your email" },
|
|
113
|
+
{ slug: "button_text", label: "Button text", type: "text", default_value: "Subscribe" },
|
|
114
|
+
{ slug: "privacy_note", label: "Privacy note", type: "text", default_value: "No spam. Unsubscribe anytime." }
|
|
115
|
+
];
|
|
116
|
+
var ARTICLE_PARAGRAPHS = [
|
|
117
|
+
"The city council voted unanimously Tuesday to approve a sweeping new infrastructure plan that would reshape public transit routes across the metropolitan area over the next decade.",
|
|
118
|
+
"Mayor Jordan Ellison called the decision a turning point for how the region thinks about getting around, noting that similar investments in peer cities have led to measurable reductions in traffic congestion and improved air quality.",
|
|
119
|
+
"The plan includes upgrades to 14 existing bus rapid transit lines, the introduction of three new light-rail corridors connecting the downtown core to outlying neighborhoods, and dedicated cycling infrastructure along six major arterials.",
|
|
120
|
+
"Critics, however, warn that the projected cost of $2.4 billion may be optimistic. Independent analysts suggest the final figure could climb to $3.8 billion once land acquisition, labor, and contingency costs are factored in over the full construction timeline.",
|
|
121
|
+
"Council member Dana Reyes, who voted in favor, acknowledged the fiscal concern but argued that inaction carries its own price, noting that congestion costs residents and businesses hundreds of millions in lost productivity each year.",
|
|
122
|
+
"Environmental groups praised the announcement, saying expanded transit capacity is essential to meet the stated goal of reducing transportation emissions by 45 percent before 2035.",
|
|
123
|
+
"Public comment sessions are scheduled for next month at community centers in all twelve districts. Construction is expected to begin no sooner than mid-next year, pending environmental review and final design approvals."
|
|
124
|
+
];
|
|
125
|
+
function escapeAttr(s) {
|
|
126
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/>/g, ">");
|
|
127
|
+
}
|
|
128
|
+
function sdkScriptTag(sdkUrl) {
|
|
129
|
+
if (!sdkUrl) {
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
132
|
+
return `<script src="${escapeAttr(sdkUrl)}" data-api-url="https://${new URL(sdkUrl).host}"></script>`;
|
|
133
|
+
}
|
|
134
|
+
function normalizeTenantSdkUrl(raw) {
|
|
135
|
+
const trimmed = raw.trim();
|
|
136
|
+
if (!trimmed) {
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
140
|
+
return withProtocol.replace(/\/+$/, "") + "/client.js";
|
|
141
|
+
}
|
|
142
|
+
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
|
+
function buildPreviewData(options) {
|
|
144
|
+
const json = JSON.stringify({
|
|
145
|
+
slug: options.slug,
|
|
146
|
+
html: options.html,
|
|
147
|
+
css: options.css,
|
|
148
|
+
js: options.js,
|
|
149
|
+
fieldValues: options.fieldValues,
|
|
150
|
+
tab: options.tab
|
|
151
|
+
});
|
|
152
|
+
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
153
|
+
}
|
|
154
|
+
function standaloneShell(options) {
|
|
155
|
+
const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
|
|
156
|
+
return `<!DOCTYPE html>
|
|
157
|
+
<html>
|
|
158
|
+
<head>
|
|
159
|
+
<meta charset="UTF-8">
|
|
160
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
161
|
+
<script>window.__ALLEGRO_PREVIEW__ = true; localStorage.setItem('allegro_debug', 'true');</script>
|
|
162
|
+
${cookieScript}
|
|
163
|
+
<style>
|
|
164
|
+
* { box-sizing: border-box; }
|
|
165
|
+
body { margin: 0; }
|
|
166
|
+
[data-allegro-interaction] { display: block; }
|
|
167
|
+
</style>
|
|
168
|
+
</head>
|
|
169
|
+
<body>
|
|
170
|
+
<script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
|
|
171
|
+
${sdkScriptTag(options.sdkUrl)}
|
|
172
|
+
<script src="/preview-client.js"></script>
|
|
173
|
+
</body>
|
|
174
|
+
</html>`;
|
|
175
|
+
}
|
|
176
|
+
function articleShell(options) {
|
|
177
|
+
const paras = ARTICLE_PARAGRAPHS.map((p) => `<p>${p}</p>`).join("\n ");
|
|
178
|
+
const cookieScript = options.blockCookies ?? true ? `<script>${COOKIE_NOOP_SCRIPT}</script>` : "";
|
|
179
|
+
return `<!DOCTYPE html>
|
|
180
|
+
<html>
|
|
181
|
+
<head>
|
|
182
|
+
<meta charset="UTF-8">
|
|
183
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
184
|
+
<script>window.__ALLEGRO_PREVIEW__ = true; localStorage.setItem('allegro_debug', 'true');</script>
|
|
185
|
+
${cookieScript}
|
|
186
|
+
<style>
|
|
187
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
188
|
+
body { font-family: Georgia, 'Times New Roman', serif; background: #fff; color: #111;
|
|
189
|
+
max-width: 680px; margin: 0 auto; padding: 32px 24px 64px; line-height: 1.75;
|
|
190
|
+
-webkit-font-smoothing: antialiased; }
|
|
191
|
+
h1 { font-size: 1.6rem; line-height: 1.25; margin: 0 0 6px; font-weight: 700; }
|
|
192
|
+
.meta { color: #6b7280; font-size: 0.82rem; font-family: system-ui, sans-serif; margin-bottom: 24px; }
|
|
193
|
+
p { margin: 0 0 1.1em; font-size: 1rem; overflow-wrap: break-word; }
|
|
194
|
+
[data-allegro-interaction] { display: block; margin: 24px 0; }
|
|
195
|
+
</style>
|
|
196
|
+
</head>
|
|
197
|
+
<body>
|
|
198
|
+
<script id="allegro-preview-data" type="application/json">${buildPreviewData(options)}</script>
|
|
199
|
+
<header>
|
|
200
|
+
<h1>Council Approves Decade-Long Transit Overhaul</h1>
|
|
201
|
+
<p class="meta">By Staff Reporter · March 14, 2026</p>
|
|
202
|
+
</header>
|
|
203
|
+
<article id="allegro-preview-article">
|
|
204
|
+
${paras}
|
|
205
|
+
</article>
|
|
206
|
+
${sdkScriptTag(options.sdkUrl)}
|
|
207
|
+
<script src="/preview-client.js"></script>
|
|
208
|
+
</body>
|
|
209
|
+
</html>`;
|
|
210
|
+
}
|
|
211
|
+
var sseClients = /* @__PURE__ */ new Set();
|
|
212
|
+
function broadcast(event) {
|
|
213
|
+
for (const client of sseClients) {
|
|
214
|
+
client.write(`event: ${event}
|
|
215
|
+
data: {}
|
|
216
|
+
|
|
217
|
+
`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function broadcastReload() {
|
|
221
|
+
broadcast("reload");
|
|
222
|
+
}
|
|
223
|
+
function broadcastConfig() {
|
|
224
|
+
broadcast("config");
|
|
225
|
+
}
|
|
226
|
+
function readTemplateMeta(dir) {
|
|
227
|
+
const jsonPath = join(dir, "template.json");
|
|
228
|
+
if (!existsSync(jsonPath)) {
|
|
229
|
+
return { name: basename(dir) };
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const parsed = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
233
|
+
return { name: parsed.name ?? basename(dir) };
|
|
234
|
+
} catch {
|
|
235
|
+
return { name: basename(dir) };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function isTemplateDir(dir) {
|
|
239
|
+
return existsSync(join(dir, "index.html"));
|
|
240
|
+
}
|
|
241
|
+
function scanTemplates(baseDir) {
|
|
242
|
+
const results = [];
|
|
243
|
+
if (isTemplateDir(baseDir)) {
|
|
244
|
+
const meta = readTemplateMeta(baseDir);
|
|
245
|
+
results.push({ slug: ".", name: meta.name });
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
|
|
249
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const sub = join(baseDir, entry.name);
|
|
253
|
+
if (!isTemplateDir(sub)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const meta = readTemplateMeta(sub);
|
|
257
|
+
results.push({ slug: entry.name, name: meta.name });
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
return results;
|
|
262
|
+
}
|
|
263
|
+
var templateFilesCache = /* @__PURE__ */ new Map();
|
|
264
|
+
function invalidateTemplateCache(filePath) {
|
|
265
|
+
const dir = filePath.replace(/[/\\][^/\\]+$/, "");
|
|
266
|
+
templateFilesCache.delete(dir);
|
|
267
|
+
}
|
|
268
|
+
function readTemplateFiles(dir) {
|
|
269
|
+
const read = (filename) => {
|
|
270
|
+
const filePath = join(dir, filename);
|
|
271
|
+
return existsSync(filePath) ? readFileSync(filePath, "utf-8") : "";
|
|
272
|
+
};
|
|
273
|
+
let fields = [];
|
|
274
|
+
let name = basename(dir);
|
|
275
|
+
let fieldsParseError = null;
|
|
276
|
+
const jsonPath = join(dir, "template.json");
|
|
277
|
+
if (existsSync(jsonPath)) {
|
|
278
|
+
try {
|
|
279
|
+
const parsed = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
280
|
+
fields = parsed.fields ?? [];
|
|
281
|
+
name = parsed.name ?? name;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
fieldsParseError = err instanceof Error ? err.message : "Invalid JSON in template.json";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const files = {
|
|
287
|
+
html: read("index.html"),
|
|
288
|
+
css: read("index.css"),
|
|
289
|
+
js: read("index.js"),
|
|
290
|
+
fields,
|
|
291
|
+
name,
|
|
292
|
+
fieldsParseError
|
|
293
|
+
};
|
|
294
|
+
templateFilesCache.set(dir, files);
|
|
295
|
+
return files;
|
|
296
|
+
}
|
|
297
|
+
function readTemplateFilesCached(dir) {
|
|
298
|
+
return templateFilesCache.get(dir) ?? readTemplateFiles(dir);
|
|
299
|
+
}
|
|
300
|
+
async function startServer(templatePath, port = 0, tenant = "") {
|
|
301
|
+
const baseDir = resolve(process.cwd(), templatePath);
|
|
302
|
+
if (!existsSync(baseDir)) {
|
|
303
|
+
console.error(`Directory not found: ${baseDir}`);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
let templates = scanTemplates(baseDir);
|
|
307
|
+
const app = express();
|
|
308
|
+
const clientDistDir = join(__dirname, "client");
|
|
309
|
+
app.use(express.static(clientDistDir));
|
|
310
|
+
app.get("/api/config", (_req, res) => {
|
|
311
|
+
res.json({ defaultSlug: templates[0]?.slug ?? null, templates, tenant });
|
|
312
|
+
});
|
|
313
|
+
app.get("/api/template", (req, res) => {
|
|
314
|
+
const slug = req.query.slug ?? ".";
|
|
315
|
+
if (slug !== "." && !templates.some((template) => template.slug === slug)) {
|
|
316
|
+
res.status(404).json({ error: "Template not found" });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const targetDir = slug === "." ? baseDir : join(baseDir, slug);
|
|
320
|
+
const resolvedBaseDir = resolve(baseDir);
|
|
321
|
+
const resolvedDir = resolve(targetDir);
|
|
322
|
+
const relativePath = relative(resolvedBaseDir, resolvedDir);
|
|
323
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
324
|
+
res.status(400).json({ error: "Invalid template path" });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (!existsSync(resolvedDir) || !isTemplateDir(resolvedDir)) {
|
|
328
|
+
res.status(404).json({ error: "Template not found" });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
res.json(readTemplateFilesCached(resolvedDir));
|
|
332
|
+
});
|
|
333
|
+
app.post("/api/template/create", express.json(), (req, res) => {
|
|
334
|
+
const body = req.body;
|
|
335
|
+
const slug = body.slug;
|
|
336
|
+
if (typeof slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
|
|
337
|
+
res.status(400).json({
|
|
338
|
+
error: "Slug must be lowercase letters, numbers, and hyphens only (e.g. my-template)."
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const targetDir = join(baseDir, slug);
|
|
343
|
+
if (existsSync(targetDir)) {
|
|
344
|
+
res.status(400).json({ error: `A template named "${slug}" already exists.` });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
mkdirSync(targetDir);
|
|
349
|
+
writeFileSync(join(targetDir, "index.html"), BOILERPLATE_HTML);
|
|
350
|
+
writeFileSync(join(targetDir, "index.css"), BOILERPLATE_CSS);
|
|
351
|
+
writeFileSync(
|
|
352
|
+
join(targetDir, "template.json"),
|
|
353
|
+
JSON.stringify({ name: slug, fields: BOILERPLATE_FIELDS }, null, 2) + "\n"
|
|
354
|
+
);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
console.error("[allegro-preview] Failed to create template:", err);
|
|
357
|
+
res.status(500).json({ error: "Failed to create template files." });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
rescan();
|
|
361
|
+
res.status(201).json({ slug });
|
|
362
|
+
});
|
|
363
|
+
app.get("/api/events", (req, res) => {
|
|
364
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
365
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
366
|
+
res.setHeader("Connection", "keep-alive");
|
|
367
|
+
res.flushHeaders();
|
|
368
|
+
sseClients.add(res);
|
|
369
|
+
req.on("close", () => {
|
|
370
|
+
sseClients.delete(res);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
app.get("/preview", (req, res) => {
|
|
374
|
+
const slug = typeof req.query.slug === "string" ? req.query.slug : "";
|
|
375
|
+
const tab = typeof req.query.tab === "string" ? req.query.tab : "standalone";
|
|
376
|
+
const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk : void 0;
|
|
377
|
+
if (!slug) {
|
|
378
|
+
res.status(400).send("Missing slug");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (slug !== "." && !templates.some((t) => t.slug === slug)) {
|
|
382
|
+
res.status(404).send("Template not found");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const targetDir = slug === "." ? baseDir : join(baseDir, slug);
|
|
386
|
+
const resolvedBaseDir = resolve(baseDir);
|
|
387
|
+
const resolvedDir = resolve(targetDir);
|
|
388
|
+
const relativePath = relative(resolvedBaseDir, resolvedDir);
|
|
389
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
390
|
+
res.status(400).send("Invalid template path");
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (!existsSync(resolvedDir) || !isTemplateDir(resolvedDir)) {
|
|
394
|
+
res.status(404).send("Template not found");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const templateFiles = readTemplateFilesCached(resolvedDir);
|
|
398
|
+
const fieldValues = [];
|
|
399
|
+
for (const [key, val] of Object.entries(req.query)) {
|
|
400
|
+
if (key.startsWith("f_") && typeof val === "string") {
|
|
401
|
+
fieldValues.push({ slug: key.slice(2), value: val });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const effectiveSdkUrl = sdkParam ?? (tenant ? normalizeTenantSdkUrl(tenant) : void 0);
|
|
405
|
+
const blockCookies = req.query.blockCookies !== "false";
|
|
406
|
+
const shellOptions = {
|
|
407
|
+
slug,
|
|
408
|
+
html: templateFiles.html,
|
|
409
|
+
css: templateFiles.css,
|
|
410
|
+
js: templateFiles.js,
|
|
411
|
+
fieldValues,
|
|
412
|
+
tab,
|
|
413
|
+
sdkUrl: effectiveSdkUrl,
|
|
414
|
+
blockCookies
|
|
415
|
+
};
|
|
416
|
+
const rendered = tab === "standalone" ? standaloneShell(shellOptions) : articleShell(shellOptions);
|
|
417
|
+
res.setHeader("Cache-Control", "no-store");
|
|
418
|
+
res.type("html").send(rendered);
|
|
419
|
+
});
|
|
420
|
+
const indexHtmlPath = join(clientDistDir, "index.html");
|
|
421
|
+
app.get("/*path", (_req, res) => {
|
|
422
|
+
if (existsSync(indexHtmlPath)) {
|
|
423
|
+
res.type("html").send(readFileSync(indexHtmlPath));
|
|
424
|
+
} else {
|
|
425
|
+
res.status(503).send(
|
|
426
|
+
'Preview client build artifacts not found. Ensure the client is built (for example, by running "npm run build" in this package during development).'
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
const TEMPLATE_FILES = ["index.html", "index.css", "index.js", "template.json"];
|
|
431
|
+
function templateFilePaths(dir) {
|
|
432
|
+
return TEMPLATE_FILES.map((f) => join(dir, f));
|
|
433
|
+
}
|
|
434
|
+
const fileWatcher = chokidar.watch(
|
|
435
|
+
templates.flatMap((t) => templateFilePaths(t.slug === "." ? baseDir : join(baseDir, t.slug))),
|
|
436
|
+
{ ignoreInitial: true }
|
|
437
|
+
);
|
|
438
|
+
fileWatcher.on("change", (changedPath) => {
|
|
439
|
+
console.log(`Changed: ${changedPath}`);
|
|
440
|
+
invalidateTemplateCache(changedPath);
|
|
441
|
+
if (basename(changedPath) === "template.json") {
|
|
442
|
+
rescan();
|
|
443
|
+
}
|
|
444
|
+
broadcastReload();
|
|
445
|
+
});
|
|
446
|
+
fileWatcher.on("add", (addedPath) => {
|
|
447
|
+
console.log(`Added: ${addedPath}`);
|
|
448
|
+
invalidateTemplateCache(addedPath);
|
|
449
|
+
if (basename(addedPath) === "index.html") {
|
|
450
|
+
rescan();
|
|
451
|
+
}
|
|
452
|
+
broadcastReload();
|
|
453
|
+
});
|
|
454
|
+
const dirWatcher = chokidar.watch(baseDir, { depth: 1, ignoreInitial: true });
|
|
455
|
+
function rescan() {
|
|
456
|
+
templates = scanTemplates(baseDir);
|
|
457
|
+
console.log(`Templates updated: ${templates.map((t) => t.slug).join(", ") || "(none)"}`);
|
|
458
|
+
broadcastConfig();
|
|
459
|
+
}
|
|
460
|
+
dirWatcher.on("addDir", (dirPath) => {
|
|
461
|
+
if (dirPath === baseDir) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
setTimeout(() => {
|
|
465
|
+
rescan();
|
|
466
|
+
fileWatcher.add(templateFilePaths(dirPath));
|
|
467
|
+
}, 300);
|
|
468
|
+
});
|
|
469
|
+
dirWatcher.on("unlinkDir", (dirPath) => {
|
|
470
|
+
if (dirPath === baseDir) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
rescan();
|
|
474
|
+
});
|
|
475
|
+
dirWatcher.on("add", (filePath) => {
|
|
476
|
+
if (basename(filePath) === "index.html") {
|
|
477
|
+
rescan();
|
|
478
|
+
fileWatcher.add(templateFilePaths(join(filePath, "..")));
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
const bindServer = (p) => new Promise((resolve2, reject) => {
|
|
482
|
+
const server = app.listen(p);
|
|
483
|
+
server.once("listening", () => {
|
|
484
|
+
const addr = server.address();
|
|
485
|
+
const assignedPort2 = typeof addr === "object" && addr ? addr.port : p;
|
|
486
|
+
resolve2({ server, assignedPort: assignedPort2 });
|
|
487
|
+
});
|
|
488
|
+
server.once("error", reject);
|
|
489
|
+
});
|
|
490
|
+
let assignedPort;
|
|
491
|
+
try {
|
|
492
|
+
({ assignedPort } = await bindServer(port));
|
|
493
|
+
} catch (err) {
|
|
494
|
+
const nodeErr = err;
|
|
495
|
+
if (nodeErr.code === "EADDRINUSE" && port !== 0) {
|
|
496
|
+
console.log(`Port ${port} is in use, using a random available port instead.`);
|
|
497
|
+
({ assignedPort } = await bindServer(0));
|
|
498
|
+
} else {
|
|
499
|
+
throw err;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const url = `http://localhost:${assignedPort}`;
|
|
503
|
+
console.log(`
|
|
504
|
+
allegro-preview \u2192 ${url}
|
|
505
|
+
`);
|
|
506
|
+
void open(url);
|
|
507
|
+
}
|
|
508
|
+
export {
|
|
509
|
+
startServer
|
|
510
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "@allegrocdp/preview",
|
|
4
|
+
"version": "0.1.0-dev.10",
|
|
5
|
+
"description": "Local dev server for previewing Allegro templates",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"allegro-preview": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "npm run build:client && npm run build:cli",
|
|
15
|
+
"build:client": "vite build",
|
|
16
|
+
"build:cli": "tsup",
|
|
17
|
+
"dev": "npm run build:client -- --watch & tsup --watch",
|
|
18
|
+
"types": "tsc --noEmit",
|
|
19
|
+
"format": "prettier --write src/",
|
|
20
|
+
"format:check": "prettier --check src/"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"chokidar": "^4.0.3",
|
|
24
|
+
"commander": "^13.1.0",
|
|
25
|
+
"express": "^5.1.0",
|
|
26
|
+
"open": "^10.1.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@alleyinteractive/allegro-platform": "*",
|
|
30
|
+
"@types/express": "^5.0.1",
|
|
31
|
+
"@types/node": "^22.13.5",
|
|
32
|
+
"prettier": "^3.4.2",
|
|
33
|
+
"prettier-plugin-organize-imports": "^4.1.0",
|
|
34
|
+
"tsup": "^8.4.0",
|
|
35
|
+
"typescript": "^5.7.2",
|
|
36
|
+
"vite": "^7.0.4"
|
|
37
|
+
}
|
|
38
|
+
}
|