astroidjs 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/bin/astroid.mjs +357 -5
- package/dist/auth/index.js +2 -2
- package/dist/commerce/roles.js +1 -1
- package/dist/commerce/sync.js +2 -1
- package/dist/config.js +9 -9
- package/dist/portfolio/scaffold.js +5 -1
- package/dist/project/actions.js +6 -1
- package/dist/project/generate.js +4 -3
- package/dist/project/index.d.ts +3 -0
- package/dist/project/index.js +3 -0
- package/dist/project/previews.d.ts +18 -0
- package/dist/project/previews.js +215 -0
- package/dist/project/provision.d.ts +77 -0
- package/dist/project/provision.js +135 -0
- package/dist/project/release.d.ts +12 -0
- package/dist/project/release.js +118 -0
- package/dist/queues/scaffold.js +5 -0
- package/dist/queues/webhook.d.ts +16 -0
- package/dist/queues/webhook.js +20 -3
- package/dist/schema/collections.js +9 -8
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/media-base.d.ts +13 -0
- package/dist/schema/media-base.js +33 -0
- package/dist/worker/generate.js +44 -18
- package/package.json +3 -3
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// `louise-toolkit/src/...`—that resolves only because the workspace aliases the
|
|
22
22
|
// package to its source, and would break the moment astroid consumes a published
|
|
23
23
|
// tarball (#327).
|
|
24
|
+
import { astroidMediaBase } from "./media-base.js";
|
|
24
25
|
import { defineCollection, } from "louise-toolkit/content/define";
|
|
25
26
|
import { assertValidSections, sanitizeSectionsRichText } from "louise-toolkit/content/sections";
|
|
26
27
|
import { sanitizeRichHtml } from "louise-toolkit/security";
|
|
@@ -39,10 +40,9 @@ import { astroidSectionCatalog } from "../components/sections.js";
|
|
|
39
40
|
* Validated by `defineCollection` at build time, so a malformed field shape throws
|
|
40
41
|
* here rather than at codegen.
|
|
41
42
|
*/
|
|
42
|
-
/** The media base a `pages` write sanitizes rich content against
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
43
|
+
/** The media base a `pages` write sanitizes rich content against, read when the
|
|
44
|
+
* write runs so a Preview checks against its own base (see media-base.ts). */
|
|
45
|
+
const pageMediaBase = astroidMediaBase;
|
|
46
46
|
/**
|
|
47
47
|
* The section catalog a `pages` write is validated + sanitized against: the
|
|
48
48
|
* site's own (`config.sectionCatalog`) when it registered bespoke sections, else
|
|
@@ -113,12 +113,11 @@ export const ASTROID_RESERVED_SLUGS = [
|
|
|
113
113
|
"sitemap.xml",
|
|
114
114
|
];
|
|
115
115
|
export function astroidPagesWriteHooks(config, site = {}) {
|
|
116
|
-
const mediaBase = pageMediaBase(config);
|
|
117
116
|
return {
|
|
118
117
|
// `body` is a richField, so it goes through pagesRoute's own sanitize seam—with
|
|
119
118
|
// the project media base, matching the hook rather than the toolkit
|
|
120
119
|
// default sanitizer that knows no media base.
|
|
121
|
-
sanitize: (html) => sanitizeRichHtml(html, { mediaBase }),
|
|
120
|
+
sanitize: (html) => sanitizeRichHtml(html, { mediaBase: pageMediaBase(config) }),
|
|
122
121
|
// `sections` is not a richField, so it's sanitized here in the transform,
|
|
123
122
|
// which pagesRoute runs BEFORE validate—the hook's sanitize-then-validate
|
|
124
123
|
// order. The site's own transform runs first, so Astroid's sanitize sees
|
|
@@ -136,7 +135,6 @@ export function astroidPagesCollection(config) {
|
|
|
136
135
|
// staged as a draft, so sanitize it on every write—never store raw HTML. A
|
|
137
136
|
// pasted `<img>` pointing off-origin (a hotlink) is dropped: body images must
|
|
138
137
|
// live in the media library. Mirrors the reference site's pages-collection hook.
|
|
139
|
-
const mediaBase = pageMediaBase(config);
|
|
140
138
|
const fields = {};
|
|
141
139
|
fields.slug = { type: "text", required: true };
|
|
142
140
|
fields.title = { type: "text", required: true };
|
|
@@ -158,7 +156,10 @@ export function astroidPagesCollection(config) {
|
|
|
158
156
|
async ({ data }) => {
|
|
159
157
|
let next = data;
|
|
160
158
|
if (typeof next.body === "string") {
|
|
161
|
-
next = {
|
|
159
|
+
next = {
|
|
160
|
+
...next,
|
|
161
|
+
body: sanitizeRichHtml(next.body, { mediaBase: pageMediaBase(config) }),
|
|
162
|
+
};
|
|
162
163
|
}
|
|
163
164
|
// Sanitize BEFORE validating: a richText field stores HTML, and
|
|
164
165
|
// validating the raw value would pass content the sanitizer is about
|
package/dist/schema/index.d.ts
CHANGED
package/dist/schema/index.js
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AstroidConfig } from "../config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Record the running deployment's media base, `vars.MEDIA_URL`. The generated
|
|
4
|
+
* worker calls this at startup; an empty or absent value leaves the config's
|
|
5
|
+
* base in effect.
|
|
6
|
+
*/
|
|
7
|
+
export declare function setAstroidMediaBase(base: string | undefined): void;
|
|
8
|
+
/**
|
|
9
|
+
* The media base a write is checked against: the running deployment's when the
|
|
10
|
+
* worker recorded one, else `deploy.mediaBase`, else `/media`. Call it when the
|
|
11
|
+
* check runs, not when it's built.
|
|
12
|
+
*/
|
|
13
|
+
export declare function astroidMediaBase(config: AstroidConfig): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// The media base the running Worker serves from, for the checks that can't read
|
|
4
|
+
// a request's `env`.
|
|
5
|
+
//
|
|
6
|
+
// The page sanitizers and the settings action are built once, from the config,
|
|
7
|
+
// and drizzle-kit loads the schema that builds them outside a Worker. So they
|
|
8
|
+
// can't take `env.MEDIA_URL` the way the generated routes do. Left on the fixed
|
|
9
|
+
// `deploy.mediaBase`, a staging Preview, which serves media from its own
|
|
10
|
+
// `/media`, drops every image uploaded on it as a hotlink.
|
|
11
|
+
//
|
|
12
|
+
// So the generated worker records `MEDIA_URL` once, at startup, and these checks
|
|
13
|
+
// read it when they run. That's safe as module state because a Worker isolate
|
|
14
|
+
// runs one deployment, and `MEDIA_URL` is fixed per deployment: production's
|
|
15
|
+
// isolates hold production's base, and a Preview's hold the Preview's.
|
|
16
|
+
let runtimeMediaBase;
|
|
17
|
+
/**
|
|
18
|
+
* Record the running deployment's media base, `vars.MEDIA_URL`. The generated
|
|
19
|
+
* worker calls this at startup; an empty or absent value leaves the config's
|
|
20
|
+
* base in effect.
|
|
21
|
+
*/
|
|
22
|
+
export function setAstroidMediaBase(base) {
|
|
23
|
+
const trimmed = base?.replace(/\/+$/, "");
|
|
24
|
+
runtimeMediaBase = trimmed ? trimmed : undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The media base a write is checked against: the running deployment's when the
|
|
28
|
+
* worker recorded one, else `deploy.mediaBase`, else `/media`. Call it when the
|
|
29
|
+
* check runs, not when it's built.
|
|
30
|
+
*/
|
|
31
|
+
export function astroidMediaBase(config) {
|
|
32
|
+
return runtimeMediaBase ?? (config.deploy?.mediaBase ?? "/media").replace(/\/+$/, "");
|
|
33
|
+
}
|
package/dist/worker/generate.js
CHANGED
|
@@ -68,6 +68,7 @@ export function generateAstroidWorker(config) {
|
|
|
68
68
|
const AI_ROUTES = new Set(["ai", "seoFix", "media"]);
|
|
69
69
|
const editorImports = [
|
|
70
70
|
"DEFAULT_PAGE_FIELDS",
|
|
71
|
+
"type PagesWrite",
|
|
71
72
|
...new Set(plan.map((route) => route.factory).filter((f) => !realtimeRouteFactories.has(f))),
|
|
72
73
|
].sort();
|
|
73
74
|
const tables = [
|
|
@@ -104,8 +105,8 @@ export function generateAstroidWorker(config) {
|
|
|
104
105
|
//
|
|
105
106
|
// `versionsTable` makes a DELETE remove the page's version snapshots,
|
|
106
107
|
// which have no foreign key to the page and would otherwise orphan.
|
|
107
|
-
// `afterWrite`
|
|
108
|
-
// stale.
|
|
108
|
+
// `afterWrite` syncs the written page's search index entry, which plain
|
|
109
|
+
// CRUD writes leave stale.
|
|
109
110
|
return 'pagesRoute({ table: pages, versionsTable: pagesVersions, resolveEditor, fields: [...DEFAULT_PAGE_FIELDS, "sections"], ...pagesWriteHooks, afterWrite: reindexPagesSearch })';
|
|
110
111
|
case "save":
|
|
111
112
|
// No `bufferKv` here, deliberately: `saveRoute` has no such option. It
|
|
@@ -122,7 +123,9 @@ export function generateAstroidWorker(config) {
|
|
|
122
123
|
// The site's sanitize + read hooks (config.settings.hooks), spread last
|
|
123
124
|
// so they add to the call rather than replace any part of it.
|
|
124
125
|
const hooksArg = config.settings?.hooks ? ", ...settingsHooks" : "";
|
|
125
|
-
|
|
126
|
+
// Built per request, because the media base is per environment: a
|
|
127
|
+
// Preview's `MEDIA_URL` differs from production's (see `mediaBaseOf`).
|
|
128
|
+
return `(request, env, ctx) => settingsRoute({ table: siteSettings, resolveEditor, columns: SETTINGS_COLUMNS, imageKeys: SETTINGS_IMAGE_KEYS, mediaBase: mediaBaseOf(env)${customArg}${hooksArg} })(request, env, ctx)`;
|
|
126
129
|
}
|
|
127
130
|
// `aiRunner` rather than `(env) => env.AI`: it reads the binding AND the
|
|
128
131
|
// LOUISE_AI kill switch, so all three assists share one definition of
|
|
@@ -173,7 +176,7 @@ export function generateAstroidWorker(config) {
|
|
|
173
176
|
p('import { env } from "cloudflare:workers";');
|
|
174
177
|
p('import { handle } from "@astrojs/cloudflare/handler";');
|
|
175
178
|
p('import type { EditorSession } from "louise-toolkit/auth";');
|
|
176
|
-
p('import {
|
|
179
|
+
p('import { reindexDoc } from "louise-toolkit/content";');
|
|
177
180
|
p(inquiries
|
|
178
181
|
? 'import { db, inquiriesForm } from "louise-toolkit/db";'
|
|
179
182
|
: 'import { db } from "louise-toolkit/db";');
|
|
@@ -201,6 +204,7 @@ export function generateAstroidWorker(config) {
|
|
|
201
204
|
"astroidPagesCollection",
|
|
202
205
|
"astroidPagesWriteHooks",
|
|
203
206
|
"readModuleSecret",
|
|
207
|
+
"setAstroidMediaBase",
|
|
204
208
|
...(inquiries ? ["sendInquiryMail"] : []),
|
|
205
209
|
...(queues ? ["type AstroidQueueMessage"] : []),
|
|
206
210
|
].sort();
|
|
@@ -228,7 +232,18 @@ export function generateAstroidWorker(config) {
|
|
|
228
232
|
p('import { handleQueueMessage } from "./queue.js";');
|
|
229
233
|
}
|
|
230
234
|
p();
|
|
231
|
-
p(`const
|
|
235
|
+
p(`const DEFAULT_MEDIA_BASE = ${JSON.stringify(mediaBase)};`);
|
|
236
|
+
p("// The public media base for this request's environment. `vars.MEDIA_URL`");
|
|
237
|
+
p('// wins, so a Preview sets its own (a path such as "/media", served from the');
|
|
238
|
+
p("// Preview's own host, since a Preview can't know its hostname in advance).");
|
|
239
|
+
p("// Read per request, not baked in: one build serves production and every");
|
|
240
|
+
p("// Preview, so nothing that differs between them can be a constant.");
|
|
241
|
+
p("const mediaBaseOf = (env: CloudflareEnv): string =>");
|
|
242
|
+
p(' (env.MEDIA_URL || DEFAULT_MEDIA_BASE).replace(/\\/+$/, "");');
|
|
243
|
+
p("// The checks built once from the config (the page sanitizers, the settings");
|
|
244
|
+
p("// action) read the base when they run. Recorded at startup, since an isolate");
|
|
245
|
+
p("// runs one deployment and `MEDIA_URL` is fixed per deployment.");
|
|
246
|
+
p("setAstroidMediaBase(env.MEDIA_URL);");
|
|
232
247
|
p("const pagesCollection = astroidPagesCollection(astroidConfig);");
|
|
233
248
|
p("// Sanitize + section-catalog validation for the raw pagesRoute, which runs");
|
|
234
249
|
p("// no collection hook—the same contract versionsRoute gets from the config.");
|
|
@@ -237,10 +252,12 @@ export function generateAstroidWorker(config) {
|
|
|
237
252
|
: "const pagesWriteHooks = astroidPagesWriteHooks(astroidConfig);");
|
|
238
253
|
p();
|
|
239
254
|
p("// pagesRoute writes with plain Drizzle, so the full-text index doesn't see a");
|
|
240
|
-
p("// title or slug change until something
|
|
241
|
-
p("//
|
|
242
|
-
p("
|
|
243
|
-
p("
|
|
255
|
+
p("// title or slug change until something syncs it. This syncs only the page the");
|
|
256
|
+
p("// write touched, rather than rebuilding the whole index on every save. After a");
|
|
257
|
+
p("// delete the row is gone, and reindexDoc removes its entry. Best-effort:");
|
|
258
|
+
p("// pagesRoute swallows a throw here, so a stale index never fails the write.");
|
|
259
|
+
p("async function reindexPagesSearch(_editor: EditorSession, { id }: PagesWrite): Promise<void> {");
|
|
260
|
+
p(" await reindexDoc(db(env.DB), pages, pagesCollection, id);");
|
|
244
261
|
p("}");
|
|
245
262
|
p();
|
|
246
263
|
p("// Editable site_settings columns the Settings panel may write, and which of");
|
|
@@ -294,7 +311,7 @@ export function generateAstroidWorker(config) {
|
|
|
294
311
|
p("// failed crawl or a failed COUNT yields zero rather than aborting the scan,");
|
|
295
312
|
p("// because a partial health report is worth strictly more than none.");
|
|
296
313
|
p("async function runHealthScan(env: CloudflareEnv) {");
|
|
297
|
-
p(" const origin = env.SITE_URL ??
|
|
314
|
+
p(" const origin = env.SITE_URL ?? mediaBaseOf(env);");
|
|
298
315
|
p(" const [brokenLinks, missingAlt, seoGaps] = await Promise.all([");
|
|
299
316
|
p(' checkLinks({ base: origin, paths: ["/"] }).catch(() => []),');
|
|
300
317
|
p(" countRows(env, \"SELECT COUNT(*) AS n FROM media WHERE alt IS NULL OR alt = ''\"),");
|
|
@@ -376,17 +393,26 @@ export function generateAstroidWorker(config) {
|
|
|
376
393
|
}
|
|
377
394
|
p("];");
|
|
378
395
|
p();
|
|
379
|
-
p("// Stream uploaded media back from R2 at
|
|
396
|
+
p("// Stream uploaded media back from R2 at the media base (self-hosted, no public");
|
|
397
|
+
p("// bucket). The base takes two shapes, and each is matched its own way:");
|
|
380
398
|
p("//");
|
|
381
|
-
p(
|
|
382
|
-
p(
|
|
383
|
-
p(
|
|
384
|
-
p("//
|
|
385
|
-
p(
|
|
399
|
+
p('// - An ORIGIN ("https://media.example.com") is a media host, where the whole');
|
|
400
|
+
p("// pathname is the R2 key. Compared by origin, never by path prefix: a");
|
|
401
|
+
p('// `url.pathname` ("/web/foo.jpg") can never start with an origin, and a guard');
|
|
402
|
+
p("// that compared the two once never ran, so every upload 404'd.");
|
|
403
|
+
p('// - A PATH ("/media") is a prefix on the site\'s own host, which is how a');
|
|
404
|
+
p("// Preview serves media, since it can't know its hostname in advance.");
|
|
386
405
|
p("const mediaAssetRoute: WorkerRoute<CloudflareEnv> = async (request, env) => {");
|
|
387
406
|
p(" const url = new URL(request.url);");
|
|
388
|
-
p("
|
|
389
|
-
p(
|
|
407
|
+
p(" const base = mediaBaseOf(env);");
|
|
408
|
+
p(' const path = base.startsWith("/")');
|
|
409
|
+
p(" ? url.pathname.startsWith(`${base}/`)");
|
|
410
|
+
p(" ? url.pathname.slice(base.length + 1)");
|
|
411
|
+
p(' : ""');
|
|
412
|
+
p(" : url.origin === base");
|
|
413
|
+
p(" ? url.pathname.slice(1)");
|
|
414
|
+
p(' : "";');
|
|
415
|
+
p(" const key = decodeURIComponent(path);");
|
|
390
416
|
p(" if (!key) return undefined;");
|
|
391
417
|
p(" const obj = await env.MEDIA.get(key);");
|
|
392
418
|
p(' if (!obj) return new Response("Not found", { status: 404 });');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astroidjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Astroid — an opinionated meta-framework over Louise Toolkit and Astro for building editable, multi-editor sites on Cloudflare Workers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -64,14 +64,14 @@
|
|
|
64
64
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
65
65
|
"@vitest/coverage-v8": "4.1.11",
|
|
66
66
|
"astro": "^7.2.9",
|
|
67
|
-
"louise-toolkit": "^0.
|
|
67
|
+
"louise-toolkit": "^0.34.0",
|
|
68
68
|
"solid-js": "^1.9.15",
|
|
69
69
|
"typescript": "^6.0.3",
|
|
70
70
|
"vitest": "^4.1.11"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"astro": "^7.0.9",
|
|
74
|
-
"louise-toolkit": "^0.
|
|
74
|
+
"louise-toolkit": "^0.34.0",
|
|
75
75
|
"solid-js": "^1.9.0"
|
|
76
76
|
},
|
|
77
77
|
"peerDependenciesMeta": {
|