create-lacspace-app 2.11.0 → 2.13.0

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 CHANGED
@@ -20,6 +20,10 @@ npx create-lacspace-app my-app --template saas --fullstack
20
20
 
21
21
  You choose the *kind* of site you're building. It writes a **real Next.js 15 + React 19 + Tailwind v4 app** — not a hello-world, but a genuinely **polished, modern site**: a fluid `clamp()` type scale, tight display headings, a refined light **and** dark palette, glass chrome, soft layered shadows, a smooth logo marquee, animated counters, scroll reveals and a shimmering primary CTA — every page filled in, an SEO stack wired end-to-end, and a **26-component UI kit** you can drop in anywhere.
22
22
 
23
+ > **New in v2.12 — real-time & privacy (16 add-ons).** Two more keyless, no-vendor add-ons:
24
+ > - **`realtime`** — live server→browser updates over **Server-Sent Events** (`@lacspace/sse`): a channel hub + a `/live` stream + a React `useSSE` feed. No WebSocket server. Full-stack.
25
+ > - **`consent`** — **GDPR-friendly cookie consent** (`@lacspace/consent`): a drop-in `<ConsentBanner/>`, per-category choices, cookie persistence and `whenConsent()` script-gating. Frontend.
26
+ >
23
27
  > **New in v2.11 — the Web Engagement Kit (14 add-ons).** Three keyless, no-vendor add-ons on brand-new `@lacspace` packages:
24
28
  > - **`push`** — real **browser push notifications** (`@lacspace/web-push`): keyless VAPID, no Firebase/FCM. Generates a service worker, a subscribe button, and a backend that stores subscriptions and sends. Full-stack.
25
29
  > - **`captcha`** — a **privacy-friendly proof-of-work CAPTCHA** (`@lacspace/captcha`): no Google/Cloudflare, no keys, no tracking. A drop-in widget + a backend verify route. Full-stack.
package/dist/index.js CHANGED
@@ -2402,6 +2402,38 @@ var FEATURES = [
2402
2402
  "Push works on https (or localhost); the generated public/sw.js receives the notifications."
2403
2403
  ],
2404
2404
  learn: "https://developer.lacspace.com/packages/web-push"
2405
+ },
2406
+ {
2407
+ key: "consent",
2408
+ label: "Cookie consent",
2409
+ description: "GDPR-friendly cookie consent (@lacspace/consent) \u2014 a drop-in <ConsentBanner/>, per-category choices, cookie persistence and whenConsent() script-gating. Frontend, any template.",
2410
+ deps: { "@lacspace/consent": "^1.0.0" },
2411
+ files: () => ({ "components/consent.tsx": consentComponent() }),
2412
+ nextSteps: [
2413
+ "Render <ConsentBanner/> once in app/layout.tsx: import { ConsentBanner } from '@/components/consent'.",
2414
+ "Gate analytics/marketing scripts: whenConsent(consent, 'analytics', () => { /* load tag */ }).",
2415
+ "The choice is stored in a cookie \u2014 gate server-side too with parseConsentCookie()."
2416
+ ],
2417
+ learn: "https://developer.lacspace.com/packages/consent"
2418
+ },
2419
+ {
2420
+ key: "realtime",
2421
+ label: "Real-time (SSE)",
2422
+ description: "Live server-to-browser updates over Server-Sent Events (@lacspace/sse) \u2014 a channel hub + a /live stream + a React useSSE feed. No WebSocket server. Full-stack.",
2423
+ requiresBackend: true,
2424
+ deps: { "@lacspace/sse": "^1.0.0" },
2425
+ files: () => ({ "app/live/page.tsx": realtimeLivePage() }),
2426
+ backend: () => ({
2427
+ deps: { "@lacspace/sse": "^1.0.0" },
2428
+ files: { "src/routes/live.ts": realtimeRoutesBackend() },
2429
+ routes: [{ path: "/live", handler: "liveRoutes", auth: false, importLine: 'import liveRoutes from "./live.js";' }]
2430
+ }),
2431
+ nextSteps: [
2432
+ "Open http://localhost:3000/live in two tabs; click 'Broadcast' in one \u2014 the other updates instantly.",
2433
+ "Push from anywhere on the server: hub.broadcast({ event: 'message', data }) in src/routes/live.ts.",
2434
+ "SSE auto-reconnects and needs no WebSocket server \u2014 great for notifications and live feeds."
2435
+ ],
2436
+ learn: "https://developer.lacspace.com/packages/sse"
2405
2437
  }
2406
2438
  ];
