@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.
Files changed (107) hide show
  1. package/cli.mjs +75 -0
  2. package/integration.mjs +286 -0
  3. package/package.json +89 -0
  4. package/src/actions/index.ts +502 -0
  5. package/src/components/DesignManager.tsx +542 -0
  6. package/src/components/LinksManager.tsx +694 -0
  7. package/src/components/Modal.astro +65 -0
  8. package/src/components/NavManager.tsx +168 -0
  9. package/src/components/WidgetRenderer.astro +84 -0
  10. package/src/components/builder/PageBuilder.tsx +575 -0
  11. package/src/components/editor/ImagePicker.tsx +215 -0
  12. package/src/components/editor/PostEditor.tsx +1011 -0
  13. package/src/components/editor/PreviewOverlay.tsx +77 -0
  14. package/src/components/editor/extensions/calendar.tsx +230 -0
  15. package/src/components/editor/extensions/cards.tsx +488 -0
  16. package/src/components/editor/extensions/codeBlock.tsx +70 -0
  17. package/src/components/editor/extensions/infoCards.tsx +270 -0
  18. package/src/components/editor/extensions/mediaCards.tsx +376 -0
  19. package/src/components/editor/extensions/postList.tsx +130 -0
  20. package/src/components/editor/extensions/slash.tsx +250 -0
  21. package/src/components/editor/extensions/tableTyping.ts +63 -0
  22. package/src/components/editor/imaging.ts +146 -0
  23. package/src/components/media/CropModal.tsx +229 -0
  24. package/src/components/media/MediaLibrary.tsx +295 -0
  25. package/src/layouts/Admin.astro +119 -0
  26. package/src/layouts/Site.astro +218 -0
  27. package/src/lib/auth.ts +94 -0
  28. package/src/lib/backup/unzip.ts +38 -0
  29. package/src/lib/backup/zip.ts +105 -0
  30. package/src/lib/blocksToHtml.ts +68 -0
  31. package/src/lib/brand.ts +26 -0
  32. package/src/lib/calendar/ics.ts +402 -0
  33. package/src/lib/calendar/render.ts +275 -0
  34. package/src/lib/calendar/src.ts +68 -0
  35. package/src/lib/content.ts +53 -0
  36. package/src/lib/dates.ts +34 -0
  37. package/src/lib/dynamicBlocks.ts +115 -0
  38. package/src/lib/env.ts +6 -0
  39. package/src/lib/highlightCode.ts +44 -0
  40. package/src/lib/i18n.ts +40 -0
  41. package/src/lib/markdown.ts +7 -0
  42. package/src/lib/media/store.ts +95 -0
  43. package/src/lib/net.ts +51 -0
  44. package/src/lib/preview.ts +28 -0
  45. package/src/lib/repo/d1.ts +44 -0
  46. package/src/lib/repo/index.ts +23 -0
  47. package/src/lib/repo/migrations.ts +231 -0
  48. package/src/lib/repo/search.ts +14 -0
  49. package/src/lib/repo/sql.ts +721 -0
  50. package/src/lib/repo/sqlite.ts +43 -0
  51. package/src/lib/repo/types.ts +132 -0
  52. package/src/lib/schema.ts +286 -0
  53. package/src/lib/seo.ts +37 -0
  54. package/src/lib/theme.ts +206 -0
  55. package/src/lib/widgets.ts +108 -0
  56. package/src/middleware.ts +109 -0
  57. package/src/pages/[...slug].astro +43 -0
  58. package/src/pages/admin/c/[collection]/[id].astro +56 -0
  59. package/src/pages/admin/c/[collection]/index.astro +163 -0
  60. package/src/pages/admin/design.astro +42 -0
  61. package/src/pages/admin/index.astro +145 -0
  62. package/src/pages/admin/links.astro +21 -0
  63. package/src/pages/admin/login.astro +83 -0
  64. package/src/pages/admin/logout.ts +7 -0
  65. package/src/pages/admin/media.astro +13 -0
  66. package/src/pages/admin/navigation.astro +17 -0
  67. package/src/pages/admin/pages/[id].astro +49 -0
  68. package/src/pages/admin/pages/index.astro +100 -0
  69. package/src/pages/admin/posts/[id].astro +6 -0
  70. package/src/pages/admin/posts/index.astro +7 -0
  71. package/src/pages/admin/settings.astro +512 -0
  72. package/src/pages/admin/tags.astro +101 -0
  73. package/src/pages/admin/users.astro +149 -0
  74. package/src/pages/api/backup.ts +63 -0
  75. package/src/pages/api/drafts.ts +106 -0
  76. package/src/pages/api/media/upload.ts +89 -0
  77. package/src/pages/api/restore.ts +102 -0
  78. package/src/pages/author/[slug].astro +66 -0
  79. package/src/pages/blog/[slug].astro +240 -0
  80. package/src/pages/blog/index.astro +51 -0
  81. package/src/pages/blog/tag/[slug].astro +83 -0
  82. package/src/pages/embed/[pageId]/[blockId].ts +45 -0
  83. package/src/pages/go/[id].ts +10 -0
  84. package/src/pages/index.astro +52 -0
  85. package/src/pages/links.astro +110 -0
  86. package/src/pages/llms.txt.ts +47 -0
  87. package/src/pages/media/[...key].ts +18 -0
  88. package/src/pages/preview/[type]/[id].astro +52 -0
  89. package/src/pages/robots.txt.ts +28 -0
  90. package/src/pages/rss.xml.ts +58 -0
  91. package/src/pages/search.astro +83 -0
  92. package/src/pages/sitemap.xml.ts +55 -0
  93. package/src/styles/admin.css +652 -0
  94. package/src/styles/builder.css +121 -0
  95. package/src/styles/cards.css +176 -0
  96. package/src/styles/editor.css +596 -0
  97. package/src/styles/site.css +427 -0
  98. package/src/themes/editorial/templates/home.astro +127 -0
  99. package/src/themes/editorial/templates/post-minimal.astro +50 -0
  100. package/src/themes/editorial/theme.css +108 -0
  101. package/src/themes/editorial/theme.json +20 -0
  102. package/src/themes/field/theme.json +17 -0
  103. package/src/themes/ink/theme.json +17 -0
  104. package/src/themes/night/theme.css +7 -0
  105. package/src/themes/night/theme.json +17 -0
  106. package/src/virtual.d.ts +25 -0
  107. 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
+ }
@@ -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
+ }