@micacms/astro 0.0.2
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/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/index.d.ts +163 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +221 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +15 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +19 -0
- package/dist/manifest.js.map +1 -0
- package/dist/read.d.ts +44 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +186 -0
- package/dist/read.js.map +1 -0
- package/dist/richtext.d.ts +46 -0
- package/dist/richtext.d.ts.map +1 -0
- package/dist/richtext.js +45 -0
- package/dist/richtext.js.map +1 -0
- package/dist/root.d.ts +18 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +34 -0
- package/dist/root.js.map +1 -0
- package/dist/source.d.ts +42 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +113 -0
- package/dist/source.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Müdespacher
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# @micacms/astro
|
|
2
|
+
|
|
3
|
+
> **Experimental.** Pre-1.0: the API and the content format can still change
|
|
4
|
+
> without notice. Use it at your own risk.
|
|
5
|
+
|
|
6
|
+
Reads a Mica content repository as Astro Content Collections, through the
|
|
7
|
+
Content Layer API — and publishes the content model for Mica to read back.
|
|
8
|
+
|
|
9
|
+
The build needs no CMS running, no URL and no token. Content is files in the
|
|
10
|
+
checkout the build is already in, so the website cannot be broken by the CMS
|
|
11
|
+
being down, slow, or mid-deploy.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// src/mica.ts
|
|
15
|
+
import { collection, defineSite, slug, text } from "@micacms/model";
|
|
16
|
+
|
|
17
|
+
export const site = defineSite({
|
|
18
|
+
root: "./content",
|
|
19
|
+
collections: {
|
|
20
|
+
news: collection({
|
|
21
|
+
label: "News",
|
|
22
|
+
fields: { title: text({ required: true }), slug: slug({ from: "title" }) },
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// src/content.config.ts
|
|
30
|
+
import { micaCollections } from "@micacms/astro";
|
|
31
|
+
|
|
32
|
+
import { site } from "./mica";
|
|
33
|
+
|
|
34
|
+
export const collections = micaCollections(site);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That is the whole setup. **Nothing goes in `astro.config`** — which is also what
|
|
38
|
+
lets `content.config.ts` import `astro:content`, a virtual module that only
|
|
39
|
+
exists inside the app's own module graph.
|
|
40
|
+
|
|
41
|
+
Pages then use ordinary Astro:
|
|
42
|
+
|
|
43
|
+
```astro
|
|
44
|
+
---
|
|
45
|
+
import { getCollection } from "astro:content";
|
|
46
|
+
const news = await getCollection("news");
|
|
47
|
+
---
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`getCollection("news")` is fully typed from the model, with no code generation
|
|
51
|
+
step — so renaming a field breaks the build rather than the page.
|
|
52
|
+
|
|
53
|
+
## What `micaCollections` does
|
|
54
|
+
|
|
55
|
+
One loader and one Zod schema per collection and singleton. Each document
|
|
56
|
+
carries a `_mica` field with what the model knows about it; a singleton becomes
|
|
57
|
+
a one-entry collection, reachable with the same `getEntry` as everything else.
|
|
58
|
+
|
|
59
|
+
The loaders also do the two things that used to need an integration, because
|
|
60
|
+
Astro hands a loader exactly what they require:
|
|
61
|
+
|
|
62
|
+
- **Publish the model** to `<root>/.mica/schema.json`, once per build or
|
|
63
|
+
dev-server start. Commit that file: it is how a Mica deployment learns what
|
|
64
|
+
this project defines. `publishModel: false` turns it off, for a project that
|
|
65
|
+
is not edited through Mica.
|
|
66
|
+
- **Reload in dev.** Astro's watcher follows the module graph and content files
|
|
67
|
+
are not in it, because no page imports `projects/haus-a/index.json`. Each
|
|
68
|
+
loader watches its own directory and coalesces the burst of writes that saving
|
|
69
|
+
one entry produces (`debounceMs`, 100 ms by default).
|
|
70
|
+
|
|
71
|
+
## Mixing with your own collections
|
|
72
|
+
|
|
73
|
+
The return value is a plain object:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
export const collections = {
|
|
77
|
+
...micaCollections(site),
|
|
78
|
+
docs: defineCollection({ loader: glob({ pattern: "**/*.md", base: "./src/docs" }) }),
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Mica edits what is in the model and does not know the rest exists. A save in one
|
|
83
|
+
of its collections reloads that collection only — never a foreign loader.
|
|
84
|
+
|
|
85
|
+
## Rich text
|
|
86
|
+
|
|
87
|
+
`renderRichText(source)` turns a Markdoc field into an HTML string;
|
|
88
|
+
`transformRichText(source)` returns the renderable tree instead, for a project
|
|
89
|
+
that wants to render with its own components.
|
|
90
|
+
|
|
91
|
+
## Install
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install @micacms/astro
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Astro 5 is a peer dependency. Requires Node 22.5 or newer. Licence: MIT.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @micacms/astro — a Git repository as an Astro content source.
|
|
3
|
+
*
|
|
4
|
+
* // src/content.config.ts
|
|
5
|
+
* import { micaCollections } from "@micacms/astro";
|
|
6
|
+
* import { defineSite } from "@micacms/model";
|
|
7
|
+
*
|
|
8
|
+
* export const site = defineSite({ collections: { … } });
|
|
9
|
+
* export const collections = micaCollections(site, { root: "./content" });
|
|
10
|
+
*
|
|
11
|
+
* From there the website uses ordinary Astro APIs — `getCollection("projects")`
|
|
12
|
+
* — and knows nothing about how the content arrived.
|
|
13
|
+
*
|
|
14
|
+
* There is no CMS in this path. The loaders read files that are already in the
|
|
15
|
+
* working tree, so a build needs a checkout and nothing else: no running
|
|
16
|
+
* server, no token, no network. That is the point of the Git-native design.
|
|
17
|
+
* Publishing is a commit, and CI builds the commit.
|
|
18
|
+
*/
|
|
19
|
+
import { type ContentSource, type InferCollection, type InferSingleton, type Site } from "@micacms/model";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { DEFAULT_CONTENT_ROOT, resolveContentRoot } from "./root.js";
|
|
22
|
+
export interface MicaCollectionsOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Overrides `defineSite({ root })`, which is where the content root belongs.
|
|
25
|
+
*
|
|
26
|
+
* Relative paths resolve against the Astro project root, like Astro's own
|
|
27
|
+
* `glob({ base })`. An absolute path is used as given — which is what a
|
|
28
|
+
* project whose content is a separate repository, checked out by CI
|
|
29
|
+
* somewhere else, wants.
|
|
30
|
+
*/
|
|
31
|
+
root?: string;
|
|
32
|
+
/** Overrides `root` entirely. Used by tests, and by a non-filesystem source. */
|
|
33
|
+
source?: ContentSource;
|
|
34
|
+
/**
|
|
35
|
+
* Turns off publishing the model to `<root>/.mica/schema.json`.
|
|
36
|
+
*
|
|
37
|
+
* Only for a project that is not edited through Mica at all: without that
|
|
38
|
+
* file in the repository, Mica has no model and refuses to start.
|
|
39
|
+
*/
|
|
40
|
+
publishModel?: false;
|
|
41
|
+
/**
|
|
42
|
+
* How long to wait after a content change before reloading, in dev.
|
|
43
|
+
*
|
|
44
|
+
* Saving one entry touches several files — `index.json` and every `.mdoc`
|
|
45
|
+
* beside it — and a Git checkout touches all of them at once. One reload
|
|
46
|
+
* after the burst is both faster and less confusing than one per file.
|
|
47
|
+
*/
|
|
48
|
+
debounceMs?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Entry metadata attached to every collection document.
|
|
52
|
+
*
|
|
53
|
+
* Much smaller than it was when a database backed the CMS. Timestamps and
|
|
54
|
+
* version numbers came from table columns that no longer exist; in Git the
|
|
55
|
+
* equivalents are commit metadata, which a static build has no cheap way to
|
|
56
|
+
* read and no strong reason to need. A date an editor cares about belongs in
|
|
57
|
+
* the model as a `date()` field, where it is editorial rather than incidental.
|
|
58
|
+
*
|
|
59
|
+
* Namespaced because these keys share a namespace with the customer's own
|
|
60
|
+
* fields, and `position` is a plausible field name.
|
|
61
|
+
*/
|
|
62
|
+
export interface MicaMeta {
|
|
63
|
+
/** The entry slug, which is its identity in the repository. */
|
|
64
|
+
id: string;
|
|
65
|
+
slug: string;
|
|
66
|
+
collection: string;
|
|
67
|
+
/** Index in the collection's order, manual when `_order.json` says so. */
|
|
68
|
+
position: number;
|
|
69
|
+
}
|
|
70
|
+
/** What a document looks like inside Astro: the payload plus `_mica`. */
|
|
71
|
+
export type AstroDocument<T> = T & {
|
|
72
|
+
_mica: MicaMeta;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The shape Astro's config loader expects.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately not built with `defineCollection` from `astro:content`: that is a
|
|
78
|
+
* virtual module, resolvable only inside an Astro app's module graph, and this
|
|
79
|
+
* package is pre-compiled. `defineCollection` does nothing more than stamp this
|
|
80
|
+
* `type`, so producing the object directly keeps the package importable
|
|
81
|
+
* anywhere — including from a plain unit test.
|
|
82
|
+
*/
|
|
83
|
+
export interface AstroCollectionConfig<T = unknown> {
|
|
84
|
+
type: "content_layer";
|
|
85
|
+
loader: ContentLoader;
|
|
86
|
+
/**
|
|
87
|
+
* Typed as `ZodType<T>` rather than a bare `ZodObject`, because Astro derives
|
|
88
|
+
* `CollectionEntry<"projects">["data"]` from `z.infer` of whatever this is.
|
|
89
|
+
* Left generic, every field in every template would degrade to `any` — and
|
|
90
|
+
* the point of the shared model is that renaming a field breaks the build.
|
|
91
|
+
*/
|
|
92
|
+
schema: z.ZodType<T>;
|
|
93
|
+
}
|
|
94
|
+
/** The subset of Astro's `Loader` contract this package uses. */
|
|
95
|
+
export interface ContentLoader {
|
|
96
|
+
name: string;
|
|
97
|
+
load(context: LoaderContext): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
export interface LoaderContext {
|
|
100
|
+
store: {
|
|
101
|
+
clear(): void;
|
|
102
|
+
set(entry: {
|
|
103
|
+
id: string;
|
|
104
|
+
data: Record<string, unknown>;
|
|
105
|
+
digest?: string;
|
|
106
|
+
}): boolean;
|
|
107
|
+
keys(): string[];
|
|
108
|
+
};
|
|
109
|
+
logger: {
|
|
110
|
+
info(message: string): void;
|
|
111
|
+
warn(message: string): void;
|
|
112
|
+
error(message: string): void;
|
|
113
|
+
debug(message: string): void;
|
|
114
|
+
};
|
|
115
|
+
meta: {
|
|
116
|
+
get(key: string): string | undefined;
|
|
117
|
+
set(key: string, value: string): void;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Astro's resolved config. Optional because this interface is a hand-written
|
|
121
|
+
* subset and the tests build a context by hand; `config.root` is what turns a
|
|
122
|
+
* relative content root into a path.
|
|
123
|
+
*/
|
|
124
|
+
config?: {
|
|
125
|
+
root: URL;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Present only while `astro dev` runs. Astro's watcher follows the module
|
|
129
|
+
* graph, and content files are not in it — no page imports
|
|
130
|
+
* `projects/haus-a/index.json` — so each loader watches its own directory
|
|
131
|
+
* here, exactly as Astro's own `glob` loader does.
|
|
132
|
+
*/
|
|
133
|
+
watcher?: {
|
|
134
|
+
add(path: string): void;
|
|
135
|
+
on(event: "add" | "change" | "unlink", handler: (path: string) => void): void;
|
|
136
|
+
};
|
|
137
|
+
parseData(input: {
|
|
138
|
+
id: string;
|
|
139
|
+
data: Record<string, unknown>;
|
|
140
|
+
}): Promise<Record<string, unknown>>;
|
|
141
|
+
generateDigest(data: Record<string, unknown> | string): string;
|
|
142
|
+
}
|
|
143
|
+
export type MicaCollections<S extends Site> = {
|
|
144
|
+
[K in keyof S["collections"]]: AstroCollectionConfig<AstroDocument<InferCollection<S["collections"][K]>>>;
|
|
145
|
+
} & {
|
|
146
|
+
[K in keyof S["singletons"]]: AstroCollectionConfig<InferSingleton<S["singletons"][K]>>;
|
|
147
|
+
};
|
|
148
|
+
export declare function micaCollections<S extends Site>(site: S, options?: MicaCollectionsOptions): MicaCollections<S>;
|
|
149
|
+
/** The document type for one collection, as it appears in Astro. */
|
|
150
|
+
export type CollectionDocument<S extends Site, K extends keyof S["collections"]> = AstroDocument<InferCollection<S["collections"][K]>>;
|
|
151
|
+
/** The document type for one singleton. */
|
|
152
|
+
export type SingletonDocument<S extends Site, K extends keyof S["singletons"]> = InferSingleton<S["singletons"][K]>;
|
|
153
|
+
export { manifestFor } from "./manifest.js";
|
|
154
|
+
export { DEFAULT_CONTENT_ROOT, resolveContentRoot };
|
|
155
|
+
export { renderRichText, transformRichText, validateRichText, type RichTextOptions, } from "./richtext.js";
|
|
156
|
+
/**
|
|
157
|
+
* Re-exported for convenience, so a project that only depends on this package
|
|
158
|
+
* can still build a source or read a collection. They are defined in
|
|
159
|
+
* `@micacms/model`, where the Mica server can reach them too.
|
|
160
|
+
*/
|
|
161
|
+
export { ContentReadError, ContentSourceError, ORDER_FILE, memorySource, readCollection, readSingleton, type ContentSource, type DirectoryEntry, } from "@micacms/model";
|
|
162
|
+
export { fileSystemSource } from "@micacms/model/node";
|
|
163
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,cAAc,EAEnB,KAAK,IAAI,EACV,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAErE,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,QAAQ;IACvB,+DAA+D;IAC/D,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;CAClB;AASD,yEAAyE;AACzE,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB,CAAC,CAAC,GAAG,OAAO;IAChD,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB;;;;;OAKG;IACH,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;CACtB;AAED,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE;QACL,KAAK,IAAI,IAAI,CAAC;QACd,GAAG,CAAC,KAAK,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC;QACpF,IAAI,IAAI,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,MAAM,EAAE;QACN,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;KAC9B,CAAC;IACF,IAAI,EAAE;QAAE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;QAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACtF;;;;OAIG;IACH,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,GAAG,CAAA;KAAE,CAAC;IACvB;;;;;OAKG;IACH,OAAO,CAAC,EAAE;QACR,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;KAC/E,CAAC;IACF,SAAS,CAAC,KAAK,EAAE;QACf,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC/B,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrC,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC;CAChE;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,IAAI,IAAI;KAC3C,CAAC,IAAI,MAAM,CAAC,CAAC,aAAa,CAAC,GAAG,qBAAqB,CAClD,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACpD;CACF,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,CAAC,YAAY,CAAC,GAAG,qBAAqB,CAAC,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACxF,CAAC;AAEF,wBAAgB,eAAe,CAAC,CAAC,SAAS,IAAI,EAC5C,IAAI,EAAE,CAAC,EACP,OAAO,GAAE,sBAA2B,GACnC,eAAe,CAAC,CAAC,CAAC,CAqJpB;AAuED,oEAAoE;AACpE,MAAM,MAAM,kBAAkB,CAC5B,CAAC,SAAS,IAAI,EACd,CAAC,SAAS,MAAM,CAAC,CAAC,aAAa,CAAC,IAC9B,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAExD,2CAA2C;AAC3C,MAAM,MAAM,iBAAiB,CAC3B,CAAC,SAAS,IAAI,EACd,CAAC,SAAS,MAAM,CAAC,CAAC,YAAY,CAAC,IAC7B,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEvC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,CAAC;AACpD,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC;AAEvB;;;;GAIG;AACH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @micacms/astro — a Git repository as an Astro content source.
|
|
3
|
+
*
|
|
4
|
+
* // src/content.config.ts
|
|
5
|
+
* import { micaCollections } from "@micacms/astro";
|
|
6
|
+
* import { defineSite } from "@micacms/model";
|
|
7
|
+
*
|
|
8
|
+
* export const site = defineSite({ collections: { … } });
|
|
9
|
+
* export const collections = micaCollections(site, { root: "./content" });
|
|
10
|
+
*
|
|
11
|
+
* From there the website uses ordinary Astro APIs — `getCollection("projects")`
|
|
12
|
+
* — and knows nothing about how the content arrived.
|
|
13
|
+
*
|
|
14
|
+
* There is no CMS in this path. The loaders read files that are already in the
|
|
15
|
+
* working tree, so a build needs a checkout and nothing else: no running
|
|
16
|
+
* server, no token, no network. That is the point of the Git-native design.
|
|
17
|
+
* Publishing is a commit, and CI builds the commit.
|
|
18
|
+
*/
|
|
19
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
20
|
+
import { dirname, join, sep } from "node:path";
|
|
21
|
+
import { fieldsSchema, MANIFEST_PATH, readCollection, readSingleton, } from "@micacms/model";
|
|
22
|
+
import { fileSystemSource } from "@micacms/model/node";
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { manifestFor } from "./manifest.js";
|
|
25
|
+
import { DEFAULT_CONTENT_ROOT, resolveContentRoot } from "./root.js";
|
|
26
|
+
const metaSchema = z.object({
|
|
27
|
+
id: z.string(),
|
|
28
|
+
slug: z.string(),
|
|
29
|
+
collection: z.string(),
|
|
30
|
+
position: z.number(),
|
|
31
|
+
});
|
|
32
|
+
export function micaCollections(site, options = {}) {
|
|
33
|
+
/**
|
|
34
|
+
* Everything below is resolved on first load rather than here, because the
|
|
35
|
+
* Astro project root is something only a loader is told — and it is what
|
|
36
|
+
* turns `./content` into a directory. Memoised: one root and one source,
|
|
37
|
+
* however many collections and however many reloads.
|
|
38
|
+
*/
|
|
39
|
+
let root;
|
|
40
|
+
let source;
|
|
41
|
+
const rootFor = (context) => (root ??= resolveContentRoot(options.root ?? site.root, context.config?.root));
|
|
42
|
+
const sourceFor = (context) => (source ??= options.source ?? fileSystemSource(rootFor(context)));
|
|
43
|
+
/** Published once per process; a model change restarts the dev server. */
|
|
44
|
+
let publishing;
|
|
45
|
+
const publish = (context) => (publishing ??=
|
|
46
|
+
options.publishModel === false || options.source
|
|
47
|
+
? Promise.resolve()
|
|
48
|
+
: publishModel(site, rootFor(context), context.logger));
|
|
49
|
+
/** Collections whose watcher is already registered, so a reload adds none. */
|
|
50
|
+
const watching = new Set();
|
|
51
|
+
const watch = (context, name, reload) => {
|
|
52
|
+
const watcher = context.watcher;
|
|
53
|
+
if (!watcher || options.source || watching.has(name))
|
|
54
|
+
return;
|
|
55
|
+
watching.add(name);
|
|
56
|
+
const base = rootFor(context);
|
|
57
|
+
watcher.add(base);
|
|
58
|
+
// A collection lives in `<root>/<name>/`; a singleton is either
|
|
59
|
+
// `<root>/<name>.json` or `<root>/<name>/…`. Both are covered by this
|
|
60
|
+
// prefix, and nothing outside it can concern this loader — which is what
|
|
61
|
+
// keeps one collection's save from re-reading every other one.
|
|
62
|
+
const prefix = join(base, name);
|
|
63
|
+
const mine = (path) => path === `${prefix}.json` || path.startsWith(prefix + sep);
|
|
64
|
+
let timer;
|
|
65
|
+
const onChange = (path) => {
|
|
66
|
+
// The manifest is this package's own output; reloading content because
|
|
67
|
+
// the model was written is a loop waiting to happen.
|
|
68
|
+
if (path.endsWith(MANIFEST_PATH) || !mine(path))
|
|
69
|
+
return;
|
|
70
|
+
if (timer)
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
timer = setTimeout(() => {
|
|
73
|
+
// Content that fails its own schema lands here. The dev server has to
|
|
74
|
+
// survive it: the next save is very likely the fix.
|
|
75
|
+
void reload().catch((error) => {
|
|
76
|
+
context.logger.error(`Could not reload content: ${describe(error)}`);
|
|
77
|
+
});
|
|
78
|
+
}, options.debounceMs ?? 100);
|
|
79
|
+
// A pending reload is never a reason to keep the process alive.
|
|
80
|
+
timer.unref?.();
|
|
81
|
+
};
|
|
82
|
+
watcher.on("change", onChange);
|
|
83
|
+
watcher.on("add", onChange);
|
|
84
|
+
watcher.on("unlink", onChange);
|
|
85
|
+
};
|
|
86
|
+
const collections = {};
|
|
87
|
+
for (const [name, def] of Object.entries(site.collections)) {
|
|
88
|
+
collections[name] = {
|
|
89
|
+
type: "content_layer",
|
|
90
|
+
// `storage` rather than `output`: a media reference is a path into the
|
|
91
|
+
// repository at rest and a path into the repository once loaded. There
|
|
92
|
+
// is no expansion step left to tell the two apart.
|
|
93
|
+
schema: fieldsSchema(def.fields, "storage").extend({ _mica: metaSchema }),
|
|
94
|
+
loader: {
|
|
95
|
+
name: `mica:${name}`,
|
|
96
|
+
load: async (context) => {
|
|
97
|
+
const read = async () => {
|
|
98
|
+
const source = sourceFor(context);
|
|
99
|
+
const entries = await guard(context, source, () => readCollection(source, def, name));
|
|
100
|
+
// A full replace, not a merge: an entry deleted from the repository
|
|
101
|
+
// has to disappear from the build.
|
|
102
|
+
context.store.clear();
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
const meta = {
|
|
105
|
+
id: entry.slug,
|
|
106
|
+
slug: entry.slug,
|
|
107
|
+
collection: name,
|
|
108
|
+
position: entry.position,
|
|
109
|
+
};
|
|
110
|
+
const data = await context.parseData({
|
|
111
|
+
id: entry.slug,
|
|
112
|
+
data: { ...entry.data, _mica: meta },
|
|
113
|
+
});
|
|
114
|
+
context.store.set({ id: entry.slug, data, digest: context.generateDigest(data) });
|
|
115
|
+
}
|
|
116
|
+
context.logger.info(`Loaded ${entries.length} ${def.label.toLowerCase()}`);
|
|
117
|
+
};
|
|
118
|
+
await publish(context);
|
|
119
|
+
await read();
|
|
120
|
+
watch(context, name, read);
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// A singleton becomes a one-entry collection whose id is its name, so the
|
|
126
|
+
// website reaches it with the same `getEntry` it uses for everything else.
|
|
127
|
+
for (const [name, def] of Object.entries(site.singletons)) {
|
|
128
|
+
collections[name] = {
|
|
129
|
+
type: "content_layer",
|
|
130
|
+
schema: fieldsSchema(def.fields, "storage"),
|
|
131
|
+
loader: {
|
|
132
|
+
name: `mica:${name}`,
|
|
133
|
+
load: async (context) => {
|
|
134
|
+
const read = async () => {
|
|
135
|
+
const source = sourceFor(context);
|
|
136
|
+
const data = await guard(context, source, () => readSingleton(source, def, name));
|
|
137
|
+
context.store.clear();
|
|
138
|
+
if (data === null) {
|
|
139
|
+
// Not fatal. A site is normally deployed before every settings
|
|
140
|
+
// form has been filled in, and the page that reads this singleton
|
|
141
|
+
// reports the absence far better than the loader can.
|
|
142
|
+
context.logger.warn(`No content for the "${def.label}" settings yet.`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const parsed = await context.parseData({ id: name, data });
|
|
146
|
+
context.store.set({ id: name, data: parsed, digest: context.generateDigest(parsed) });
|
|
147
|
+
};
|
|
148
|
+
await publish(context);
|
|
149
|
+
await read();
|
|
150
|
+
watch(context, name, read);
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return collections;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Writes the model into the content root, for Mica to read back.
|
|
159
|
+
*
|
|
160
|
+
* Mica holds no content model: the Astro project is the only place a collection
|
|
161
|
+
* is defined, and this is how it says so. The file goes *inside the content
|
|
162
|
+
* root* so that it travels to Mica the way content does — through Git and the
|
|
163
|
+
* repository connector — with nothing to mount and no second channel to keep in
|
|
164
|
+
* step. Commit it.
|
|
165
|
+
*
|
|
166
|
+
* Done from a loader rather than an integration because this is the moment both
|
|
167
|
+
* facts are in hand: the model, and the project root that turns `./content`
|
|
168
|
+
* into a directory. It also means `astro.config` needs no Mica in it, which is
|
|
169
|
+
* what lets a project define Mica collections and ordinary Astro ones in the
|
|
170
|
+
* same `content.config.ts`.
|
|
171
|
+
*/
|
|
172
|
+
async function publishModel(site, root, logger) {
|
|
173
|
+
const target = join(root, MANIFEST_PATH);
|
|
174
|
+
const contents = `${JSON.stringify(manifestFor(site), null, 2)}\n`;
|
|
175
|
+
try {
|
|
176
|
+
// Only when it differs. The content root is watched in dev, and rewriting
|
|
177
|
+
// an identical file on every start would ask for a reload for no reason.
|
|
178
|
+
if ((await readFile(target, "utf8").catch(() => null)) === contents)
|
|
179
|
+
return;
|
|
180
|
+
await mkdir(dirname(target), { recursive: true });
|
|
181
|
+
await writeFile(target, contents, "utf8");
|
|
182
|
+
logger.info(`Wrote the content model to ${MANIFEST_PATH} — commit it for Mica to read.`);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
// A site that builds correctly should not fail over this, and a read-only
|
|
186
|
+
// checkout is a normal way to build one in CI. Mica reports a missing model
|
|
187
|
+
// itself, and far more usefully.
|
|
188
|
+
logger.warn(`Could not write the content model: ${describe(error)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function describe(error) {
|
|
192
|
+
return error instanceof Error ? error.message : String(error);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Reports a read failure against the content root that produced it.
|
|
196
|
+
*
|
|
197
|
+
* A build failing on unreadable content is correct — publishing a half-empty
|
|
198
|
+
* site would be worse — but the message has to name the directory that was
|
|
199
|
+
* read, because the usual cause is a content root pointing at the wrong place.
|
|
200
|
+
*/
|
|
201
|
+
async function guard(context, source, read) {
|
|
202
|
+
try {
|
|
203
|
+
return await read();
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
207
|
+
context.logger.error(detail);
|
|
208
|
+
throw new Error(`Could not read content from ${source.origin}.\n ${detail}`, { cause: error });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export { manifestFor } from "./manifest.js";
|
|
212
|
+
export { DEFAULT_CONTENT_ROOT, resolveContentRoot };
|
|
213
|
+
export { renderRichText, transformRichText, validateRichText, } from "./richtext.js";
|
|
214
|
+
/**
|
|
215
|
+
* Re-exported for convenience, so a project that only depends on this package
|
|
216
|
+
* can still build a source or read a collection. They are defined in
|
|
217
|
+
* `@micacms/model`, where the Mica server can reach them too.
|
|
218
|
+
*/
|
|
219
|
+
export { ContentReadError, ContentSourceError, ORDER_FILE, memorySource, readCollection, readSingleton, } from "@micacms/model";
|
|
220
|
+
export { fileSystemSource } from "@micacms/model/node";
|
|
221
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The protocol manifest this project publishes.
|
|
3
|
+
*
|
|
4
|
+
* Its own module rather than a function in `index.ts`, so that the integration
|
|
5
|
+
* can emit a manifest without importing the loaders, and the loaders can be
|
|
6
|
+
* imported without pulling in the integration. The two files would otherwise
|
|
7
|
+
* import each other.
|
|
8
|
+
*/
|
|
9
|
+
import { type ProtocolManifest, type Site } from "@micacms/model";
|
|
10
|
+
/**
|
|
11
|
+
* Mica holds no model of its own. The Astro project is the only place a
|
|
12
|
+
* collection is defined, and this is how it says so.
|
|
13
|
+
*/
|
|
14
|
+
export declare function manifestFor(site: Site): ProtocolManifest;
|
|
15
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAkB,KAAK,gBAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAKlF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,gBAAgB,CAExD"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The protocol manifest this project publishes.
|
|
3
|
+
*
|
|
4
|
+
* Its own module rather than a function in `index.ts`, so that the integration
|
|
5
|
+
* can emit a manifest without importing the loaders, and the loaders can be
|
|
6
|
+
* imported without pulling in the integration. The two files would otherwise
|
|
7
|
+
* import each other.
|
|
8
|
+
*/
|
|
9
|
+
import { createManifest } from "@micacms/model";
|
|
10
|
+
/** Replaced at publish time; the fallback keeps a workspace build honest. */
|
|
11
|
+
const PACKAGE_VERSION = "0.0.1";
|
|
12
|
+
/**
|
|
13
|
+
* Mica holds no model of its own. The Astro project is the only place a
|
|
14
|
+
* collection is defined, and this is how it says so.
|
|
15
|
+
*/
|
|
16
|
+
export function manifestFor(site) {
|
|
17
|
+
return createManifest(site, `@micacms/astro@${PACKAGE_VERSION}`);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,cAAc,EAAoC,MAAM,gBAAgB,CAAC;AAElF,6EAA6E;AAC7E,MAAM,eAAe,GAAG,OAAO,CAAC;AAEhC;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAU;IACpC,OAAO,cAAc,CAAC,IAAI,EAAE,kBAAkB,eAAe,EAAE,CAAC,CAAC;AACnE,CAAC"}
|
package/dist/read.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry discovery.
|
|
3
|
+
*
|
|
4
|
+
* Turns a content directory into payloads, using the layout rules the model
|
|
5
|
+
* defines. Nothing here decides where a file belongs — `@micacms/model` does —
|
|
6
|
+
* so a directory written by the editor and a directory written by hand are read
|
|
7
|
+
* the same way.
|
|
8
|
+
*/
|
|
9
|
+
import { type Collection, type Singleton } from "@micacms/model";
|
|
10
|
+
import type { ContentSource } from "./source.js";
|
|
11
|
+
/**
|
|
12
|
+
* Optional manual ordering for a collection, listing slugs in the order an
|
|
13
|
+
* editor arranged them.
|
|
14
|
+
*
|
|
15
|
+
* A separate file rather than a field on each entry, because order is a
|
|
16
|
+
* property of the collection: writing it into the entries would make a single
|
|
17
|
+
* drag rewrite every one of them, and the resulting Git diff would say nothing
|
|
18
|
+
* about what changed.
|
|
19
|
+
*/
|
|
20
|
+
export declare const ORDER_FILE = "_order.json";
|
|
21
|
+
export interface ContentEntry {
|
|
22
|
+
slug: string;
|
|
23
|
+
/** Index in the collection's order — manual when `_order.json` says so. */
|
|
24
|
+
position: number;
|
|
25
|
+
data: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/** Raised with the offending path attached, which a bare parse error lacks. */
|
|
28
|
+
export declare class ContentReadError extends Error {
|
|
29
|
+
readonly path: string;
|
|
30
|
+
name: string;
|
|
31
|
+
constructor(message: string, path: string, options?: {
|
|
32
|
+
cause?: unknown;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export declare function readCollection(source: ContentSource, def: Collection, name: string): Promise<ContentEntry[]>;
|
|
36
|
+
/**
|
|
37
|
+
* A singleton's payload, or null when the repository has no file for it yet.
|
|
38
|
+
*
|
|
39
|
+
* Null rather than an error: a site is normally deployed before every settings
|
|
40
|
+
* form has been filled in, and the collection schema is a better place to
|
|
41
|
+
* report a missing required value than the reader is.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readSingleton(source: ContentSource, def: Singleton, name: string): Promise<Record<string, unknown> | null>;
|
|
44
|
+
//# sourceMappingURL=read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../src/read.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAOL,KAAK,UAAU,EAEf,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,gBAAgB,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,+EAA+E;AAC/E,qBAAa,gBAAiB,SAAQ,KAAK;IAIvC,QAAQ,CAAC,IAAI,EAAE,MAAM;IAHd,IAAI,SAAsB;gBAEjC,OAAO,EAAE,MAAM,EACN,IAAI,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIhC;AAMD,wBAAsB,cAAc,CAClC,MAAM,EAAE,aAAa,EACrB,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,EAAE,CAAC,CAYzB;AA4ED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,aAAa,EACrB,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAEzC"}
|
package/dist/read.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry discovery.
|
|
3
|
+
*
|
|
4
|
+
* Turns a content directory into payloads, using the layout rules the model
|
|
5
|
+
* defines. Nothing here decides where a file belongs — `@micacms/model` does —
|
|
6
|
+
* so a directory written by the editor and a directory written by hand are read
|
|
7
|
+
* the same way.
|
|
8
|
+
*/
|
|
9
|
+
import { DOCUMENT_EXTENSION, FormatError, INDEX_FILE, entryLayout, isValidSlug, parseEntry, } from "@micacms/model";
|
|
10
|
+
/**
|
|
11
|
+
* Optional manual ordering for a collection, listing slugs in the order an
|
|
12
|
+
* editor arranged them.
|
|
13
|
+
*
|
|
14
|
+
* A separate file rather than a field on each entry, because order is a
|
|
15
|
+
* property of the collection: writing it into the entries would make a single
|
|
16
|
+
* drag rewrite every one of them, and the resulting Git diff would say nothing
|
|
17
|
+
* about what changed.
|
|
18
|
+
*/
|
|
19
|
+
export const ORDER_FILE = "_order.json";
|
|
20
|
+
/** Raised with the offending path attached, which a bare parse error lacks. */
|
|
21
|
+
export class ContentReadError extends Error {
|
|
22
|
+
path;
|
|
23
|
+
name = "ContentReadError";
|
|
24
|
+
constructor(message, path, options) {
|
|
25
|
+
super(message, options);
|
|
26
|
+
this.path = path;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/* ------------------------------------------------------------------ *
|
|
30
|
+
* Collections
|
|
31
|
+
* ------------------------------------------------------------------ */
|
|
32
|
+
export async function readCollection(source, def, name) {
|
|
33
|
+
const slugs = await listSlugs(source, def.fields, name);
|
|
34
|
+
const ordered = await applyOrder(source, name, slugs);
|
|
35
|
+
const entries = [];
|
|
36
|
+
for (const [position, slug] of ordered.entries()) {
|
|
37
|
+
const data = await readEntry(source, def.fields, `${name}/${slug}`, entryBase(def.fields, name, slug));
|
|
38
|
+
if (data)
|
|
39
|
+
entries.push({ slug, position, data });
|
|
40
|
+
}
|
|
41
|
+
return entries;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Slugs present in a collection directory.
|
|
45
|
+
*
|
|
46
|
+
* The layout decides what to look at: directory-shaped entries are directories,
|
|
47
|
+
* file-shaped ones are `.json` files. Anything that does not match — a stray
|
|
48
|
+
* README, the order file, a directory without an `index.json` — is skipped
|
|
49
|
+
* rather than rejected, because a content repository belongs to its owner and
|
|
50
|
+
* may hold things Mica did not put there.
|
|
51
|
+
*/
|
|
52
|
+
async function listSlugs(source, fields, name) {
|
|
53
|
+
const directoryLayout = entryLayout(fields) === "directory";
|
|
54
|
+
const items = await source.list(name);
|
|
55
|
+
const slugs = [];
|
|
56
|
+
for (const item of items) {
|
|
57
|
+
if (directoryLayout) {
|
|
58
|
+
if (item.type !== "directory")
|
|
59
|
+
continue;
|
|
60
|
+
if (isValidSlug(item.name))
|
|
61
|
+
slugs.push(item.name);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (item.type !== "file" || !item.name.endsWith(".json"))
|
|
65
|
+
continue;
|
|
66
|
+
const slug = item.name.slice(0, -".json".length);
|
|
67
|
+
if (isValidSlug(slug))
|
|
68
|
+
slugs.push(slug);
|
|
69
|
+
}
|
|
70
|
+
return slugs;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reorders slugs to match `_order.json`.
|
|
74
|
+
*
|
|
75
|
+
* Tolerant in both directions: a slug the file does not mention goes after the
|
|
76
|
+
* ones it does, and a slug the file mentions but which no longer exists is
|
|
77
|
+
* dropped. Either happens routinely when someone adds or deletes an entry in
|
|
78
|
+
* Git without touching the order file.
|
|
79
|
+
*/
|
|
80
|
+
async function applyOrder(source, name, slugs) {
|
|
81
|
+
const path = `${name}/${ORDER_FILE}`;
|
|
82
|
+
const raw = await source.read(path);
|
|
83
|
+
if (raw === null)
|
|
84
|
+
return slugs;
|
|
85
|
+
let listed;
|
|
86
|
+
try {
|
|
87
|
+
listed = JSON.parse(raw);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw new ContentReadError(`${path} is not valid JSON.`, path, { cause: error });
|
|
91
|
+
}
|
|
92
|
+
if (!Array.isArray(listed)) {
|
|
93
|
+
throw new ContentReadError(`${path} must be an array of slugs.`, path);
|
|
94
|
+
}
|
|
95
|
+
const available = new Set(slugs);
|
|
96
|
+
const ordered = listed.filter((slug) => typeof slug === "string" && available.delete(slug));
|
|
97
|
+
// Whatever the order file did not mention keeps its alphabetical position.
|
|
98
|
+
return [...ordered, ...slugs.filter((slug) => available.has(slug))];
|
|
99
|
+
}
|
|
100
|
+
/* ------------------------------------------------------------------ *
|
|
101
|
+
* Singletons
|
|
102
|
+
* ------------------------------------------------------------------ */
|
|
103
|
+
/**
|
|
104
|
+
* A singleton's payload, or null when the repository has no file for it yet.
|
|
105
|
+
*
|
|
106
|
+
* Null rather than an error: a site is normally deployed before every settings
|
|
107
|
+
* form has been filled in, and the collection schema is a better place to
|
|
108
|
+
* report a missing required value than the reader is.
|
|
109
|
+
*/
|
|
110
|
+
export async function readSingleton(source, def, name) {
|
|
111
|
+
return readEntry(source, def.fields, name, entryBase(def.fields, name));
|
|
112
|
+
}
|
|
113
|
+
/* ------------------------------------------------------------------ *
|
|
114
|
+
* Shared
|
|
115
|
+
* ------------------------------------------------------------------ */
|
|
116
|
+
/** The directory an entry's files live in, or null for file-shaped entries. */
|
|
117
|
+
function entryBase(fields, ...segments) {
|
|
118
|
+
return entryLayout(fields) === "directory" ? segments.join("/") : null;
|
|
119
|
+
}
|
|
120
|
+
async function readEntry(source, fields, label, base) {
|
|
121
|
+
const dataPath = base === null ? `${label}.json` : `${base}/${INDEX_FILE}`;
|
|
122
|
+
const data = await source.read(dataPath);
|
|
123
|
+
if (data === null)
|
|
124
|
+
return null;
|
|
125
|
+
const documents = base === null ? {} : await readDocuments(source, base);
|
|
126
|
+
let parsed;
|
|
127
|
+
try {
|
|
128
|
+
parsed = parseEntry(fields, { data, documents });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
// A `FormatError` names the problem but not the file, and a build that
|
|
132
|
+
// says only "Content is not valid JSON" is close to useless.
|
|
133
|
+
if (error instanceof FormatError) {
|
|
134
|
+
throw new ContentReadError(`${dataPath}: ${error.message}`, dataPath, { cause: error });
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
return base === null ? parsed : resolveMedia(fields, parsed, base);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Rewrites media references to paths relative to the content root.
|
|
142
|
+
*
|
|
143
|
+
* On disk a reference is written the way a person reading the entry would
|
|
144
|
+
* write it: `media/exterior.jpg` for a file colocated with the entry, or
|
|
145
|
+
* `/shared/logo.svg` for one the whole site uses. Both are ambiguous once the
|
|
146
|
+
* entry is out of its directory, so the root-relative form is what leaves here.
|
|
147
|
+
*/
|
|
148
|
+
function resolveMedia(fields, data, base) {
|
|
149
|
+
const resolveOne = (value) => typeof value === "string" && value !== ""
|
|
150
|
+
? value.startsWith("/")
|
|
151
|
+
? value.slice(1)
|
|
152
|
+
: `${base}/${value}`
|
|
153
|
+
: value;
|
|
154
|
+
const visit = (map, record) => {
|
|
155
|
+
const result = { ...record };
|
|
156
|
+
for (const [key, field] of Object.entries(map)) {
|
|
157
|
+
const value = record[key];
|
|
158
|
+
if (field.kind === "media") {
|
|
159
|
+
result[key] = Array.isArray(value) ? value.map(resolveOne) : resolveOne(value);
|
|
160
|
+
}
|
|
161
|
+
else if (field.kind === "object" && isRecord(value)) {
|
|
162
|
+
result[key] = visit(field.fields, value);
|
|
163
|
+
}
|
|
164
|
+
else if (field.kind === "repeater" && Array.isArray(value)) {
|
|
165
|
+
result[key] = value.map((row) => (isRecord(row) ? visit(field.fields, row) : row));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
};
|
|
170
|
+
return visit(fields, data);
|
|
171
|
+
}
|
|
172
|
+
function isRecord(value) {
|
|
173
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
174
|
+
}
|
|
175
|
+
async function readDocuments(source, base) {
|
|
176
|
+
const documents = {};
|
|
177
|
+
for (const item of await source.list(base)) {
|
|
178
|
+
if (item.type !== "file" || !item.name.endsWith(DOCUMENT_EXTENSION))
|
|
179
|
+
continue;
|
|
180
|
+
const contents = await source.read(`${base}/${item.name}`);
|
|
181
|
+
if (contents !== null)
|
|
182
|
+
documents[item.name] = contents;
|
|
183
|
+
}
|
|
184
|
+
return documents;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=read.js.map
|
package/dist/read.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read.js","sourceRoot":"","sources":["../src/read.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering a rich-text field.
|
|
3
|
+
*
|
|
4
|
+
* A `richText()` field is persisted as Markdoc, and this is what turns it into
|
|
5
|
+
* HTML at build time. It runs during `astro build` only; the deployed page is
|
|
6
|
+
* static, and no visitor's browser ever sees Markdoc.
|
|
7
|
+
*
|
|
8
|
+
* Markdoc rather than Markdown-with-HTML for one reason that matters: a Markdoc
|
|
9
|
+
* document can only produce what the configuration allows. An undeclared
|
|
10
|
+
* `{% tag %}` renders as nothing rather than as itself, and raw HTML is inert
|
|
11
|
+
* unless it is explicitly permitted — which it is not here. So the canonical
|
|
12
|
+
* content format cannot carry markup that escapes the site's own styling, and a
|
|
13
|
+
* compromised or careless editor session cannot inject script into a build.
|
|
14
|
+
*/
|
|
15
|
+
import Markdoc, { type Config, type RenderableTreeNode } from "@markdoc/markdoc";
|
|
16
|
+
export interface RichTextOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Extra Markdoc configuration: custom tags, nodes or variables.
|
|
19
|
+
*
|
|
20
|
+
* The place a project adds a callout or a gallery block without giving up the
|
|
21
|
+
* closed set that makes the format safe. A tag must be declared here to
|
|
22
|
+
* render at all.
|
|
23
|
+
*/
|
|
24
|
+
config?: Config;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Markdoc source to an HTML string.
|
|
28
|
+
*
|
|
29
|
+
* Returns an empty string for empty input, so a caller can render the result
|
|
30
|
+
* directly without a null check ahead of it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function renderRichText(source: string | null | undefined, options?: RichTextOptions): string;
|
|
33
|
+
/**
|
|
34
|
+
* The intermediate renderable tree, for a project that wants to render with
|
|
35
|
+
* its own components rather than to an HTML string.
|
|
36
|
+
*/
|
|
37
|
+
export declare function transformRichText(source: string, options?: RichTextOptions): RenderableTreeNode;
|
|
38
|
+
/**
|
|
39
|
+
* Problems Markdoc found in a document, for a build that wants to be strict.
|
|
40
|
+
*
|
|
41
|
+
* Not called by `renderRichText`: a malformed heading should not stop a site
|
|
42
|
+
* from deploying, and Markdoc degrades to plain text rather than to nothing.
|
|
43
|
+
* Exposed so a project can decide otherwise.
|
|
44
|
+
*/
|
|
45
|
+
export declare function validateRichText(source: string, options?: RichTextOptions): Markdoc.ValidateError[];
|
|
46
|
+
//# sourceMappingURL=richtext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"richtext.d.ts","sourceRoot":"","sources":["../src/richtext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,OAAO,EAAE,EAAE,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEjF,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CAKvG;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,eAAoB,GAC5B,kBAAkB,CAEpB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,2BAE7E"}
|
package/dist/richtext.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering a rich-text field.
|
|
3
|
+
*
|
|
4
|
+
* A `richText()` field is persisted as Markdoc, and this is what turns it into
|
|
5
|
+
* HTML at build time. It runs during `astro build` only; the deployed page is
|
|
6
|
+
* static, and no visitor's browser ever sees Markdoc.
|
|
7
|
+
*
|
|
8
|
+
* Markdoc rather than Markdown-with-HTML for one reason that matters: a Markdoc
|
|
9
|
+
* document can only produce what the configuration allows. An undeclared
|
|
10
|
+
* `{% tag %}` renders as nothing rather than as itself, and raw HTML is inert
|
|
11
|
+
* unless it is explicitly permitted — which it is not here. So the canonical
|
|
12
|
+
* content format cannot carry markup that escapes the site's own styling, and a
|
|
13
|
+
* compromised or careless editor session cannot inject script into a build.
|
|
14
|
+
*/
|
|
15
|
+
import Markdoc, {} from "@markdoc/markdoc";
|
|
16
|
+
/**
|
|
17
|
+
* Markdoc source to an HTML string.
|
|
18
|
+
*
|
|
19
|
+
* Returns an empty string for empty input, so a caller can render the result
|
|
20
|
+
* directly without a null check ahead of it.
|
|
21
|
+
*/
|
|
22
|
+
export function renderRichText(source, options = {}) {
|
|
23
|
+
if (!source || source.trim() === "")
|
|
24
|
+
return "";
|
|
25
|
+
const tree = transformRichText(source, options);
|
|
26
|
+
return Markdoc.renderers.html(tree);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The intermediate renderable tree, for a project that wants to render with
|
|
30
|
+
* its own components rather than to an HTML string.
|
|
31
|
+
*/
|
|
32
|
+
export function transformRichText(source, options = {}) {
|
|
33
|
+
return Markdoc.transform(Markdoc.parse(source), options.config ?? {});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Problems Markdoc found in a document, for a build that wants to be strict.
|
|
37
|
+
*
|
|
38
|
+
* Not called by `renderRichText`: a malformed heading should not stop a site
|
|
39
|
+
* from deploying, and Markdoc degrades to plain text rather than to nothing.
|
|
40
|
+
* Exposed so a project can decide otherwise.
|
|
41
|
+
*/
|
|
42
|
+
export function validateRichText(source, options = {}) {
|
|
43
|
+
return Markdoc.validate(Markdoc.parse(source), options.config ?? {});
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=richtext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"richtext.js","sourceRoot":"","sources":["../src/richtext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,OAAO,EAAE,EAAwC,MAAM,kBAAkB,CAAC;AAajF;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,MAAiC,EAAE,UAA2B,EAAE;IAC7F,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAE/C,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,OAAO,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAc,EACd,UAA2B,EAAE;IAE7B,OAAO,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,UAA2B,EAAE;IAC5E,OAAO,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;AACvE,CAAC"}
|
package/dist/root.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the content root is, said once.
|
|
3
|
+
*
|
|
4
|
+
* The collections, the dev integration and the manifest writer all have to
|
|
5
|
+
* agree on this directory, and a disagreement between them produces an empty
|
|
6
|
+
* build rather than an error — the worst kind of failure. So the project states
|
|
7
|
+
* it once, in `defineSite({ root })`, and everything here resolves that one
|
|
8
|
+
* string the same way.
|
|
9
|
+
*
|
|
10
|
+
* Resolved against the **Astro project root**, not the working directory, which
|
|
11
|
+
* is what Astro's own `glob({ base })` does. A relative root therefore means
|
|
12
|
+
* the same thing whether the build was started from the project, from a
|
|
13
|
+
* workspace root, or by an editor extension. Absolute paths are left alone, for
|
|
14
|
+
* content that lives outside the project.
|
|
15
|
+
*/
|
|
16
|
+
export declare const DEFAULT_CONTENT_ROOT = "./content";
|
|
17
|
+
export declare function resolveContentRoot(root: string | undefined, projectRoot?: URL): string;
|
|
18
|
+
//# sourceMappingURL=root.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAEhD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,CAAC,EAAE,GAAG,GAAG,MAAM,CAetF"}
|
package/dist/root.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the content root is, said once.
|
|
3
|
+
*
|
|
4
|
+
* The collections, the dev integration and the manifest writer all have to
|
|
5
|
+
* agree on this directory, and a disagreement between them produces an empty
|
|
6
|
+
* build rather than an error — the worst kind of failure. So the project states
|
|
7
|
+
* it once, in `defineSite({ root })`, and everything here resolves that one
|
|
8
|
+
* string the same way.
|
|
9
|
+
*
|
|
10
|
+
* Resolved against the **Astro project root**, not the working directory, which
|
|
11
|
+
* is what Astro's own `glob({ base })` does. A relative root therefore means
|
|
12
|
+
* the same thing whether the build was started from the project, from a
|
|
13
|
+
* workspace root, or by an editor extension. Absolute paths are left alone, for
|
|
14
|
+
* content that lives outside the project.
|
|
15
|
+
*/
|
|
16
|
+
import { isAbsolute, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
export const DEFAULT_CONTENT_ROOT = "./content";
|
|
19
|
+
export function resolveContentRoot(root, projectRoot) {
|
|
20
|
+
const configured = root ?? DEFAULT_CONTENT_ROOT;
|
|
21
|
+
if (isAbsolute(configured))
|
|
22
|
+
return configured;
|
|
23
|
+
// Only outside a loader or a hook — a plain unit test, or an Astro version
|
|
24
|
+
// that stops passing its config. The working directory is the best guess
|
|
25
|
+
// left, and it is right whenever the build was started from the project.
|
|
26
|
+
if (!projectRoot)
|
|
27
|
+
return resolve(configured);
|
|
28
|
+
// A base without a trailing slash would have its last segment replaced
|
|
29
|
+
// rather than appended to. Astro's `config.root` has one; nothing else is
|
|
30
|
+
// guaranteed to.
|
|
31
|
+
const base = projectRoot.href.endsWith("/") ? projectRoot.href : `${projectRoot.href}/`;
|
|
32
|
+
return fileURLToPath(new URL(configured, base));
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=root.js.map
|
package/dist/root.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root.js","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,CAAC;AAEhD,MAAM,UAAU,kBAAkB,CAAC,IAAwB,EAAE,WAAiB;IAC5E,MAAM,UAAU,GAAG,IAAI,IAAI,oBAAoB,CAAC;IAEhD,IAAI,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,UAAU,CAAC;IAE9C,2EAA2E;IAC3E,yEAAyE;IACzE,yEAAyE;IACzE,IAAI,CAAC,WAAW;QAAE,OAAO,OAAO,CAAC,UAAU,CAAC,CAAC;IAE7C,uEAAuE;IACvE,0EAA0E;IAC1E,iBAAiB;IACjB,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,IAAI,GAAG,CAAC;IACxF,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;AAClD,CAAC"}
|
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where content files come from.
|
|
3
|
+
*
|
|
4
|
+
* Two lines wide on purpose. Everything above it — entry discovery, layout,
|
|
5
|
+
* parsing — is written against `read` and `list` and nothing else, so the same
|
|
6
|
+
* reader serves a checked-out repository during a build, an in-memory fixture
|
|
7
|
+
* in a test, and later a repository connector that answers from a provider API
|
|
8
|
+
* instead of a disk.
|
|
9
|
+
*/
|
|
10
|
+
export interface DirectoryEntry {
|
|
11
|
+
name: string;
|
|
12
|
+
type: "file" | "directory";
|
|
13
|
+
}
|
|
14
|
+
export interface ContentSource {
|
|
15
|
+
/** A description of the origin, for error messages. */
|
|
16
|
+
readonly origin: string;
|
|
17
|
+
/** File contents as UTF-8 text, or null when there is no such file. */
|
|
18
|
+
read(path: string): Promise<string | null>;
|
|
19
|
+
/** Direct children of a directory; empty when the directory does not exist. */
|
|
20
|
+
list(path: string): Promise<DirectoryEntry[]>;
|
|
21
|
+
}
|
|
22
|
+
export declare class ContentSourceError extends Error {
|
|
23
|
+
name: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Reads a content directory in a checked-out repository.
|
|
27
|
+
*
|
|
28
|
+
* A missing file is not an error: an entry with no rich text simply has no
|
|
29
|
+
* `.mdoc` beside it, and a collection an editor has not filled in yet is an
|
|
30
|
+
* empty directory. Refusing to build over either would be wrong. Anything else
|
|
31
|
+
* — a permission problem, a corrupt file, a path that escapes the root — is
|
|
32
|
+
* raised, because it means the content the build was asked for is not there.
|
|
33
|
+
*/
|
|
34
|
+
export declare function fileSystemSource(root: string): ContentSource;
|
|
35
|
+
/**
|
|
36
|
+
* A source built from a flat path-to-contents map, for tests and fixtures.
|
|
37
|
+
*
|
|
38
|
+
* Directories are inferred from the paths rather than declared, which keeps a
|
|
39
|
+
* fixture to the files it actually cares about.
|
|
40
|
+
*/
|
|
41
|
+
export declare function memorySource(files: Record<string, string>, origin?: string): ContentSource;
|
|
42
|
+
//# sourceMappingURL=source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3C,+EAA+E;IAC/E,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;CAC/C;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAClC,IAAI,SAAwB;CACtC;AAMD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CA6C5D;AAYD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,SAAW,GAAG,aAAa,CA8B5F"}
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where content files come from.
|
|
3
|
+
*
|
|
4
|
+
* Two lines wide on purpose. Everything above it — entry discovery, layout,
|
|
5
|
+
* parsing — is written against `read` and `list` and nothing else, so the same
|
|
6
|
+
* reader serves a checked-out repository during a build, an in-memory fixture
|
|
7
|
+
* in a test, and later a repository connector that answers from a provider API
|
|
8
|
+
* instead of a disk.
|
|
9
|
+
*/
|
|
10
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
11
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
12
|
+
export class ContentSourceError extends Error {
|
|
13
|
+
name = "ContentSourceError";
|
|
14
|
+
}
|
|
15
|
+
/* ------------------------------------------------------------------ *
|
|
16
|
+
* Filesystem
|
|
17
|
+
* ------------------------------------------------------------------ */
|
|
18
|
+
/**
|
|
19
|
+
* Reads a content directory in a checked-out repository.
|
|
20
|
+
*
|
|
21
|
+
* A missing file is not an error: an entry with no rich text simply has no
|
|
22
|
+
* `.mdoc` beside it, and a collection an editor has not filled in yet is an
|
|
23
|
+
* empty directory. Refusing to build over either would be wrong. Anything else
|
|
24
|
+
* — a permission problem, a corrupt file, a path that escapes the root — is
|
|
25
|
+
* raised, because it means the content the build was asked for is not there.
|
|
26
|
+
*/
|
|
27
|
+
export function fileSystemSource(root) {
|
|
28
|
+
const base = resolve(root);
|
|
29
|
+
const within = (path) => {
|
|
30
|
+
if (isAbsolute(path)) {
|
|
31
|
+
throw new ContentSourceError(`Content path "${path}" must be relative to the content root.`);
|
|
32
|
+
}
|
|
33
|
+
const full = resolve(base, path);
|
|
34
|
+
// `startsWith(base)` alone would accept a sibling called `content-old`.
|
|
35
|
+
if (full !== base && !full.startsWith(base + sep)) {
|
|
36
|
+
throw new ContentSourceError(`Content path "${path}" escapes the content root.`);
|
|
37
|
+
}
|
|
38
|
+
return full;
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
origin: base,
|
|
42
|
+
async read(path) {
|
|
43
|
+
try {
|
|
44
|
+
return await readFile(within(path), "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (isMissing(error))
|
|
48
|
+
return null;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async list(path) {
|
|
53
|
+
let items;
|
|
54
|
+
try {
|
|
55
|
+
items = await readdir(within(path), { withFileTypes: true });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (isMissing(error))
|
|
59
|
+
return [];
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
return items
|
|
63
|
+
.filter((item) => item.isFile() || item.isDirectory())
|
|
64
|
+
// Sorted here rather than at each call site, so that a build does not
|
|
65
|
+
// depend on the order a filesystem happens to report.
|
|
66
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
67
|
+
.map((item) => ({ name: item.name, type: item.isFile() ? "file" : "directory" }));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** ENOENT, and ENOTDIR for a path whose parent turned out to be a file. */
|
|
72
|
+
function isMissing(error) {
|
|
73
|
+
const code = error?.code;
|
|
74
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
75
|
+
}
|
|
76
|
+
/* ------------------------------------------------------------------ *
|
|
77
|
+
* In memory
|
|
78
|
+
* ------------------------------------------------------------------ */
|
|
79
|
+
/**
|
|
80
|
+
* A source built from a flat path-to-contents map, for tests and fixtures.
|
|
81
|
+
*
|
|
82
|
+
* Directories are inferred from the paths rather than declared, which keeps a
|
|
83
|
+
* fixture to the files it actually cares about.
|
|
84
|
+
*/
|
|
85
|
+
export function memorySource(files, origin = "memory") {
|
|
86
|
+
const normalise = (path) => path.replace(/^\.?\//, "").replace(/\/$/, "");
|
|
87
|
+
return {
|
|
88
|
+
origin,
|
|
89
|
+
async read(path) {
|
|
90
|
+
return files[normalise(path)] ?? null;
|
|
91
|
+
},
|
|
92
|
+
async list(path) {
|
|
93
|
+
const prefix = normalise(path) === "" ? "" : `${normalise(path)}/`;
|
|
94
|
+
const seen = new Map();
|
|
95
|
+
for (const key of Object.keys(files)) {
|
|
96
|
+
if (!key.startsWith(prefix))
|
|
97
|
+
continue;
|
|
98
|
+
const rest = key.slice(prefix.length);
|
|
99
|
+
if (rest === "")
|
|
100
|
+
continue;
|
|
101
|
+
const slash = rest.indexOf("/");
|
|
102
|
+
if (slash === -1)
|
|
103
|
+
seen.set(rest, "file");
|
|
104
|
+
else
|
|
105
|
+
seen.set(rest.slice(0, slash), "directory");
|
|
106
|
+
}
|
|
107
|
+
return [...seen]
|
|
108
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
109
|
+
.map(([name, type]) => ({ name, type }));
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAgB3D,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,IAAI,GAAG,oBAAoB,CAAC;CACtC;AAED;;wEAEwE;AAExE;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B,MAAM,MAAM,GAAG,CAAC,IAAY,EAAU,EAAE;QACtC,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,kBAAkB,CAAC,iBAAiB,IAAI,yCAAyC,CAAC,CAAC;QAC/F,CAAC;QAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACjC,wEAAwE;QACxE,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;YAClD,MAAM,IAAI,kBAAkB,CAAC,iBAAiB,IAAI,6BAA6B,CAAC,CAAC;QACnF,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;IAEF,OAAO;QACL,MAAM,EAAE,IAAI;QAEZ,KAAK,CAAC,IAAI,CAAC,IAAI;YACb,IAAI,CAAC;gBACH,OAAO,MAAM,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;YAC9C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,SAAS,CAAC,KAAK,CAAC;oBAAE,OAAO,IAAI,CAAC;gBAClC,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,IAAI;YACb,IAAI,KAAK,CAAC;YACV,IAAI,CAAC;gBACH,KAAK,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/D,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,SAAS,CAAC,KAAK,CAAC;oBAAE,OAAO,EAAE,CAAC;gBAChC,MAAM,KAAK,CAAC;YACd,CAAC;YAED,OAAO,KAAK;iBACT,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;gBACtD,sEAAsE;gBACtE,sDAAsD;iBACrD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;iBAC5C,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACtF,CAAC;KACF,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,SAAS,SAAS,CAAC,KAAc;IAC/B,MAAM,IAAI,GAAI,KAAsC,EAAE,IAAI,CAAC;IAC3D,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS,CAAC;AACjD,CAAC;AAED;;wEAEwE;AAExE;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,KAA6B,EAAE,MAAM,GAAG,QAAQ;IAC3E,MAAM,SAAS,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAElF,OAAO;QACL,MAAM;QAEN,KAAK,CAAC,IAAI,CAAC,IAAI;YACb,OAAO,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;QACxC,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,IAAI;YACb,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;YACnE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAgC,CAAC;YAErD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACrC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC;oBAAE,SAAS;gBAEtC,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;gBACtC,IAAI,IAAI,KAAK,EAAE;oBAAE,SAAS;gBAE1B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBAChC,IAAI,KAAK,KAAK,CAAC,CAAC;oBAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;;oBACpC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,WAAW,CAAC,CAAC;YACnD,CAAC;YAED,OAAO,CAAC,GAAG,IAAI,CAAC;iBACb,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;iBACtC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC7C,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@micacms/astro",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Reads a Mica content repository as Astro Content Collections via the Content Layer API.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Alexander Müdespacher",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mica",
|
|
10
|
+
"cms",
|
|
11
|
+
"astro",
|
|
12
|
+
"astro-integration",
|
|
13
|
+
"content-layer",
|
|
14
|
+
"markdoc"
|
|
15
|
+
],
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@markdoc/markdoc": "^0.5.9",
|
|
31
|
+
"zod": "^3.25.76",
|
|
32
|
+
"@micacms/model": "0.0.2"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"astro": "^5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.20.1",
|
|
39
|
+
"astro": "^5.13.0",
|
|
40
|
+
"typescript": "^5.7.3"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
45
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
46
|
+
"test": "tsc -p tsconfig.json && node --test test/*.test.ts"
|
|
47
|
+
}
|
|
48
|
+
}
|