2407
2439
  var aiChatRoute = (_ctx) => `import { resolveConfig } from "@lacspace/providers";
@@ -6920,6 +6952,69 @@ router.post("/send", asyncHandler(async (req, res) => {
6920
6952
  res.json({ sent: results.filter((r) => r.result && !r.result.expired).length, removed: expired.length });
6921
6953
  }));
6922
6954
 
6955
+ export default router;
6956
+ `;
6957
+ var consentComponent = () => `"use client";
6958
+ // how this works: re-export the consent UI + API from one component. Render
6959
+ // <ConsentBanner/> once in app/layout.tsx; gate scripts with whenConsent().
6960
+ export { ConsentBanner, useConsent } from "@lacspace/consent/react";
6961
+ export { consent, whenConsent } from "@lacspace/consent";
6962
+ `;
6963
+ var realtimeLivePage = () => `"use client";
6964
+ import { useState } from "react";
6965
+ import { useSSE } from "@lacspace/sse/react";
6966
+
6967
+ const API = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
6968
+
6969
+ export default function LivePage() {
6970
+ const [log, setLog] = useState<string[]>([]);
6971
+ const { status } = useSSE(API + "/live", {
6972
+ onEvent: { message: (m) => setLog((prev) => [String((m as { text?: string }).text ?? ""), ...prev].slice(0, 50)) },
6973
+ });
6974
+
6975
+ async function broadcast() {
6976
+ await fetch(API + "/live/broadcast", {
6977
+ method: "POST",
6978
+ headers: { "Content-Type": "application/json" },
6979
+ body: JSON.stringify({ text: "Hello at " + new Date().toLocaleTimeString() }),
6980
+ });
6981
+ }
6982
+
6983
+ return (
6984
+ <main className="mx-auto max-w-lg px-6 py-16">
6985
+ <h1 className="text-2xl font-bold">Live feed</h1>
6986
+ <p className="mt-1 text-muted">Server-Sent Events \u2014 status: {status}.</p>
6987
+ <button className="mt-4 rounded-xl border border-hairline px-4 py-2" onClick={broadcast}>Broadcast a message</button>
6988
+ <ul className="mt-6 space-y-2">
6989
+ {log.map((line, i) => (<li key={i} className="rounded-xl border border-hairline p-3">{line}</li>))}
6990
+ </ul>
6991
+ </main>
6992
+ );
6993
+ }
6994
+ `;
6995
+ var realtimeRoutesBackend = () => `import express from "express";
6996
+ import { SSEHub, sseHandler } from "@lacspace/sse";
6997
+ import { asyncHandler } from "../http.js";
6998
+
6999
+ // how this works: one shared hub holds every open SSE connection. GET /live opens
7000
+ // a stream; POST /live/broadcast pushes a "message" event to everyone connected.
7001
+ // Push from anywhere in your app by importing this hub and calling hub.broadcast().
7002
+ const router = express.Router();
7003
+ const hub = new SSEHub();
7004
+ hub.startHeartbeat();
7005
+
7006
+ router.get("/", (_req, res) => {
7007
+ const client = sseHandler(res, { onClose: () => hub.remove(client) });
7008
+ hub.add(client);
7009
+ client.send({ event: "message", data: { text: "Connected to the live feed." } });
7010
+ });
7011
+
7012
+ router.post("/broadcast", asyncHandler(async (req, res) => {
7013
+ const text = typeof req.body?.text === "string" ? req.body.text : "ping";
7014
+ const sent = hub.broadcast({ event: "message", data: { text } });
7015
+ res.json({ sent });
7016
+ }));
7017
+
6923
7018
  export default router;
6924
7019
  `;
6925
7020
  var scope = (ctx) => `@${ctx.name}`;
@@ -7373,6 +7468,7 @@ var backendPkgJson = (ctx) => {
7373
7468
  "@lacspace/id": "^1.1.0",
7374
7469
  "@lacspace/rate-limit": "^1.2.0",
7375
7470
  "@lacspace/cache": "^1.1.0",
7471
+ "@lacspace/logger": "^1.0.0",
7376
7472
  ...featureDeps,
7377
7473
  [`${scope(ctx)}/types`]: "*"
7378
7474
  },
@@ -7482,21 +7578,54 @@ var backendIndex = (ctx) => `import "./load-env.js"; // MUST be first: fills pro
7482
7578
  import { env } from "./env.js";
7483
7579
  import { connectDb } from "./db.js";
7484
7580
  import { createApp } from "./app.js";
7581
+ import { log } from "./logger.js";
7485
7582
 
7486
7583
  // how this works: connect to MongoDB, build the Express app, then listen.
7487
7584
  async function main(): Promise<void> {
7488
7585
  await connectDb(env.MONGODB_URI);
7489
7586
  const app = createApp();
7490
7587
  app.listen(env.PORT, () => {
7491
- console.log(\`\u{1F680} ${ctx.name} API ready on http://localhost:\${env.PORT}\`);
7588
+ log.info("${ctx.name} API ready", { url: \`http://localhost:\${env.PORT}\` });
7492
7589
  });
7493
7590
  }
7494
7591
 
7495
7592
  main().catch((err) => {
7496
- console.error("Failed to start the API:", err);
7593
+ log.fatal("Failed to start the API", { err });
7497
7594
  process.exit(1);
7498
7595
  });
7499
7596
  `;
