@janmarkuslanger/astro-strapi-platform 0.1.0 → 0.1.1

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.
Files changed (38) hide show
  1. package/dist/core/blocks.d.ts +34 -0
  2. package/dist/core/blocks.js +7 -0
  3. package/dist/core/index.d.ts +6 -0
  4. package/dist/core/index.js +6 -0
  5. package/dist/core/llms.d.ts +3 -0
  6. package/dist/core/llms.js +42 -0
  7. package/dist/core/paths.d.ts +9 -0
  8. package/dist/core/paths.js +13 -0
  9. package/dist/core/repository.d.ts +18 -0
  10. package/dist/core/repository.js +1 -0
  11. package/dist/core/seo.d.ts +13 -0
  12. package/dist/core/seo.js +34 -0
  13. package/dist/core/types.d.ts +49 -0
  14. package/dist/core/types.js +9 -0
  15. package/dist/strapi/adapter.d.ts +91 -0
  16. package/dist/strapi/adapter.js +129 -0
  17. package/dist/strapi/index.d.ts +11 -0
  18. package/dist/strapi/index.js +10 -0
  19. package/dist/strapi/mapper.d.ts +74 -0
  20. package/dist/strapi/mapper.js +100 -0
  21. package/dist/strapi-kit/config.d.ts +102 -0
  22. package/dist/strapi-kit/config.js +95 -0
  23. package/dist/strapi-kit/index.d.ts +5 -0
  24. package/dist/strapi-kit/index.js +18 -0
  25. package/dist/strapi-kit/package.json +1 -0
  26. package/dist/strapi-kit/permissions.d.ts +15 -0
  27. package/dist/strapi-kit/permissions.js +33 -0
  28. package/dist/strapi-kit/platform.d.ts +8 -0
  29. package/dist/strapi-kit/platform.js +21 -0
  30. package/dist/strapi-kit/rebuild.d.ts +47 -0
  31. package/dist/strapi-kit/rebuild.js +123 -0
  32. package/dist/strapi-kit/schemas-cli.d.ts +5 -0
  33. package/dist/strapi-kit/schemas-cli.js +104 -0
  34. package/dist/strapi-kit/schemas.d.ts +56 -0
  35. package/dist/strapi-kit/schemas.js +180 -0
  36. package/dist/strapi-kit/seed.d.ts +52 -0
  37. package/dist/strapi-kit/seed.js +113 -0
  38. package/package.json +56 -13
