@acms-subir/core 0.0.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/cli.mjs +75 -0
- package/integration.mjs +286 -0
- package/package.json +89 -0
- package/src/actions/index.ts +502 -0
- package/src/components/DesignManager.tsx +542 -0
- package/src/components/LinksManager.tsx +694 -0
- package/src/components/Modal.astro +65 -0
- package/src/components/NavManager.tsx +168 -0
- package/src/components/WidgetRenderer.astro +84 -0
- package/src/components/builder/PageBuilder.tsx +575 -0
- package/src/components/editor/ImagePicker.tsx +215 -0
- package/src/components/editor/PostEditor.tsx +1011 -0
- package/src/components/editor/PreviewOverlay.tsx +77 -0
- package/src/components/editor/extensions/calendar.tsx +230 -0
- package/src/components/editor/extensions/cards.tsx +488 -0
- package/src/components/editor/extensions/codeBlock.tsx +70 -0
- package/src/components/editor/extensions/infoCards.tsx +270 -0
- package/src/components/editor/extensions/mediaCards.tsx +376 -0
- package/src/components/editor/extensions/postList.tsx +130 -0
- package/src/components/editor/extensions/slash.tsx +250 -0
- package/src/components/editor/extensions/tableTyping.ts +63 -0
- package/src/components/editor/imaging.ts +146 -0
- package/src/components/media/CropModal.tsx +229 -0
- package/src/components/media/MediaLibrary.tsx +295 -0
- package/src/layouts/Admin.astro +119 -0
- package/src/layouts/Site.astro +218 -0
- package/src/lib/auth.ts +94 -0
- package/src/lib/backup/unzip.ts +38 -0
- package/src/lib/backup/zip.ts +105 -0
- package/src/lib/blocksToHtml.ts +68 -0
- package/src/lib/brand.ts +26 -0
- package/src/lib/calendar/ics.ts +402 -0
- package/src/lib/calendar/render.ts +275 -0
- package/src/lib/calendar/src.ts +68 -0
- package/src/lib/content.ts +53 -0
- package/src/lib/dates.ts +34 -0
- package/src/lib/dynamicBlocks.ts +115 -0
- package/src/lib/env.ts +6 -0
- package/src/lib/highlightCode.ts +44 -0
- package/src/lib/i18n.ts +40 -0
- package/src/lib/markdown.ts +7 -0
- package/src/lib/media/store.ts +95 -0
- package/src/lib/net.ts +51 -0
- package/src/lib/preview.ts +28 -0
- package/src/lib/repo/d1.ts +44 -0
- package/src/lib/repo/index.ts +23 -0
- package/src/lib/repo/migrations.ts +231 -0
- package/src/lib/repo/search.ts +14 -0
- package/src/lib/repo/sql.ts +721 -0
- package/src/lib/repo/sqlite.ts +43 -0
- package/src/lib/repo/types.ts +132 -0
- package/src/lib/schema.ts +286 -0
- package/src/lib/seo.ts +37 -0
- package/src/lib/theme.ts +206 -0
- package/src/lib/widgets.ts +108 -0
- package/src/middleware.ts +109 -0
- package/src/pages/[...slug].astro +43 -0
- package/src/pages/admin/c/[collection]/[id].astro +56 -0
- package/src/pages/admin/c/[collection]/index.astro +163 -0
- package/src/pages/admin/design.astro +42 -0
- package/src/pages/admin/index.astro +145 -0
- package/src/pages/admin/links.astro +21 -0
- package/src/pages/admin/login.astro +83 -0
- package/src/pages/admin/logout.ts +7 -0
- package/src/pages/admin/media.astro +13 -0
- package/src/pages/admin/navigation.astro +17 -0
- package/src/pages/admin/pages/[id].astro +49 -0
- package/src/pages/admin/pages/index.astro +100 -0
- package/src/pages/admin/posts/[id].astro +6 -0
- package/src/pages/admin/posts/index.astro +7 -0
- package/src/pages/admin/settings.astro +512 -0
- package/src/pages/admin/tags.astro +101 -0
- package/src/pages/admin/users.astro +149 -0
- package/src/pages/api/backup.ts +63 -0
- package/src/pages/api/drafts.ts +106 -0
- package/src/pages/api/media/upload.ts +89 -0
- package/src/pages/api/restore.ts +102 -0
- package/src/pages/author/[slug].astro +66 -0
- package/src/pages/blog/[slug].astro +240 -0
- package/src/pages/blog/index.astro +51 -0
- package/src/pages/blog/tag/[slug].astro +83 -0
- package/src/pages/embed/[pageId]/[blockId].ts +45 -0
- package/src/pages/go/[id].ts +10 -0
- package/src/pages/index.astro +52 -0
- package/src/pages/links.astro +110 -0
- package/src/pages/llms.txt.ts +47 -0
- package/src/pages/media/[...key].ts +18 -0
- package/src/pages/preview/[type]/[id].astro +52 -0
- package/src/pages/robots.txt.ts +28 -0
- package/src/pages/rss.xml.ts +58 -0
- package/src/pages/search.astro +83 -0
- package/src/pages/sitemap.xml.ts +55 -0
- package/src/styles/admin.css +652 -0
- package/src/styles/builder.css +121 -0
- package/src/styles/cards.css +176 -0
- package/src/styles/editor.css +596 -0
- package/src/styles/site.css +427 -0
- package/src/themes/editorial/templates/home.astro +127 -0
- package/src/themes/editorial/templates/post-minimal.astro +50 -0
- package/src/themes/editorial/theme.css +108 -0
- package/src/themes/editorial/theme.json +20 -0
- package/src/themes/field/theme.json +17 -0
- package/src/themes/ink/theme.json +17 -0
- package/src/themes/night/theme.css +7 -0
- package/src/themes/night/theme.json +17 -0
- package/src/virtual.d.ts +25 -0
- package/src/worker.ts +28 -0
package/cli.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `acms` — tiny maintenance CLI installed with @acms-subir/core.
|
|
3
|
+
// acms update check the registry, install the latest core, print what's new
|
|
4
|
+
// acms doctor sanity-check the project setup
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
const PKG = '@acms-subir/core';
|
|
11
|
+
const cmd = process.argv[2];
|
|
12
|
+
|
|
13
|
+
const read = (p) => { try { return readFileSync(p, 'utf8'); } catch { return ''; } };
|
|
14
|
+
const installedVersion = () => {
|
|
15
|
+
try {
|
|
16
|
+
const require = createRequire(join(process.cwd(), 'package.json'));
|
|
17
|
+
return JSON.parse(readFileSync(require.resolve(`${PKG}/package.json`), 'utf8')).version;
|
|
18
|
+
} catch { return null; }
|
|
19
|
+
};
|
|
20
|
+
const latestVersion = async () => {
|
|
21
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PKG)}/latest`);
|
|
22
|
+
if (!res.ok) throw new Error(`registry responded ${res.status}`);
|
|
23
|
+
return (await res.json()).version;
|
|
24
|
+
};
|
|
25
|
+
const semverLt = (a, b) => {
|
|
26
|
+
const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
|
|
27
|
+
for (let i = 0; i < 3; i++) { if ((pa[i] ?? 0) < (pb[i] ?? 0)) return true; if ((pa[i] ?? 0) > (pb[i] ?? 0)) return false; }
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (cmd === 'update') {
|
|
32
|
+
const current = installedVersion();
|
|
33
|
+
if (!current) { console.error(`✗ ${PKG} is not installed here — run this from your site's folder.`); process.exit(1); }
|
|
34
|
+
let latest;
|
|
35
|
+
try { latest = await latestVersion(); } catch (e) { console.error(`✗ Could not reach the npm registry (${e.message}).`); process.exit(1); }
|
|
36
|
+
if (!semverLt(current, latest)) {
|
|
37
|
+
console.log(`✓ Already up to date (${current}).`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
console.log(`Updating ${PKG} ${current} → ${latest}…`);
|
|
41
|
+
const res = spawnSync('npm', ['install', `${PKG}@latest`, '--no-audit', '--no-fund'], { stdio: 'inherit' });
|
|
42
|
+
if (res.status !== 0) process.exit(res.status ?? 1);
|
|
43
|
+
console.log(`
|
|
44
|
+
✓ Updated to ${latest}. What's new: https://github.com/zurielbm/acms-subir/releases
|
|
45
|
+
Deploy when ready: npm run deploy
|
|
46
|
+
(the database migrates itself on the first request — no manual steps)`);
|
|
47
|
+
} else if (cmd === 'doctor') {
|
|
48
|
+
let bad = 0;
|
|
49
|
+
const check = (ok, label, hint) => {
|
|
50
|
+
console.log(`${ok ? '✓' : '✗'} ${label}${ok || !hint ? '' : `\n ↳ ${hint}`}`);
|
|
51
|
+
if (!ok) bad++;
|
|
52
|
+
};
|
|
53
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
54
|
+
check(major >= 20, `Node ${process.versions.node} (need ≥20)`, 'Upgrade Node.');
|
|
55
|
+
check(installedVersion() !== null, `${PKG} resolvable`, 'npm install');
|
|
56
|
+
const cfg = read('astro.config.mjs') + read('astro.config.ts');
|
|
57
|
+
check(/@acms-subir\/core/.test(cfg), 'astro.config uses the acms integration', "import acms from '@acms-subir/core'");
|
|
58
|
+
check(/export\s+\{\s*server\s*\}/.test(read('src/actions/index.ts')), 'src/actions/index.ts re-exports core actions', "export { server } from '@acms-subir/core/actions';");
|
|
59
|
+
const wrangler = read('wrangler.jsonc') + read('wrangler.json') + read('wrangler.toml');
|
|
60
|
+
if (wrangler) {
|
|
61
|
+
check(!wrangler.includes('00000000-0000-0000-0000-000000000000'), 'wrangler D1 database_id is real', 'npx wrangler d1 create <name>-db, then paste the id.');
|
|
62
|
+
} else {
|
|
63
|
+
console.log('• no wrangler config (Node target?) — skipping Cloudflare checks');
|
|
64
|
+
}
|
|
65
|
+
check(existsSync('.dev.vars') || existsSync('.env'), 'local secrets file (.env / .dev.vars)', 'Set ADMIN_PASSWORD and SESSION_SECRET.');
|
|
66
|
+
console.log(bad === 0 ? '\nAll good.' : `\n${bad} issue${bad > 1 ? 's' : ''} found.`);
|
|
67
|
+
process.exit(bad === 0 ? 0 : 1);
|
|
68
|
+
} else {
|
|
69
|
+
console.log(`acms — maintenance CLI for @acms-subir/core
|
|
70
|
+
|
|
71
|
+
Usage:
|
|
72
|
+
acms update Update the CMS core to the latest release
|
|
73
|
+
acms doctor Check this project's setup`);
|
|
74
|
+
process.exit(cmd ? 1 : 0);
|
|
75
|
+
}
|
package/integration.mjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
// This file is loaded by Node while Astro evaluates the consumer's
|
|
5
|
+
// astro.config, so it must stay plain .mjs — Node does not strip TypeScript
|
|
6
|
+
// from node_modules. Everything it points at (routes, middleware, worker)
|
|
7
|
+
// is processed by Vite inside the consumer's build, so those stay .ts/.astro.
|
|
8
|
+
|
|
9
|
+
const entry = (p) => fileURLToPath(new URL(p, import.meta.url));
|
|
10
|
+
|
|
11
|
+
const CORE_VERSION = JSON.parse(
|
|
12
|
+
readFileSync(new URL('./package.json', import.meta.url), 'utf8')
|
|
13
|
+
).version;
|
|
14
|
+
|
|
15
|
+
// The Workers entrypoint with the scheduled() cron handler. The adapter's
|
|
16
|
+
// workerEntryPoint option needs a real file path, and only the consumer's
|
|
17
|
+
// config can construct the adapter — so this is exported for it:
|
|
18
|
+
// cloudflare({ workerEntryPoint: { path: workerEntrypoint, namedExports: [] } })
|
|
19
|
+
export const workerEntrypoint = entry('./src/worker.ts');
|
|
20
|
+
|
|
21
|
+
// pattern → file under src/pages/, mirroring Astro's file-based conventions.
|
|
22
|
+
// Project-local routes take precedence over injected ones, so a consumer can
|
|
23
|
+
// override any public page by creating the same path in their own src/pages.
|
|
24
|
+
const ROUTES = [
|
|
25
|
+
// public site (post + tag routes are built from config in acms() below)
|
|
26
|
+
['/', 'index.astro'],
|
|
27
|
+
['/[...slug]', '[...slug].astro'],
|
|
28
|
+
['/author/[slug]', 'author/[slug].astro'],
|
|
29
|
+
['/links', 'links.astro'],
|
|
30
|
+
['/search', 'search.astro'],
|
|
31
|
+
['/go/[id]', 'go/[id].ts'],
|
|
32
|
+
['/media/[...key]', 'media/[...key].ts'],
|
|
33
|
+
['/embed/[pageId]/[blockId]', 'embed/[pageId]/[blockId].ts'],
|
|
34
|
+
['/preview/[type]/[id]', 'preview/[type]/[id].astro'],
|
|
35
|
+
['/rss.xml', 'rss.xml.ts'],
|
|
36
|
+
['/sitemap.xml', 'sitemap.xml.ts'],
|
|
37
|
+
['/robots.txt', 'robots.txt.ts'],
|
|
38
|
+
['/llms.txt', 'llms.txt.ts'],
|
|
39
|
+
// admin
|
|
40
|
+
['/admin', 'admin/index.astro'],
|
|
41
|
+
['/admin/design', 'admin/design.astro'],
|
|
42
|
+
['/admin/links', 'admin/links.astro'],
|
|
43
|
+
['/admin/login', 'admin/login.astro'],
|
|
44
|
+
['/admin/logout', 'admin/logout.ts'],
|
|
45
|
+
['/admin/media', 'admin/media.astro'],
|
|
46
|
+
['/admin/navigation', 'admin/navigation.astro'],
|
|
47
|
+
['/admin/pages', 'admin/pages/index.astro'],
|
|
48
|
+
['/admin/pages/[id]', 'admin/pages/[id].astro'],
|
|
49
|
+
// collection-scoped content lists + editors; [collection] is the config key
|
|
50
|
+
['/admin/c/[collection]', 'admin/c/[collection]/index.astro'],
|
|
51
|
+
['/admin/c/[collection]/[id]', 'admin/c/[collection]/[id].astro'],
|
|
52
|
+
// legacy paths for the built-in collection — thin 301s to /admin/c/posts
|
|
53
|
+
['/admin/posts', 'admin/posts/index.astro'],
|
|
54
|
+
['/admin/posts/[id]', 'admin/posts/[id].astro'],
|
|
55
|
+
['/admin/settings', 'admin/settings.astro'],
|
|
56
|
+
['/admin/tags', 'admin/tags.astro'],
|
|
57
|
+
['/admin/users', 'admin/users.astro'],
|
|
58
|
+
// API
|
|
59
|
+
['/api/backup', 'api/backup.ts'],
|
|
60
|
+
['/api/drafts', 'api/drafts.ts'],
|
|
61
|
+
['/api/media/upload', 'api/media/upload.ts'],
|
|
62
|
+
['/api/restore', 'api/restore.ts'],
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// First segments of the fixed routes above — a configured base landing on one
|
|
66
|
+
// of these would shadow a real page, so it's a build-time error.
|
|
67
|
+
const RESERVED_BASES = new Set([
|
|
68
|
+
'/admin', '/api', '/author', '/embed', '/go', '/links', '/media', '/preview', '/search',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
// Collection keys that would collide with admin sections or read ambiguously.
|
|
72
|
+
// The key is the permanent internal identifier (stored on rows, used in
|
|
73
|
+
// /admin/c/<key> URLs) — labels and bases stay freely renameable instead.
|
|
74
|
+
const RESERVED_COLLECTION_KEYS = new Set(['pages', 'tags', 'media', 'links', 'users', 'new']);
|
|
75
|
+
|
|
76
|
+
// Defaults + validation for acms({ content }). Runs while astro.config is
|
|
77
|
+
// evaluated, so a bad key or base fails the build with a fixable message
|
|
78
|
+
// instead of deploying broken routes. The resolved object is what pages see
|
|
79
|
+
// at runtime via virtual:acms-config, so lib/content.ts never re-validates.
|
|
80
|
+
function resolveContent(content = {}) {
|
|
81
|
+
// Unknown keys fail loudly — a typo must never leave the config author
|
|
82
|
+
// staring at an unchanged UI.
|
|
83
|
+
const unknown = Object.keys(content).filter((k) => !['posts', 'collections', 'tags'].includes(k));
|
|
84
|
+
if (unknown.length > 0) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`acms: unknown content key${unknown.length > 1 ? 's' : ''} ${unknown.map((k) => `"${k}"`).join(', ')} — ` +
|
|
87
|
+
'collections go under content.collections; content.posts and content.tags are the other valid keys'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const extra = content.collections ?? {};
|
|
91
|
+
if (content.posts && extra.posts) {
|
|
92
|
+
throw new Error('acms: content.posts and content.collections.posts are both set — configure the built-in collection in one place');
|
|
93
|
+
}
|
|
94
|
+
// `posts` always exists (the built-in collection); content.posts is sugar
|
|
95
|
+
// for content.collections.posts. Insertion order drives sidebar/nav order.
|
|
96
|
+
const defs = { posts: content.posts ?? extra.posts ?? {}, ...Object.fromEntries(Object.entries(extra).filter(([k]) => k !== 'posts')) };
|
|
97
|
+
|
|
98
|
+
const collections = {};
|
|
99
|
+
for (const [key, def] of Object.entries(defs)) {
|
|
100
|
+
if (!/^[a-z0-9-]+$/.test(key)) {
|
|
101
|
+
throw new Error(`acms: collection key "${key}" must use lowercase letters, digits, and hyphens only`);
|
|
102
|
+
}
|
|
103
|
+
if (key !== 'posts' && RESERVED_COLLECTION_KEYS.has(key)) {
|
|
104
|
+
throw new Error(`acms: collection key "${key}" is reserved (it collides with an admin section) — pick another key`);
|
|
105
|
+
}
|
|
106
|
+
const fallbackPlural = key === 'posts' ? 'Posts' : key.charAt(0).toUpperCase() + key.slice(1);
|
|
107
|
+
const plural = def.plural ?? fallbackPlural;
|
|
108
|
+
collections[key] = {
|
|
109
|
+
singular: def.singular ?? (key === 'posts' ? 'Post' : plural.endsWith('s') ? plural.slice(0, -1) : plural),
|
|
110
|
+
plural,
|
|
111
|
+
base: def.base ?? (key === 'posts' ? '/blog' : `/${key}`),
|
|
112
|
+
redirectFrom: def.redirectFrom ?? [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const resolved = { collections, tags: { base: (content.tags ?? {}).base ?? '/tag' } };
|
|
116
|
+
|
|
117
|
+
const seen = new Map();
|
|
118
|
+
const labeled = [
|
|
119
|
+
...Object.entries(collections).map(([k, c]) => [`content.collections.${k}.base`, c.base]),
|
|
120
|
+
['content.tags.base', resolved.tags.base],
|
|
121
|
+
];
|
|
122
|
+
for (const [label, base] of labeled) {
|
|
123
|
+
if (!/^\/[a-z0-9-]+(\/[a-z0-9-]+)*$/.test(base)) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`acms: ${label} must start with "/" and use lowercase letters, digits, and hyphens (no trailing slash) — got "${base}"`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (RESERVED_BASES.has(base)) {
|
|
129
|
+
throw new Error(`acms: ${label} "${base}" collides with a built-in route — pick another path`);
|
|
130
|
+
}
|
|
131
|
+
if (seen.has(base)) {
|
|
132
|
+
throw new Error(`acms: ${label} and ${seen.get(base)} are both "${base}" — bases must be unique`);
|
|
133
|
+
}
|
|
134
|
+
seen.set(base, label);
|
|
135
|
+
}
|
|
136
|
+
return resolved;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Defaults + validation for acms({ i18n }). Locales are URL identifiers
|
|
140
|
+
// (non-default locales are served under /<locale>), so they follow the same
|
|
141
|
+
// build-time rules as collection bases: validated here, never re-checked at
|
|
142
|
+
// runtime. Content stays in the DB — this only declares which languages exist
|
|
143
|
+
// and which one owns the root URL.
|
|
144
|
+
function resolveI18n(i18n, contentBases) {
|
|
145
|
+
if (i18n === undefined) return { locales: [], defaultLocale: '' };
|
|
146
|
+
const unknown = Object.keys(i18n).filter((k) => !['locales', 'defaultLocale'].includes(k));
|
|
147
|
+
if (unknown.length > 0) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`acms: unknown i18n key${unknown.length > 1 ? 's' : ''} ${unknown.map((k) => `"${k}"`).join(', ')} — ` +
|
|
150
|
+
'valid keys are i18n.locales and i18n.defaultLocale'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const locales = i18n.locales ?? [];
|
|
154
|
+
if (!Array.isArray(locales) || locales.length === 0) {
|
|
155
|
+
throw new Error('acms: i18n.locales must be a non-empty array of language codes, e.g. ["en", "es"]');
|
|
156
|
+
}
|
|
157
|
+
for (const l of locales) {
|
|
158
|
+
if (typeof l !== 'string' || !/^[a-z]{2,3}(-[a-z0-9]+)*$/.test(l)) {
|
|
159
|
+
throw new Error(`acms: i18n locale "${l}" must be a lowercase language code like "en", "es", or "pt-br"`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (new Set(locales).size !== locales.length) {
|
|
163
|
+
throw new Error('acms: i18n.locales contains duplicates');
|
|
164
|
+
}
|
|
165
|
+
const defaultLocale = i18n.defaultLocale ?? locales[0];
|
|
166
|
+
if (!locales.includes(defaultLocale)) {
|
|
167
|
+
throw new Error(`acms: i18n.defaultLocale "${defaultLocale}" is not in i18n.locales`);
|
|
168
|
+
}
|
|
169
|
+
// non-default locales become URL prefixes — they must not shadow a route
|
|
170
|
+
for (const l of locales) {
|
|
171
|
+
if (l === defaultLocale) continue;
|
|
172
|
+
const prefix = `/${l}`;
|
|
173
|
+
if (RESERVED_BASES.has(prefix)) {
|
|
174
|
+
throw new Error(`acms: i18n locale "${l}" would be served at "${prefix}", which collides with a built-in route`);
|
|
175
|
+
}
|
|
176
|
+
if (contentBases.has(prefix)) {
|
|
177
|
+
throw new Error(`acms: i18n locale "${l}" would be served at "${prefix}", which collides with ${contentBases.get(prefix)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { locales, defaultLocale };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const CONFIG_ID = 'virtual:acms-config';
|
|
184
|
+
const THEMES_ID = 'virtual:acms-user-themes';
|
|
185
|
+
|
|
186
|
+
// The consumer project's own themes, discovered with root-absolute globs so
|
|
187
|
+
// they resolve against the consumer's src/ — the core's relative globs can't
|
|
188
|
+
// see outside the package. Same folder contract as built-in themes.
|
|
189
|
+
const USER_THEMES_CODE = `
|
|
190
|
+
export const jsons = import.meta.glob('/src/themes/*/theme.json', { eager: true, import: 'default' });
|
|
191
|
+
export const csses = import.meta.glob('/src/themes/*/theme.css', { eager: true, query: '?raw', import: 'default' });
|
|
192
|
+
export const homes = import.meta.glob('/src/themes/*/templates/home.astro', { eager: true });
|
|
193
|
+
export const postTpls = import.meta.glob('/src/themes/*/templates/post-*.astro', { eager: true });
|
|
194
|
+
export const pageTpls = import.meta.glob('/src/themes/*/templates/page-*.astro', { eager: true });
|
|
195
|
+
`;
|
|
196
|
+
|
|
197
|
+
function acmsVitePlugin(userConfig) {
|
|
198
|
+
const configCode = `export const userConfig = ${JSON.stringify(userConfig)};`;
|
|
199
|
+
return {
|
|
200
|
+
name: 'acms-virtual-modules',
|
|
201
|
+
resolveId(id) {
|
|
202
|
+
// deliberately no \0 prefix: vite's import.meta.glob transform must
|
|
203
|
+
// still process the themes module
|
|
204
|
+
if (id === CONFIG_ID || id === THEMES_ID) return id;
|
|
205
|
+
},
|
|
206
|
+
load(id) {
|
|
207
|
+
if (id === CONFIG_ID) return configCode;
|
|
208
|
+
if (id === THEMES_ID) return USER_THEMES_CODE;
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {object} [options]
|
|
215
|
+
* @param {{name?: string, version?: string, stack?: string}} [options.brand]
|
|
216
|
+
* User-visible identity in the admin chrome (sidebar, titles, breadcrumb).
|
|
217
|
+
* @param {{siteTitle?: string, tagline?: string, heroHeading?: string, heroSub?: string}} [options.seed]
|
|
218
|
+
* First-boot database seed content. Only read when the database is empty.
|
|
219
|
+
* @param {{posts?: {singular?: string, plural?: string, base?: string, redirectFrom?: string[]}, collections?: Record<string, {singular?: string, plural?: string, base?: string, redirectFrom?: string[]}>, tags?: {base?: string}}} [options.content]
|
|
220
|
+
* Content collections. `collections` is a keyed map — each key gets its own
|
|
221
|
+
* public routes at `base`, admin section at /admin/c/<key>, and labels from
|
|
222
|
+
* `singular`/`plural`. The `posts` key is the built-in collection (default
|
|
223
|
+
* base '/blog'); `content.posts` is shorthand for `collections.posts`.
|
|
224
|
+
* `redirectFrom` 301s extra legacy bases. Tags are standalone at `tags.base`
|
|
225
|
+
* (default '/tag'), shared across collections. Collection keys are permanent
|
|
226
|
+
* internal identifiers (DB rows, backups, admin URLs) — never renamed.
|
|
227
|
+
* @param {{locales?: string[], defaultLocale?: string}} [options.i18n]
|
|
228
|
+
* Site languages. The default locale (first entry unless `defaultLocale`
|
|
229
|
+
* says otherwise) is served at `/`; every other locale gets its own home
|
|
230
|
+
* page route at `/<locale>` plus a language switcher in the site header.
|
|
231
|
+
* Home page content per language is authored in /admin as a normal page
|
|
232
|
+
* marked "home" with that language selected. Omit for monolingual sites —
|
|
233
|
+
* nothing changes.
|
|
234
|
+
* @returns {import('astro').AstroIntegration}
|
|
235
|
+
*/
|
|
236
|
+
export default function acms(options = {}) {
|
|
237
|
+
const content = resolveContent(options.content);
|
|
238
|
+
const contentBases = new Map([
|
|
239
|
+
...Object.entries(content.collections).map(([k, c]) => [c.base, `content.collections.${k}.base`]),
|
|
240
|
+
[content.tags.base, 'content.tags.base'],
|
|
241
|
+
]);
|
|
242
|
+
const i18n = resolveI18n(options.i18n, contentBases);
|
|
243
|
+
const userConfig = {
|
|
244
|
+
coreVersion: CORE_VERSION,
|
|
245
|
+
brand: options.brand ?? {},
|
|
246
|
+
seed: options.seed ?? {},
|
|
247
|
+
content,
|
|
248
|
+
i18n,
|
|
249
|
+
};
|
|
250
|
+
const routes = [...ROUTES];
|
|
251
|
+
for (const c of Object.values(content.collections)) {
|
|
252
|
+
routes.push([c.base, 'blog/index.astro'], [`${c.base}/[slug]`, 'blog/[slug].astro']);
|
|
253
|
+
}
|
|
254
|
+
routes.push([`${content.tags.base}/[slug]`, 'blog/tag/[slug].astro']);
|
|
255
|
+
// one home route per non-default locale; index.astro resolves the locale
|
|
256
|
+
// from the request path (injectRoute can't pass props)
|
|
257
|
+
for (const l of i18n.locales) {
|
|
258
|
+
if (l !== i18n.defaultLocale) routes.push([`/${l}`, 'index.astro']);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
name: 'acms',
|
|
262
|
+
hooks: {
|
|
263
|
+
'astro:config:setup': ({ command, injectRoute, addMiddleware, updateConfig }) => {
|
|
264
|
+
updateConfig({ vite: { plugins: [acmsVitePlugin(userConfig)] } });
|
|
265
|
+
|
|
266
|
+
for (const [pattern, file] of routes) {
|
|
267
|
+
injectRoute({ pattern, entrypoint: entry(`./src/pages/${file}`) });
|
|
268
|
+
}
|
|
269
|
+
// Auth gate, trailing-slash canonicalization, redirect fallback,
|
|
270
|
+
// publish sweep for targets without cron.
|
|
271
|
+
addMiddleware({ entrypoint: entry('./src/middleware.ts'), order: 'pre' });
|
|
272
|
+
|
|
273
|
+
// workerd has no MessageChannel at module scope, which react-dom's
|
|
274
|
+
// default server build touches. The edge build fixes that but its CJS
|
|
275
|
+
// wrapper breaks under Vite's dev module runner, so the alias applies
|
|
276
|
+
// only to built Worker bundles — same rule the pre-package config had.
|
|
277
|
+
const target = process.env.DEPLOY_TARGET ?? 'cloudflare';
|
|
278
|
+
if (target === 'cloudflare' && command === 'build') {
|
|
279
|
+
updateConfig({
|
|
280
|
+
vite: { resolve: { alias: { 'react-dom/server': 'react-dom/server.edge' } } },
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acms-subir/core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Widget-based CMS for Astro on Cloudflare Workers \u2014 admin UI, content storage, themes, and APIs shipped as one integration.",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"homepage": "https://acms.subir.dev",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/zurielbm/acms-subir.git",
|
|
14
|
+
"directory": "packages/core"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"astro",
|
|
18
|
+
"astro-integration",
|
|
19
|
+
"cms",
|
|
20
|
+
"cloudflare",
|
|
21
|
+
"d1",
|
|
22
|
+
"workers"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"acms": "./cli.mjs"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./integration.mjs",
|
|
29
|
+
"./package.json": "./package.json",
|
|
30
|
+
"./actions": "./src/actions/index.ts",
|
|
31
|
+
"./middleware": "./src/middleware.ts",
|
|
32
|
+
"./worker": "./src/worker.ts",
|
|
33
|
+
"./lib/*": "./src/lib/*",
|
|
34
|
+
"./pages/*": "./src/pages/*",
|
|
35
|
+
"./components/*": "./src/components/*",
|
|
36
|
+
"./layouts/*": "./src/layouts/*",
|
|
37
|
+
"./styles/*": "./src/styles/*",
|
|
38
|
+
"./themes/*": "./src/themes/*"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"integration.mjs",
|
|
42
|
+
"cli.mjs",
|
|
43
|
+
"src"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@dnd-kit/core": "^6.3.1",
|
|
47
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
48
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
49
|
+
"@libsql/client": "^0.15.0",
|
|
50
|
+
"@tiptap/extension-code-block-lowlight": "^3.29.2",
|
|
51
|
+
"@tiptap/extension-table": "^3.29.2",
|
|
52
|
+
"@tiptap/extensions": "^3.29.2",
|
|
53
|
+
"@tiptap/pm": "^3.29.2",
|
|
54
|
+
"@tiptap/react": "^3.29.2",
|
|
55
|
+
"@tiptap/starter-kit": "^3.29.2",
|
|
56
|
+
"@tiptap/suggestion": "^3.29.2",
|
|
57
|
+
"hast-util-to-html": "^9.0.5",
|
|
58
|
+
"lowlight": "^3.3.0",
|
|
59
|
+
"marked": "^15.0.0",
|
|
60
|
+
"prismjs": "^1.30.0",
|
|
61
|
+
"simple-icons": "^16.27.1",
|
|
62
|
+
"zod": "^3.24.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@astrojs/cloudflare": "^12.6.13",
|
|
66
|
+
"@astrojs/node": "^9.2.0",
|
|
67
|
+
"@astrojs/react": "^6.0.2",
|
|
68
|
+
"astro": "^5.7.0",
|
|
69
|
+
"react": "^19.0.0",
|
|
70
|
+
"react-dom": "^19.0.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"@astrojs/cloudflare": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@astrojs/node": {
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"@types/prismjs": "^1.26.6",
|
|
82
|
+
"@types/react": "^19.2.18",
|
|
83
|
+
"@types/react-dom": "^19.2.4",
|
|
84
|
+
"vitest": "^3.2.0"
|
|
85
|
+
},
|
|
86
|
+
"scripts": {
|
|
87
|
+
"test": "vitest run"
|
|
88
|
+
}
|
|
89
|
+
}
|