7597
+ var backendLogger = () => `import { createLogger, jsonConsole, prettyConsole } from "@lacspace/logger";
7598
+ import type { Request, Response, NextFunction } from "express";
7599
+ import { env } from "./env.js";
7600
+
7601
+ // how this works: one structured logger for the whole API (@lacspace/logger, zero-dep).
7602
+ // In production we emit JSON-per-line (clean for log aggregators); in dev we print a
7603
+ // readable, coloured line. Common secret fields are redacted before anything is logged.
7604
+ const isProd = env.NODE_ENV === "production";
7605
+ export const log = createLogger({
7606
+ level: isProd ? "info" : "debug",
7607
+ transports: [isProd ? jsonConsole() : prettyConsole({ colors: true })],
7608
+ redact: ["password", "token", "authorization", "*.password", "*.token"],
7609
+ });
7610
+
7611
+ // Request logger \u2014 one line per request with method, path, status and duration.
7612
+ // Each request gets a child logger with a short request id you can thread through.
7613
+ export function requestLogger(req: Request, res: Response, next: NextFunction): void {
7614
+ const start = Date.now();
7615
+ const reqId = Math.random().toString(36).slice(2, 10);
7616
+ (req as Request & { log: typeof log }).log = log.child({ reqId });
7617
+ res.on("finish", () => {
7618
+ log.info("request", {
7619
+ reqId,
7620
+ method: req.method,
7621
+ path: req.originalUrl,
7622
+ status: res.statusCode,
7623
+ ms: Date.now() - start,
7624
+ });
7625
+ });
7626
+ next();
7627
+ }
7628
+ `;
7500
7629
  var backendDb = () => `import mongoose from "mongoose";
7501
7630
 
7502
7631
  // how this works: opens the Mongoose connection. Call once at boot.
@@ -7812,16 +7941,19 @@ export default router;
7812
7941
  var backendApp = (ctx) => `import express from "express";
7813
7942
  import cors from "cors";
7814
7943
  import { env } from "./env.js";
7944
+ import { requestLogger } from "./logger.js";
7815
7945
  import { errorHandler } from "./middleware/error.js";
7816
7946
  import { registerRoutes } from "./routes/index.js";
7817
7947
 
7818
7948
  // how this works: assembles the Express app \u2014 CORS for the frontend, JSON parsing,