@@ -0,0 +1,100 @@
1
+ import { marked } from "marked";
2
+ /**
3
+ * Component id of the reference block. Adapter-internal: references are
4
+ * resolved here and never reach the domain model.
5
+ */
6
+ export const REFERENCE_COMPONENT = "blocks.reference";
7
+ /** Populate query for a dynamic zone holding these blocks plus references. */
8
+ export function blockPopulate(mappings) {
9
+ return [
10
+ ...Object.values(mappings).map((m) => `populate[blocks][on][${m.component}]${m.populate ?? "[populate]=*"}`),
11
+ `populate[blocks][on][${REFERENCE_COMPONENT}][populate]=block`,
12
+ ].join("&");
13
+ }
14
+ /**
15
+ * Media URLs arrive relative from Strapi ("/uploads/x.jpg"). They must become
16
+ * absolute against the PUBLIC base, not the build-time base — the visitor's
17
+ * browser loads them, not the build. See CLAUDE.md §4.
18
+ *
19
+ * Offered to every block's `map` as `ctx.image`, so no block can get this
20
+ * wrong — it is the classic production-only bug.
21
+ */
22
+ export function mapImage(raw, mediaUrl) {
23
+ if (!raw)
24
+ return undefined;
25
+ const src = raw.url ?? "";
26
+ return {
27
+ src: src.startsWith("http") ? src : `${mediaUrl}${src}`,
28
+ alt: raw.alternativeText ?? "",
29
+ width: raw.width ?? undefined,
30
+ height: raw.height ?? undefined,
31
+ };
32
+ }
33
+ function mapLink(raw) {
34
+ if (!raw?.href)
35
+ return undefined;
36
+ return { label: raw.label ?? "", href: raw.href };
37
+ }
38
+ /** Markdown → HTML once, at build time, so components never parse content. */
39
+ function md(value) {
40
+ return value ? marked.parse(String(value), { async: false }) : "";
41
+ }
42
+ /** The helpers handed to every `map`, bound to the public media URL. */
43
+ export function mapContext(mediaUrl) {
44
+ return { image: (value) => mapImage(value, mediaUrl), markdown: md };
45
+ }
46
+ function mapSeo(raw, fallbackTitle) {
47
+ return {
48
+ title: raw?.title ?? fallbackTitle,
49
+ description: raw?.description ?? "",
50
+ };
51
+ }
52
+ /**
53
+ * Turns Strapi's dynamic zone into domain blocks, using the registered
54
+ * mappings.
55
+ *
56
+ * Unknown component ids are SKIPPED, not thrown on: a block added in the CMS
57
+ * before the frontend knows it must never take the site down.
58
+ */
59
+ export function mapBlocks(raw, mediaUrl, mappings, resolve) {
60
+ const ctx = mapContext(mediaUrl);
61
+ const byComponent = new Map(Object.entries(mappings).map(([type, m]) => [m.component, { type, m }]));
62
+ const blocks = [];
63
+ for (const item of raw ?? []) {
64
+ // Resolved here, never surfaced: the domain model has no reference type,
65
+ // so pages cannot tell a shared block from an inline one.
66
+ if (item.__component === REFERENCE_COMPONENT) {
67
+ const id = item.block?.documentId ?? item.block?.id;
68
+ if (id && resolve)
69
+ blocks.push(...resolve(String(id)));
70
+ continue;
71
+ }
72
+ const entry = byComponent.get(item.__component);
73
+ if (!entry)
74
+ continue;
75
+ blocks.push({ ...entry.m.map(item, ctx), type: entry.type });
76
+ }
77
+ return blocks;
78
+ }
79
+ // Site fields first: a platform field always wins, so an extension can add
80
+ // to a page but never change what the platform's own code relies on.
81
+ export function mapPage(entry, mediaUrl, mappings, resolve, extension) {
82
+ return {
83
+ ...extension?.map(entry, mapContext(mediaUrl)),
84
+ slug: entry.slug ?? "",
85
+ title: entry.title ?? "",
86
+ seo: mapSeo(entry.seo, entry.title ?? ""),
87
+ blocks: mapBlocks(entry.blocks ?? [], mediaUrl, mappings, resolve),
88
+ };
89
+ }
90
+ export function mapGlobal(entry, mediaUrl, extension) {
91
+ const links = (raw) => (raw ?? []).map(mapLink).filter(Boolean);
92
+ return {
93
+ ...extension?.map(entry, mapContext(mediaUrl)),
94
+ siteName: entry.siteName ?? "",
95
+ claim: entry.claim ?? undefined,
96
+ email: entry.email ?? "",
97
+ navigation: links(entry.navigation),
98
+ footerLinks: links(entry.footerLinks),
99
+ };
100
+ }
@@ -0,0 +1,102 @@
1
+ import type { Core } from "@strapi/strapi";
2
+ /**
3
+ * The CMS app's config, shared by every site. Each helper has the shape of a
4
+ * Strapi config function, so a site's config file is one line — or spreads
5
+ * the result and overrides what it needs:
6
+ *
7
+ * // config/server.ts
8
+ * export default serverConfig;
9
+ * // config/middlewares.ts, with one more middleware
10
+ * export default (params) => [...middlewaresConfig(params), "global::my-middleware"];
11
+ *
12
+ * The environment variable names are part of the platform: the deployment
13
+ * docs and every site's server .env use them.
14
+ */
15
+ type Params = Core.Config.Shared.ConfigParams;
16
+ export declare const adminConfig: ({ env }: Params) => {
17
+ auth: {
18
+ secret: string | undefined;
19
+ };
20
+ apiToken: {
21
+ salt: string | undefined;
22
+ };
23
+ transfer: {
24
+ token: {
25
+ salt: string | undefined;
26
+ };
27
+ };
28
+ };
29
+ export declare const serverConfig: ({ env }: Params) => {
30
+ host: string;
31
+ port: number;
32
+ app: {
33
+ keys: string[] | undefined;
34
+ };
35
+ url: string;
36
+ };
37
+ /**
38
+ * SQLite locally, Postgres in production. Switch with DATABASE_CLIENT.
39
+ *
40
+ * @param appRoot the CMS app directory; the SQLite file (default
41
+ * `.tmp/data.db`, gitignored) lives under it. Deleting it is the supported
42
+ * way to re-seed.
43
+ */
44
+ export declare const databaseConfig: ({ env }: Params, appRoot: string) => {
45
+ connection: {
46
+ client: string;
47
+ connection: {
48
+ host: string;
49
+ port: number;
50
+ database: string;
51
+ user: string;
52
+ password: string | undefined;
53
+ ssl: boolean;
54
+ filename?: undefined;
55
+ };
56
+ pool: {
57
+ min: number;
58
+ max: number;
59
+ };
60
+ useNullAsDefault?: undefined;
61
+ };
62
+ } | {
63
+ connection: {
64
+ client: string;
65
+ connection: {
66
+ filename: string;
67
+ host?: undefined;
68
+ port?: undefined;
69
+ database?: undefined;
70
+ user?: undefined;
71
+ password?: undefined;
72
+ ssl?: undefined;
73
+ };
74
+ useNullAsDefault: boolean;
75
+ pool?: undefined;
76
+ };
77
+ };
78
+ export declare const middlewaresConfig: ({ env }: Params) => (string | {
79
+ name: string;
80
+ config: object;
81
+ })[];
82
+ export declare const pluginsConfig: ({ env }: Params) => {
83
+ email: {
84
+ config: {
85
+ provider: string;
86
+ providerOptions: {
87
+ host: string | undefined;
88
+ port: number;
89
+ secure: boolean;
90
+ auth: {
91
+ user: string | undefined;
92
+ pass: string | undefined;
93
+ };
94
+ };
95
+ settings: {
96
+ defaultFrom: string | undefined;
97
+ defaultReplyTo: string | (string & Record<never, never>) | undefined;
98
+ };
99
+ };
100
+ };
101
+ };
102
+ export {};
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.pluginsConfig = exports.middlewaresConfig = exports.databaseConfig = exports.serverConfig = exports.adminConfig = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const adminConfig = ({ env }) => ({
9
+ auth: { secret: env("ADMIN_JWT_SECRET") },
10
+ apiToken: { salt: env("API_TOKEN_SALT") },
11
+ transfer: { token: { salt: env("TRANSFER_TOKEN_SALT") } },
12
+ });
13
+ exports.adminConfig = adminConfig;
14
+ const serverConfig = ({ env }) => ({
15
+ host: env("HOST", "0.0.0.0"),
16
+ port: env.int("PORT", 1337),
17
+ app: { keys: env.array("APP_KEYS") },
18
+ // Absolute URLs Strapi generates (media, admin links) use this.
19
+ url: env("PUBLIC_URL", "http://127.0.0.1:1337"),
20
+ });
21
+ exports.serverConfig = serverConfig;
22
+ /**
23
+ * SQLite locally, Postgres in production. Switch with DATABASE_CLIENT.
24
+ *
25
+ * @param appRoot the CMS app directory; the SQLite file (default
26
+ * `.tmp/data.db`, gitignored) lives under it. Deleting it is the supported
27
+ * way to re-seed.
28
+ */
29
+ const databaseConfig = ({ env }, appRoot) => {
30
+ if (env("DATABASE_CLIENT", "sqlite") === "postgres") {
31
+ return {
32
+ connection: {
33
+ client: "postgres",
34
+ connection: {
35
+ host: env("DATABASE_HOST", "127.0.0.1"),
36
+ port: env.int("DATABASE_PORT", 5432),
37
+ database: env("DATABASE_NAME", "strapi"),
38
+ user: env("DATABASE_USERNAME", "strapi"),
39
+ password: env("DATABASE_PASSWORD"),
40
+ ssl: env.bool("DATABASE_SSL", false),
41
+ },
42
+ pool: { min: 0, max: 10 },
43
+ },
44
+ };
45
+ }
46
+ return {
47
+ connection: {
48
+ client: "sqlite",
49
+ connection: { filename: node_path_1.default.join(appRoot, env("DATABASE_FILENAME", ".tmp/data.db")) },
50
+ useNullAsDefault: true,
51
+ },
52
+ };
53
+ };
54
+ exports.databaseConfig = databaseConfig;
55
+ const middlewaresConfig = ({ env }) => [
56
+ "strapi::logger",
57
+ "strapi::errors",
58
+ "strapi::security",
59
+ {
60
+ name: "strapi::cors",
61
+ config: {
62
+ // Restrict to the frontend domains in production. A wide-open CORS
63
+ // policy on a CMS that also receives form posts is an invitation.
64
+ origin: env.array("CORS_ORIGINS", ["http://localhost:4321"]),
65
+ },
66
+ },
67
+ "strapi::poweredBy",
68
+ "strapi::query",
69
+ "strapi::body",
70
+ "strapi::session",
71
+ "strapi::favicon",
72
+ "strapi::public",
73
+ ];
74
+ exports.middlewaresConfig = middlewaresConfig;
75
+ const pluginsConfig = ({ env }) => ({
76
+ email: {
77
+ config: {
78
+ provider: "nodemailer",
79
+ providerOptions: {
80
+ host: env("SMTP_HOST"),
81
+ port: env.int("SMTP_PORT", 587),
82
+ secure: env.bool("SMTP_SECURE", false),
83
+ auth: {
84
+ user: env("SMTP_USERNAME"),
85
+ pass: env("SMTP_PASSWORD"),
86
+ },
87
+ },
88
+ settings: {
89
+ defaultFrom: env("EMAIL_FROM"),
90
+ defaultReplyTo: env("EMAIL_REPLY_TO", env("EMAIL_FROM")),
91
+ },
92
+ },
93
+ },
94
+ });
95
+ exports.pluginsConfig = pluginsConfig;
@@ -0,0 +1,5 @@
1
+ export { setPublicPermissions } from "./permissions";
2
+ export { seedContent, type SeedOptions, type SeedStep } from "./seed";
3
+ export { registerRebuildTrigger, type RebuildOptions } from "./rebuild";
4
+ export { PLATFORM_CONTENT_TYPES, PLATFORM_PUBLIC_ACTIONS } from "./platform";
5
+ export { adminConfig, databaseConfig, middlewaresConfig, pluginsConfig, serverConfig, } from "./config";
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.serverConfig = exports.pluginsConfig = exports.middlewaresConfig = exports.databaseConfig = exports.adminConfig = exports.PLATFORM_PUBLIC_ACTIONS = exports.PLATFORM_CONTENT_TYPES = exports.registerRebuildTrigger = exports.seedContent = exports.setPublicPermissions = void 0;
4
+ var permissions_1 = require("./permissions");
5
+ Object.defineProperty(exports, "setPublicPermissions", { enumerable: true, get: function () { return permissions_1.setPublicPermissions; } });
6
+ var seed_1 = require("./seed");
7
+ Object.defineProperty(exports, "seedContent", { enumerable: true, get: function () { return seed_1.seedContent; } });
8
+ var rebuild_1 = require("./rebuild");
9
+ Object.defineProperty(exports, "registerRebuildTrigger", { enumerable: true, get: function () { return rebuild_1.registerRebuildTrigger; } });
10
+ var platform_1 = require("./platform");
11
+ Object.defineProperty(exports, "PLATFORM_CONTENT_TYPES", { enumerable: true, get: function () { return platform_1.PLATFORM_CONTENT_TYPES; } });
12
+ Object.defineProperty(exports, "PLATFORM_PUBLIC_ACTIONS", { enumerable: true, get: function () { return platform_1.PLATFORM_PUBLIC_ACTIONS; } });
13
+ var config_1 = require("./config");
14
+ Object.defineProperty(exports, "adminConfig", { enumerable: true, get: function () { return config_1.adminConfig; } });
15
+ Object.defineProperty(exports, "databaseConfig", { enumerable: true, get: function () { return config_1.databaseConfig; } });
16
+ Object.defineProperty(exports, "middlewaresConfig", { enumerable: true, get: function () { return config_1.middlewaresConfig; } });
17
+ Object.defineProperty(exports, "pluginsConfig", { enumerable: true, get: function () { return config_1.pluginsConfig; } });
18
+ Object.defineProperty(exports, "serverConfig", { enumerable: true, get: function () { return config_1.serverConfig; } });
@@ -0,0 +1 @@
1
+ { "type": "commonjs" }
@@ -0,0 +1,15 @@
1
+ import type { Core } from "@strapi/strapi";
2
+ /**
3
+ * Grants the public role the given actions (e.g. "api::page.page.find"), so a
4
+ * statically built frontend can fetch without a token. Idempotent.
5
+ *
6
+ * Done in code rather than clicked in the panel: a fresh install has to work
7
+ * without a manual checklist, and a forgotten checkbox shows up as a
8
+ * confusing 403 during the frontend build.
9
+ *
10
+ * Note this is ROLE permission, not FIELD permission. Strapi's field-level
11
+ * permissions are separate and are NOT auto-granted for new fields — after
12
+ * adding a field, editors will report they cannot see it, and the fix is in
13
+ * Settings → Roles in the panel. See CLAUDE.md §5.
14
+ */
15
+ export declare function setPublicPermissions(strapi: Core.Strapi, actions: string[]): Promise<void>;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setPublicPermissions = setPublicPermissions;
4
+ /**
5
+ * Grants the public role the given actions (e.g. "api::page.page.find"), so a
6
+ * statically built frontend can fetch without a token. Idempotent.
7
+ *
8
+ * Done in code rather than clicked in the panel: a fresh install has to work
9
+ * without a manual checklist, and a forgotten checkbox shows up as a
10
+ * confusing 403 during the frontend build.
11
+ *
12
+ * Note this is ROLE permission, not FIELD permission. Strapi's field-level
13
+ * permissions are separate and are NOT auto-granted for new fields — after
14
+ * adding a field, editors will report they cannot see it, and the fix is in
15
+ * Settings → Roles in the panel. See CLAUDE.md §5.
16
+ */
17
+ async function setPublicPermissions(strapi, actions) {
18
+ const publicRole = await strapi.db
19
+ .query("plugin::users-permissions.role")
20
+ .findOne({ where: { type: "public" } });
21
+ if (!publicRole)
22
+ return;
23
+ for (const action of actions) {
24
+ const existing = await strapi.db
25
+ .query("plugin::users-permissions.permission")
26
+ .findOne({ where: { action, role: publicRole.id } });
27
+ if (!existing) {
28
+ await strapi.db
29
+ .query("plugin::users-permissions.permission")
30
+ .create({ data: { action, role: publicRole.id } });
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The content types this package's schemas define (see schemas/). A site
3
+ * spreads these into its own lists, so a platform type added or renamed in a
4
+ * new version is public and watched without a site change.
5
+ */
6
+ export declare const PLATFORM_CONTENT_TYPES: string[];
7
+ /** Read access the frontend needs to the platform types, nothing more. */
8
+ export declare const PLATFORM_PUBLIC_ACTIONS: string[];
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PLATFORM_PUBLIC_ACTIONS = exports.PLATFORM_CONTENT_TYPES = void 0;
4
+ /**
5
+ * The content types this package's schemas define (see schemas/). A site
6
+ * spreads these into its own lists, so a platform type added or renamed in a
7
+ * new version is public and watched without a site change.
8
+ */
9
+ exports.PLATFORM_CONTENT_TYPES = [
10
+ "api::page.page",
11
+ "api::shared-block.shared-block",
12
+ "api::global.global",
13
+ ];
14
+ /** Read access the frontend needs to the platform types, nothing more. */
15
+ exports.PLATFORM_PUBLIC_ACTIONS = [
16
+ "api::page.page.find",
17
+ "api::page.page.findOne",
18
+ "api::shared-block.shared-block.find",
19
+ "api::shared-block.shared-block.findOne",
20
+ "api::global.global.find",
21
+ ];
@@ -0,0 +1,47 @@
1
+ import type { Core } from "@strapi/strapi";
2
+ /**
3
+ * Triggers a rebuild of the static frontend when content changes.
4
+ *
5
+ * The site is static, so a publish is invisible until something rebuilds. This
6
+ * module decides *when*; HOW is a deployment detail and therefore a strategy:
7
+ *
8
+ * hook POST to a URL (Vercel/Netlify/Cloudflare deploy hook, or a CI
9
+ * repository_dispatch endpoint). Works from anywhere.
10
+ * script run a local build script — only when the CMS and the site share a
11
+ * machine. Fastest, no token, works when the internet doesn't. The
12
+ * script is YOURS to write (the template ships none, because what it
13
+ * does depends on the host); docs/deployment-targets.md describes
14
+ * what it has to get right. Point REBUILD_SCRIPT at it.
15
+ * none rebuilds are handled elsewhere (or the site isn't live yet).
16
+ *
17
+ * Three decisions worth knowing:
18
+ *
19
+ * - **Which events count.** Content types with draft & publish only react to
20
+ * publish/unpublish/delete — reacting to every save would rebuild the site
21
+ * for a draft nobody should see yet. Types without draft & publish (global
22
+ * settings, categories) have no publish step, so a plain update IS the
23
+ * change.
24
+ * - **Which content types count.** Form submissions are content too, but a
25
+ * visitor sending a message must never trigger a build.
26
+ * - **Bursts collapse.** Events are collected for a few seconds, so publishing
27
+ * five pages costs one build instead of five.
28
+ *
29
+ * Configuration (all optional):
30
+ * REBUILD_STRATEGY "hook" | "script" | "none" (default: none)
31
+ * REBUILD_ENABLED "false" disables entirely; in development it is off
32
+ * unless set to "true"
33
+ * REBUILD_HOOK_URL strategy=hook: the URL to POST to
34
+ * REBUILD_SCRIPT strategy=script: path to your build script (required —
35
+ * the default path is a convention, not a shipped file)
36
+ * REBUILD_DELAY_MS collection window (default 20000)
37
+ * REBUILD_LOG strategy=script: log file for build output
38
+ */
39
+ export interface RebuildOptions {
40
+ /**
41
+ * Content types whose changes are visible on the site. Deliberately NOT
42
+ * here: form submissions — a visitor sending a message must not cause a
43
+ * deploy.
44
+ */
45
+ watched: string[];
46
+ }
47
+ export declare function registerRebuildTrigger(strapi: Core.Strapi, options: RebuildOptions): void;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerRebuildTrigger = registerRebuildTrigger;
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const PUBLISH_EVENTS = ["entry.publish", "entry.unpublish", "entry.delete"];
11
+ const DIRECT_EVENTS = ["entry.create", "entry.update", "entry.delete"];
12
+ const REPO_ROOT = node_path_1.default.resolve(process.cwd(), "..", "..");
13
+ function resolveConfig() {
14
+ const enabled = process.env.REBUILD_ENABLED === "true" ||
15
+ (process.env.REBUILD_ENABLED !== "false" &&
16
+ process.env.NODE_ENV === "production");
17
+ return {
18
+ enabled,
19
+ strategy: (process.env.REBUILD_STRATEGY ?? "none"),
20
+ hookUrl: process.env.REBUILD_HOOK_URL ?? "",
21
+ script: process.env.REBUILD_SCRIPT ??
22
+ node_path_1.default.join(REPO_ROOT, "deploy", "rebuild-web.sh"),
23
+ delay: Number(process.env.REBUILD_DELAY_MS ?? 20_000),
24
+ logFile: process.env.REBUILD_LOG ??
25
+ node_path_1.default.resolve(REPO_ROOT, "..", "shared", "rebuild.log"),
26
+ };
27
+ }
28
+ function registerRebuildTrigger(strapi, options) {
29
+ const cfg = resolveConfig();
30
+ if (!cfg.enabled || cfg.strategy === "none") {
31
+ strapi.log.info(`Rebuild: inactive (strategy=${cfg.strategy}, enabled=${cfg.enabled}).`);
32
+ return;
33
+ }
34
+ if (cfg.strategy === "hook" && !cfg.hookUrl) {
35
+ strapi.log.warn("Rebuild: strategy=hook but REBUILD_HOOK_URL is empty.");
36
+ return;
37
+ }
38
+ if (cfg.strategy === "script" && !node_fs_1.default.existsSync(cfg.script)) {
39
+ strapi.log.warn(`Rebuild: script not found (${cfg.script}) — content changes will only ` +
40
+ "appear with the next deploy.");
41
+ return;
42
+ }
43
+ let timer = null;
44
+ let pending = 0;
45
+ const runHook = async () => {
46
+ try {
47
+ const res = await fetch(cfg.hookUrl, { method: "POST" });
48
+ if (res.ok)
49
+ strapi.log.info("Rebuild: hook triggered.");
50
+ else
51
+ strapi.log.warn(`Rebuild: hook returned ${res.status}.`);
52
+ }
53
+ catch (error) {
54
+ strapi.log.error(`Rebuild: hook failed: ${error}`);
55
+ }
56
+ };
57
+ const runScript = () => {
58
+ let out = "ignore";
59
+ try {
60
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfg.logFile), { recursive: true });
61
+ out = node_fs_1.default.openSync(cfg.logFile, "a");
62
+ }
63
+ catch (error) {
64
+ strapi.log.warn(`Rebuild: log file not writable (${error}).`);
65
+ }
66
+ // Detached: the build outlives this request and must not keep Strapi
67
+ // waiting. A failed build leaves the published site untouched.
68
+ const child = (0, node_child_process_1.spawn)("bash", [cfg.script], {
69
+ detached: true,
70
+ stdio: ["ignore", out, out],
71
+ });
72
+ // The child holds its own copy; keeping ours would leak one per rebuild.
73
+ if (typeof out === "number")
74
+ node_fs_1.default.closeSync(out);
75
+ child.on("error", (error) => strapi.log.error(`Rebuild: could not start script: ${error}`));
76
+ child.on("exit", (code) => {
77
+ if (code === 0)
78
+ strapi.log.info("Rebuild: site republished.");
79
+ else
80
+ strapi.log.warn(`Rebuild: script exited with ${code} — see ${cfg.logFile}.`);
81
+ });
82
+ child.unref();
83
+ };
84
+ const run = () => {
85
+ const count = pending;
86
+ timer = null;
87
+ pending = 0;
88
+ strapi.log.info(`Rebuild: starting (${count} change${count === 1 ? "" : "s"}).`);
89
+ if (cfg.strategy === "hook")
90
+ void runHook();
91
+ else
92
+ runScript();
93
+ };
94
+ const schedule = (event, uid) => {
95
+ pending += 1;
96
+ strapi.log.debug(`Rebuild: queued ${event} on ${uid}.`);
97
+ if (timer)
98
+ clearTimeout(timer);
99
+ timer = setTimeout(run, cfg.delay);
100
+ timer.unref?.();
101
+ };
102
+ for (const uid of options.watched) {
103
+ let contentType;
104
+ try {
105
+ contentType = strapi.contentType(uid);
106
+ }
107
+ catch {
108
+ // A content type that doesn't exist yet is not an error — the list above
109
+ // may run ahead of a deploy.
110
+ continue;
111
+ }
112
+ const events = contentType?.options?.draftAndPublish
113
+ ? PUBLISH_EVENTS
114
+ : DIRECT_EVENTS;
115
+ for (const event of events) {
116
+ strapi.eventHub.on(event, async (payload) => {
117
+ if (payload?.uid === uid)
118
+ schedule(event, uid);
119
+ });
120
+ }
121
+ }
122
+ strapi.log.info(`Rebuild: active (strategy=${cfg.strategy}, debounce ${Math.round(cfg.delay / 1000)}s).`);
123
+ }
@@ -0,0 +1,5 @@
1
+ export declare function syncSchemas(appDir: string, platformDir?: string): {
2
+ written: number;
3
+ removed: number;
4
+ };
5
+ export declare function main(): void;