@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.
- package/dist/core/blocks.d.ts +34 -0
- package/dist/core/blocks.js +7 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +6 -0
- package/dist/core/llms.d.ts +3 -0
- package/dist/core/llms.js +42 -0
- package/dist/core/paths.d.ts +9 -0
- package/dist/core/paths.js +13 -0
- package/dist/core/repository.d.ts +18 -0
- package/dist/core/repository.js +1 -0
- package/dist/core/seo.d.ts +13 -0
- package/dist/core/seo.js +34 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/core/types.js +9 -0
- package/dist/strapi/adapter.d.ts +91 -0
- package/dist/strapi/adapter.js +129 -0
- package/dist/strapi/index.d.ts +11 -0
- package/dist/strapi/index.js +10 -0
- package/dist/strapi/mapper.d.ts +74 -0
- package/dist/strapi/mapper.js +100 -0
- package/dist/strapi-kit/config.d.ts +102 -0
- package/dist/strapi-kit/config.js +95 -0
- package/dist/strapi-kit/index.d.ts +5 -0
- package/dist/strapi-kit/index.js +18 -0
- package/dist/strapi-kit/package.json +1 -0
- package/dist/strapi-kit/permissions.d.ts +15 -0
- package/dist/strapi-kit/permissions.js +33 -0
- package/dist/strapi-kit/platform.d.ts +8 -0
- package/dist/strapi-kit/platform.js +21 -0
- package/dist/strapi-kit/rebuild.d.ts +47 -0
- package/dist/strapi-kit/rebuild.js +123 -0
- package/dist/strapi-kit/schemas-cli.d.ts +5 -0
- package/dist/strapi-kit/schemas-cli.js +104 -0
- package/dist/strapi-kit/schemas.d.ts +56 -0
- package/dist/strapi-kit/schemas.js +180 -0
- package/dist/strapi-kit/seed.d.ts +52 -0
- package/dist/strapi-kit/seed.js +113 -0
- 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
|
+
}
|