7819
- // a health check, all route groups (see routes/index.ts), then the error handler LAST.
7949
+ // structured request logging (@lacspace/logger), a health check, all route groups
7950
+ // (see routes/index.ts), then the error handler LAST.
7820
7951
  export function createApp(): express.Express {
7821
7952
  const app = express();
7822
7953
 
7823
7954
  app.use(cors({ origin: env.CORS_ORIGIN.split(",").map((o) => o.trim()), credentials: true }));
7824
7955
  app.use(express.json());
7956
+ app.use(requestLogger); // one structured log line per request
7825
7957
 
7826
7958
  app.get("/health", (_req, res) => { res.json({ ok: true, service: "${ctx.name}-api" }); });
7827
7959
  registerRoutes(app);
@@ -7862,6 +7994,7 @@ function backendFiles(ctx) {
7862
7994
  "backend/src/env.ts": backendEnv(ctx),
7863
7995
  "backend/src/db.ts": backendDb(),
7864
7996
  "backend/src/cache.ts": backendCache(),
7997
+ "backend/src/logger.ts": backendLogger(),
7865
7998
  "backend/src/app.ts": backendApp(ctx),
7866
7999
  "backend/src/http.ts": backendHttp(),
7867
8000
  "backend/src/express.d.ts": backendExpressTypes(),
package/dist/lib.cjs CHANGED
@@ -2406,6 +2406,38 @@ var FEATURES = [
2406
2406
  "Push works on https (or localhost); the generated public/sw.js receives the notifications."
2407
2407
  ],
2408
2408
  learn: "https://developer.lacspace.com/packages/web-push"
2409
+ },
2410
+ {
2411
+ key: "consent",
2412
+ label: "Cookie consent",
2413
+ description: "GDPR-friendly cookie consent (@lacspace/consent) \u2014 a drop-in <ConsentBanner/>, per-category choices, cookie persistence and whenConsent() script-gating. Frontend, any template.",
2414
+ deps: { "@lacspace/consent": "^1.0.0" },
2415
+ files: () => ({ "components/consent.tsx": consentComponent() }),
2416
+ nextSteps: [
2417
+ "Render <ConsentBanner/> once in app/layout.tsx: import { ConsentBanner } from '@/components/consent'.",
2418
+ "Gate analytics/marketing scripts: whenConsent(consent, 'analytics', () => { /* load tag */ }).",
2419
+ "The choice is stored in a cookie \u2014 gate server-side too with parseConsentCookie()."
2420
+ ],
2421
+ learn: "https://developer.lacspace.com/packages/consent"
2422
+ },
2423
+ {
2424
+ key: "realtime",
2425
+ label: "Real-time (SSE)",
2426
+ description: "Live server-to-browser updates over Server-Sent Events (@lacspace/sse) \u2014 a channel hub + a /live stream + a React useSSE feed. No WebSocket server. Full-stack.",
2427
+ requiresBackend: true,
2428
+ deps: { "@lacspace/sse": "^1.0.0" },
2429
+ files: () => ({ "app/live/page.tsx": realtimeLivePage() }),
2430
+ backend: () => ({
2431
+ deps: { "@lacspace/sse": "^1.0.0" },
2432
+ files: { "src/routes/live.ts": realtimeRoutesBackend() },
2433
+ routes: [{ path: "/live", handler: "liveRoutes", auth: false, importLine: 'import liveRoutes from "./live.js";' }]
2434
+ }),
2435
+ nextSteps: [
2436
+ "Open http://localhost:3000/live in two tabs; click 'Broadcast' in one \u2014 the other updates instantly.",
2437
+ "Push from anywhere on the server: hub.broadcast({ event: 'message', data }) in src/routes/live.ts.",
2438
+ "SSE auto-reconnects and needs no WebSocket server \u2014 great for notifications and live feeds."
2439
+ ],
2440
+ learn: "https://developer.lacspace.com/packages/sse"
2409
2441
  }
2410
2442
  ];
2411
2443
  var aiChatRoute = (_ctx) => `import { resolveConfig } from "@lacspace/providers";
@@ -6924,6 +6956,69 @@ router.post("/send", asyncHandler(async (req, res) => {
6924
6956
  res.json({ sent: results.filter((r) => r.result && !r.result.expired).length, removed: expired.length });
6925
6957
  }));
6926
6958
 
6959
+ export default router;
6960
+ `;
6961
+ var consentComponent = () => `"use client";
6962
+ // how this works: re-export the consent UI + API from one component. Render
6963
+ // <ConsentBanner/> once in app/layout.tsx; gate scripts with whenConsent().
6964
+ export { ConsentBanner, useConsent } from "@lacspace/consent/react";
6965
+ export { consent, whenConsent } from "@lacspace/consent";
6966
+ `;
6967
+ var realtimeLivePage = () => `"use client";
6968
+ import { useState } from "react";
6969
+ import { useSSE } from "@lacspace/sse/react";
6970
+
6971
+ const API = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
6972
+
6973
+ export default function LivePage() {
6974
+ const [log, setLog] = useState<string[]>([]);
6975
+ const { status } = useSSE(API + "/live", {
6976
+ onEvent: { message: (m) => setLog((prev) => [String((m as { text?: string }).text ?? ""), ...prev].slice(0, 50)) },
6977
+ });
6978
+
6979
+ async function broadcast() {
6980
+ await fetch(API + "/live/broadcast", {
6981
+ method: "POST",
6982
+ headers: { "Content-Type": "application/json" },
6983
+ body: JSON.stringify({ text: "Hello at " + new Date().toLocaleTimeString() }),
6984
+ });
6985
+ }
6986
+
6987
+ return (
6988
+ <main className="mx-auto max-w-lg px-6 py-16">
6989
+ <h1 className="text-2xl font-bold">Live feed</h1>
6990
+ <p className="mt-1 text-muted">Server-Sent Events \u2014 status: {status}.</p>
6991
+ <button className="mt-4 rounded-xl border border-hairline px-4 py-2" onClick={broadcast}>Broadcast a message</button>
6992
+ <ul className="mt-6 space-y-2">
6993
+ {log.map((line, i) => (<li key={i} className="rounded-xl border border-hairline p-3">{line}</li>))}
6994
+ </ul>
6995
+ </main>
6996
+ );
6997
+ }
6998
+ `;
6999
+ var realtimeRoutesBackend = () => `import express from "express";
7000
+ import { SSEHub, sseHandler } from "@lacspace/sse";
7001
+ import { asyncHandler } from "../http.js";
7002
+
7003
+ // how this works: one shared hub holds every open SSE connection. GET /live opens
7004
+ // a stream; POST /live/broadcast pushes a "message" event to everyone connected.
7005
+ // Push from anywhere in your app by importing this hub and calling hub.broadcast().
7006
+ const router = express.Router();
7007
+ const hub = new SSEHub();
7008
+ hub.startHeartbeat();
7009
+
7010
+ router.get("/", (_req, res) => {
7011
+ const client = sseHandler(res, { onClose: () => hub.remove(client) });
7012
+ hub.add(client);
7013
+ client.send({ event: "message", data: { text: "Connected to the live feed." } });
7014
+ });
7015
+
7016
+ router.post("/broadcast", asyncHandler(async (req, res) => {
7017
+ const text = typeof req.body?.text === "string" ? req.body.text : "ping";
7018
+ const sent = hub.broadcast({ event: "message", data: { text } });
7019
+ res.json({ sent });
7020
+ }));
7021
+
6927
7022
  export default router;
6928
7023
  `;
6929
7024
  var scope = (ctx) => `@${ctx.name}`;
@@ -7377,6 +7472,7 @@ var backendPkgJson = (ctx) => {
7377
7472
  "@lacspace/id": "^1.1.0",
7378
7473
  "@lacspace/rate-limit": "^1.2.0",
7379
7474
  "@lacspace/cache": "^1.1.0",
7475
+ "@lacspace/logger": "^1.0.0",
7380
7476
  ...featureDeps,
7381
7477
  [`${scope(ctx)}/types`]: "*"
7382
7478
  },
@@ -7486,21 +7582,54 @@ var backendIndex = (ctx) => `import "./load-env.js"; // MUST be first: fills pro
7486
7582
  import { env } from "./env.js";
7487
7583
  import { connectDb } from "./db.js";
7488
7584
  import { createApp } from "./app.js";
7585
+ import { log } from "./logger.js";
7489
7586
 
7490
7587
  // how this works: connect to MongoDB, build the Express app, then listen.
7491
7588
  async function main(): Promise<void> {
7492
7589
  await connectDb(env.MONGODB_URI);
7493
7590
  const app = createApp();
7494
7591
  app.listen(env.PORT, () => {
7495
- console.log(\`\u{1F680} ${ctx.name} API ready on http://localhost:\${env.PORT}\`);
7592
+ log.info("${ctx.name} API ready", { url: \`http://localhost:\${env.PORT}\` });
7496
7593
  });
7497
7594
  }
7498
7595
 
7499
7596
  main().catch((err) => {
7500
- console.error("Failed to start the API:", err);
7597
+ log.fatal("Failed to start the API", { err });
7501
7598
  process.exit(1);
7502
7599
  });
7503
7600
  `;
7601
+ var backendLogger = () => `import { createLogger, jsonConsole, prettyConsole } from "@lacspace/logger";
7602
+ import type { Request, Response, NextFunction } from "express";
7603
+ import { env } from "./env.js";
7604
+
7605
+ // how this works: one structured logger for the whole API (@lacspace/logger, zero-dep).
7606
+ // In production we emit JSON-per-line (clean for log aggregators); in dev we print a
7607
+ // readable, coloured line. Common secret fields are redacted before anything is logged.
7608
+ const isProd = env.NODE_ENV === "production";
7609
+ export const log = createLogger({
7610
+ level: isProd ? "info" : "debug",
7611
+ transports: [isProd ? jsonConsole() : prettyConsole({ colors: true })],
7612
+ redact: ["password", "token", "authorization", "*.password", "*.token"],
7613
+ });
7614
+
7615
+ // Request logger \u2014 one line per request with method, path, status and duration.
7616
+ // Each request gets a child logger with a short request id you can thread through.
7617
+ export function requestLogger(req: Request, res: Response, next: NextFunction): void {
7618
+ const start = Date.now();
7619
+ const reqId = Math.random().toString(36).slice(2, 10);
7620
+ (req as Request & { log: typeof log }).log = log.child({ reqId });
7621
+ res.on("finish", () => {
7622
+ log.info("request", {
7623
+ reqId,
7624
+ method: req.method,
7625
+ path: req.originalUrl,
7626
+ status: res.statusCode,
7627
+ ms: Date.now() - start,
7628
+ });
7629
+ });
7630
+ next();
7631
+ }
7632
+ `;
7504
7633
  var backendDb = () => `import mongoose from "mongoose";
7505
7634
 
7506
7635
  // how this works: opens the Mongoose connection. Call once at boot.
@@ -7816,16 +7945,19 @@ export default router;
7816
7945
  var backendApp = (ctx) => `import express from "express";
7817
7946
  import cors from "cors";
7818
7947
  import { env } from "./env.js";
7948
+ import { requestLogger } from "./logger.js";
7819
7949
  import { errorHandler } from "./middleware/error.js";
7820
7950
  import { registerRoutes } from "./routes/index.js";
7821
7951
 
7822
7952
  // how this works: assembles the Express app \u2014 CORS for the frontend, JSON parsing,
7823
- // a health check, all route groups (see routes/index.ts), then the error handler LAST.
7953
+ // structured request logging (@lacspace/logger), a health check, all route groups
7954
+ // (see routes/index.ts), then the error handler LAST.
7824
7955
  export function createApp(): express.Express {
7825
7956
  const app = express();
7826
7957
 
7827
7958
  app.use(cors({ origin: env.CORS_ORIGIN.split(",").map((o) => o.trim()), credentials: true }));
7828
7959
  app.use(express.json());
7960
+ app.use(requestLogger); // one structured log line per request
7829
7961
 
7830
7962
  app.get("/health", (_req, res) => { res.json({ ok: true, service: "${ctx.name}-api" }); });
7831
7963
  registerRoutes(app);
@@ -7866,6 +7998,7 @@ function backendFiles(ctx) {
7866
7998
  "backend/src/env.ts": backendEnv(ctx),
7867
7999
  "backend/src/db.ts": backendDb(),
7868
8000
  "backend/src/cache.ts": backendCache(),
8001
+ "backend/src/logger.ts": backendLogger(),
7869
8002
  "backend/src/app.ts": backendApp(ctx),
7870
8003
  "backend/src/http.ts": backendHttp(),
7871
8004
  "backend/src/express.d.ts": backendExpressTypes(),
package/dist/lib.js CHANGED
@@ -2403,6 +2403,38 @@ var FEATURES = [
2403
2403
  "Push works on https (or localhost); the generated public/sw.js receives the notifications."
2404
2404
  ],
2405
2405
  learn: "https://developer.lacspace.com/packages/web-push"
2406
+ },
2407
+ {
2408
+ key: "consent",
2409
+ label: "Cookie consent",
2410
+ description: "GDPR-friendly cookie consent (@lacspace/consent) \u2014 a drop-in <ConsentBanner/>, per-category choices, cookie persistence and whenConsent() script-gating. Frontend, any template.",
2411
+ deps: { "@lacspace/consent": "^1.0.0" },
2412
+ files: () => ({ "components/consent.tsx": consentComponent() }),
2413
+ nextSteps: [
2414
+ "Render <ConsentBanner/> once in app/layout.tsx: import { ConsentBanner } from '@/components/consent'.",
2415
+ "Gate analytics/marketing scripts: whenConsent(consent, 'analytics', () => { /* load tag */ }).",
2416
+ "The choice is stored in a cookie \u2014 gate server-side too with parseConsentCookie()."
2417
+ ],
2418
+ learn: "https://developer.lacspace.com/packages/consent"
2419
+ },
2420
+ {
2421
+ key: "realtime",
2422
+ label: "Real-time (SSE)",
2423
+ description: "Live server-to-browser updates over Server-Sent Events (@lacspace/sse) \u2014 a channel hub + a /live stream + a React useSSE feed. No WebSocket server. Full-stack.",
2424
+ requiresBackend: true,
2425
+ deps: { "@lacspace/sse": "^1.0.0" },
2426
+ files: () => ({ "app/live/page.tsx": realtimeLivePage() }),
2427
+ backend: () => ({
2428
+ deps: { "@lacspace/sse": "^1.0.0" },
2429
+ files: { "src/routes/live.ts": realtimeRoutesBackend() },
2430
+ routes: [{ path: "/live", handler: "liveRoutes", auth: false, importLine: 'import liveRoutes from "./live.js";' }]
2431
+ }),
2432
+ nextSteps: [
2433
+ "Open http://localhost:3000/live in two tabs; click 'Broadcast' in one \u2014 the other updates instantly.",
2434
+ "Push from anywhere on the server: hub.broadcast({ event: 'message', data }) in src/routes/live.ts.",
2435
+ "SSE auto-reconnects and needs no WebSocket server \u2014 great for notifications and live feeds."
2436
+ ],
2437
+ learn: "https://developer.lacspace.com/packages/sse"
2406
2438
  }
2407
2439
  ];
2408
2440
  var aiChatRoute = (_ctx) => `import { resolveConfig } from "@lacspace/providers";
@@ -6921,6 +6953,69 @@ router.post("/send", asyncHandler(async (req, res) => {
6921
6953
  res.json({ sent: results.filter((r) => r.result && !r.result.expired).length, removed: expired.length });
6922
6954
  }));
6923
6955
 
6956
+ export default router;
6957
+ `;
6958
+ var consentComponent = () => `"use client";
6959
+ // how this works: re-export the consent UI + API from one component. Render
6960
+ // <ConsentBanner/> once in app/layout.tsx; gate scripts with whenConsent().
6961
+ export { ConsentBanner, useConsent } from "@lacspace/consent/react";
6962
+ export { consent, whenConsent } from "@lacspace/consent";
6963
+ `;
6964
+ var realtimeLivePage = () => `"use client";
6965
+ import { useState } from "react";
6966
+ import { useSSE } from "@lacspace/sse/react";
6967
+
6968
+ const API = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
6969
+
6970
+ export default function LivePage() {
6971
+ const [log, setLog] = useState<string[]>([]);
6972
+ const { status } = useSSE(API + "/live", {
6973
+ onEvent: { message: (m) => setLog((prev) => [String((m as { text?: string }).text ?? ""), ...prev].slice(0, 50)) },
6974
+ });
6975
+
6976
+ async function broadcast() {
6977
+ await fetch(API + "/live/broadcast", {
6978
+ method: "POST",
6979
+ headers: { "Content-Type": "application/json" },
6980
+ body: JSON.stringify({ text: "Hello at " + new Date().toLocaleTimeString() }),
6981
+ });
6982
+ }
6983
+
6984
+ return (
6985
+ <main className="mx-auto max-w-lg px-6 py-16">
6986
+ <h1 className="text-2xl font-bold">Live feed</h1>
6987
+ <p className="mt-1 text-muted">Server-Sent Events \u2014 status: {status}.</p>
6988
+ <button className="mt-4 rounded-xl border border-hairline px-4 py-2" onClick={broadcast}>Broadcast a message</button>
6989
+ <ul className="mt-6 space-y-2">
6990
+ {log.map((line, i) => (<li key={i} className="rounded-xl border border-hairline p-3">{line}</li>))}
6991
+ </ul>
6992
+ </main>
6993
+ );
6994
+ }
6995
+ `;
6996
+ var realtimeRoutesBackend = () => `import express from "express";
6997
+ import { SSEHub, sseHandler } from "@lacspace/sse";
6998
+ import { asyncHandler } from "../http.js";
6999
+
7000
+ // how this works: one shared hub holds every open SSE connection. GET /live opens
7001
+ // a stream; POST /live/broadcast pushes a "message" event to everyone connected.
7002
+ // Push from anywhere in your app by importing this hub and calling hub.broadcast().
7003
+ const router = express.Router();
7004
+ const hub = new SSEHub();
7005
+ hub.startHeartbeat();
7006
+
7007
+ router.get("/", (_req, res) => {
7008
+ const client = sseHandler(res, { onClose: () => hub.remove(client) });
7009
+ hub.add(client);
7010
+ client.send({ event: "message", data: { text: "Connected to the live feed." } });
7011
+ });
7012
+
7013
+ router.post("/broadcast", asyncHandler(async (req, res) => {
7014
+ const text = typeof req.body?.text === "string" ? req.body.text : "ping";
7015
+ const sent = hub.broadcast({ event: "message", data: { text } });
7016
+ res.json({ sent });
7017
+ }));
7018
+
6924
7019
  export default router;
6925
7020
  `;
6926
7021
  var scope = (ctx) => `@${ctx.name}`;
@@ -7374,6 +7469,7 @@ var backendPkgJson = (ctx) => {
7374
7469
  "@lacspace/id": "^1.1.0",
7375
7470
  "@lacspace/rate-limit": "^1.2.0",
7376
7471
  "@lacspace/cache": "^1.1.0",
7472
+ "@lacspace/logger": "^1.0.0",
7377
7473
  ...featureDeps,
7378
7474
  [`${scope(ctx)}/types`]: "*"
7379
7475
  },
@@ -7483,21 +7579,54 @@ var backendIndex = (ctx) => `import "./load-env.js"; // MUST be first: fills pro
7483
7579
  import { env } from "./env.js";
7484
7580
  import { connectDb } from "./db.js";
7485
7581
  import { createApp } from "./app.js";
7582
+ import { log } from "./logger.js";
7486
7583
 
7487
7584
  // how this works: connect to MongoDB, build the Express app, then listen.
7488
7585
  async function main(): Promise<void> {
7489
7586
  await connectDb(env.MONGODB_URI);
7490
7587
  const app = createApp();
7491
7588
  app.listen(env.PORT, () => {
7492
- console.log(\`\u{1F680} ${ctx.name} API ready on http://localhost:\${env.PORT}\`);
7589
+ log.info("${ctx.name} API ready", { url: \`http://localhost:\${env.PORT}\` });
7493
7590
  });
7494
7591
  }
7495
7592
 
7496
7593
  main().catch((err) => {
7497
- console.error("Failed to start the API:", err);
7594
+ log.fatal("Failed to start the API", { err });
7498
7595
  process.exit(1);
7499
7596
  });
7500
7597
  `;
7598
+ var backendLogger = () => `import { createLogger, jsonConsole, prettyConsole } from "@lacspace/logger";
7599
+ import type { Request, Response, NextFunction } from "express";
7600
+ import { env } from "./env.js";
7601
+
7602
+ // how this works: one structured logger for the whole API (@lacspace/logger, zero-dep).
7603
+ // In production we emit JSON-per-line (clean for log aggregators); in dev we print a
7604
+ // readable, coloured line. Common secret fields are redacted before anything is logged.
7605
+ const isProd = env.NODE_ENV === "production";
7606
+ export const log = createLogger({
7607
+ level: isProd ? "info" : "debug",
7608
+ transports: [isProd ? jsonConsole() : prettyConsole({ colors: true })],
7609
+ redact: ["password", "token", "authorization", "*.password", "*.token"],
7610
+ });
7611
+
7612
+ // Request logger \u2014 one line per request with method, path, status and duration.
7613
+ // Each request gets a child logger with a short request id you can thread through.
7614
+ export function requestLogger(req: Request, res: Response, next: NextFunction): void {
7615
+ const start = Date.now();
7616
+ const reqId = Math.random().toString(36).slice(2, 10);
7617
+ (req as Request & { log: typeof log }).log = log.child({ reqId });
7618
+ res.on("finish", () => {
7619
+ log.info("request", {
7620
+ reqId,
7621
+ method: req.method,
7622
+ path: req.originalUrl,
7623
+ status: res.statusCode,
7624
+ ms: Date.now() - start,
7625
+ });
7626
+ });
7627
+ next();
7628
+ }
7629
+ `;
7501
7630
  var backendDb = () => `import mongoose from "mongoose";
7502
7631
 
7503
7632
  // how this works: opens the Mongoose connection. Call once at boot.
@@ -7813,16 +7942,19 @@ export default router;
7813
7942
  var backendApp = (ctx) => `import express from "express";
7814
7943
  import cors from "cors";
7815
7944
  import { env } from "./env.js";
7945
+ import { requestLogger } from "./logger.js";
7816
7946
  import { errorHandler } from "./middleware/error.js";
7817
7947
  import { registerRoutes } from "./routes/index.js";
7818
7948
 
7819
7949
  // how this works: assembles the Express app \u2014 CORS for the frontend, JSON parsing,
7820
- // a health check, all route groups (see routes/index.ts), then the error handler LAST.
7950
+ // structured request logging (@lacspace/logger), a health check, all route groups
7951
+ // (see routes/index.ts), then the error handler LAST.
7821
7952
  export function createApp(): express.Express {
7822
7953
  const app = express();
7823
7954
 
7824
7955
  app.use(cors({ origin: env.CORS_ORIGIN.split(",").map((o) => o.trim()), credentials: true }));
7825
7956
  app.use(express.json());
7957
+ app.use(requestLogger); // one structured log line per request
7826
7958
 
7827
7959
  app.get("/health", (_req, res) => { res.json({ ok: true, service: "${ctx.name}-api" }); });
7828
7960
  registerRoutes(app);
@@ -7863,6 +7995,7 @@ function backendFiles(ctx) {
7863
7995
  "backend/src/env.ts": backendEnv(ctx),
7864
7996
  "backend/src/db.ts": backendDb(),
7865
7997
  "backend/src/cache.ts": backendCache(),
7998
+ "backend/src/logger.ts": backendLogger(),
7866
7999
  "backend/src/app.ts": backendApp(ctx),
7867
8000
  "backend/src/http.ts": backendHttp(),
7868
8001
  "backend/src/express.d.ts": backendExpressTypes(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lacspace-app",
3
- "version": "2.11.0",
3
+ "version": "2.13.0",
4
4
  "description": "Scaffold a beautiful, production-ready Next.js app from a Lacspace template — portfolio, business, e-commerce, SaaS, blog, docs, dashboard, restaurant or marketplace — pre-wired with SEO, security headers, sitemap and robots. Use it as a CLI (like create-next-app, but you start gorgeous) or import it as a library to generate projects programmatically.",
5
5
  "type": "module",
6
6
  "bin": {