forgepress 0.0.0 → 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.
@@ -0,0 +1,105 @@
1
+ import { Entry, EntryMeta, EntryRef, EntryStatus, ForgePressEntry, ForgePressOutput, ForgePressSchema, ForgePressSchemaRegistry, OutputMeta, OutputOf, RegisteredSchema, SchemaLocale } from "./_chunks/entry.mjs";
2
+ import { OutputCollection, OutputEntry, OutputIndex, OutputManifest } from "./_chunks/types.mjs";
3
+ import { ForgePressConfig, OutputConfig } from "./_chunks/config.mjs";
4
+ export declare function defineForgePressConfig(config: ForgePressConfig): ForgePressConfig;
5
+ type Operator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'contains';
6
+ type CollectionName<TSchema extends ForgePressSchema> = keyof TSchema['collections'] & string;
7
+ type Fields<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>> = TSchema['collections'][TName]['fields'];
8
+ type FieldName<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>> = keyof Fields<TSchema, TName> & string;
9
+ type Simplify<TValue> = { [TKey in keyof TValue]: TValue[TKey]; } & {};
10
+ type Kind<TField> = TField extends {
11
+ type: 'number';
12
+ } ? 'number' : TField extends {
13
+ type: 'text' | 'richtext';
14
+ } ? 'text' : TField extends {
15
+ type: 'relation';
16
+ multiple: true;
17
+ } ? 'links' : TField extends {
18
+ type: 'relation';
19
+ } ? 'link' : TField extends {
20
+ type: 'dynamic';
21
+ } ? 'links' : 'none';
22
+ type KindOf<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>, TKey> = TKey extends 'id' ? 'text' : TKey extends 'createdAt' | 'updatedAt' ? 'date' : TKey extends FieldName<TSchema, TName> ? Kind<Fields<TSchema, TName>[TKey]> : 'none';
23
+ interface Operands {
24
+ text: {
25
+ eq: string;
26
+ ne: string;
27
+ in: readonly string[];
28
+ contains: string;
29
+ };
30
+ number: {
31
+ eq: number;
32
+ ne: number;
33
+ gt: number;
34
+ gte: number;
35
+ lt: number;
36
+ lte: number;
37
+ in: readonly number[];
38
+ };
39
+ date: {
40
+ eq: string;
41
+ ne: string;
42
+ gt: string;
43
+ gte: string;
44
+ lt: string;
45
+ lte: string;
46
+ in: readonly string[];
47
+ };
48
+ link: {
49
+ eq: string;
50
+ ne: string;
51
+ in: readonly string[];
52
+ };
53
+ links: {
54
+ contains: string;
55
+ };
56
+ none: Record<never, never>;
57
+ }
58
+ type Keys<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>> = 'id' | 'createdAt' | 'updatedAt' | FieldName<TSchema, TName>;
59
+ type OperatorOf<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>, TKey> = keyof Operands[KindOf<TSchema, TName, TKey>] & Operator;
60
+ type Operand<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>, TKey, TOperator> = TOperator extends keyof Operands[KindOf<TSchema, TName, TKey>] ? Operands[KindOf<TSchema, TName, TKey>][TOperator] : never;
61
+ type Filterable<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>> = { [TKey in Keys<TSchema, TName>]: [OperatorOf<TSchema, TName, TKey>] extends [never] ? never : TKey; }[Keys<TSchema, TName>];
62
+ type Comparable<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>> = { [TKey in Keys<TSchema, TName>]: 'eq' extends OperatorOf<TSchema, TName, TKey> ? TKey : never; }[Keys<TSchema, TName>];
63
+ type Sortable<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>> = { [TKey in Keys<TSchema, TName>]: KindOf<TSchema, TName, TKey> extends 'text' | 'number' | 'date' | 'link' ? TKey : never; }[Keys<TSchema, TName>];
64
+ type Linkable<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>> = { [TKey in FieldName<TSchema, TName>]: Fields<TSchema, TName>[TKey] extends {
65
+ type: 'relation' | 'dynamic';
66
+ } ? TKey : never; }[FieldName<TSchema, TName>];
67
+ type LinkedBlock<TSchema extends ForgePressSchema, TTarget> = TTarget extends CollectionName<TSchema> ? {
68
+ collection: TTarget;
69
+ id: string;
70
+ entry: OutputOf<TSchema, TTarget>;
71
+ } : never;
72
+ type Linked<TSchema extends ForgePressSchema, TField> = TField extends {
73
+ type: 'relation';
74
+ collection: infer TTarget extends CollectionName<TSchema>;
75
+ } ? TField extends {
76
+ multiple: true;
77
+ } ? OutputOf<TSchema, TTarget>[] : OutputOf<TSchema, TTarget> : TField extends {
78
+ type: 'dynamic';
79
+ collections: readonly (infer TTarget)[];
80
+ } ? LinkedBlock<TSchema, TTarget>[] : never;
81
+ type WithLinked<TResult, TKey extends keyof TResult, TValue> = Simplify<Omit<TResult, TKey> & (Record<never, never> extends Pick<TResult, TKey> ? { [TField in TKey]?: TValue; } : { [TField in TKey]: TValue; })>;
82
+ type SiteLocale<TSchema extends ForgePressSchema> = [SchemaLocale<TSchema>] extends [never] ? string : SchemaLocale<TSchema>;
83
+ interface QueryBuilder<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>, TResult> extends Promise<TResult[]> {
84
+ where: {
85
+ <TKey extends Comparable<TSchema, TName>>(field: TKey, value: Operand<TSchema, TName, TKey, 'eq'>): QueryBuilder<TSchema, TName, TResult>;
86
+ <TKey extends Filterable<TSchema, TName>, TOperator extends OperatorOf<TSchema, TName, TKey>>(field: TKey, operator: TOperator, value: Operand<TSchema, TName, TKey, TOperator>): QueryBuilder<TSchema, TName, TResult>;
87
+ };
88
+ sort: (field: Sortable<TSchema, TName>, direction?: 'asc' | 'desc') => QueryBuilder<TSchema, TName, TResult>;
89
+ limit: (count: number) => QueryBuilder<TSchema, TName, TResult>;
90
+ offset: (count: number) => QueryBuilder<TSchema, TName, TResult>;
91
+ locale: (locale: SiteLocale<TSchema>) => QueryBuilder<TSchema, TName, TResult>;
92
+ with: <TKey extends Linkable<TSchema, TName> & keyof TResult>(field: TKey) => QueryBuilder<TSchema, TName, WithLinked<TResult, TKey, Linked<TSchema, Fields<TSchema, TName>[TKey]>>>;
93
+ pick: <TKey extends keyof TResult & string>(...fields: TKey[]) => QueryBuilder<TSchema, TName, Simplify<Pick<TResult, TKey>>>;
94
+ first: () => Promise<TResult | undefined>;
95
+ }
96
+ type Query<TSchema extends ForgePressSchema> = <TName extends CollectionName<TSchema>>(collection: TName) => QueryBuilder<TSchema, TName, OutputOf<TSchema, TName>>;
97
+ interface ClientOptions {
98
+ url?: string;
99
+ }
100
+ interface Client<TSchema extends ForgePressSchema = RegisteredSchema> {
101
+ query: Query<TSchema>;
102
+ }
103
+ export declare function createClient<TSchema extends ForgePressSchema = RegisteredSchema>(options?: ClientOptions): Client<TSchema>;
104
+ export declare const query: Query<RegisteredSchema>;
105
+ export type { Client, ClientOptions, Entry, EntryMeta, EntryRef, EntryStatus, ForgePressConfig, ForgePressEntry, ForgePressOutput, ForgePressSchema, ForgePressSchemaRegistry, LinkedBlock, Operator, OutputCollection, OutputConfig, OutputEntry, OutputIndex, OutputManifest, OutputMeta, QueryBuilder };
package/dist/index.mjs ADDED
@@ -0,0 +1,247 @@
1
+ import { isRecord, quote } from "./_chunks/value.mjs";
2
+ import { entryKey } from "./_chunks/references.mjs";
3
+ import { OUTPUT_INDEX } from "./_chunks/types.mjs";
4
+ import { fetchReader } from "./_chunks/fetch.mjs";
5
+ import { overlaid } from "./_chunks/overlay.mjs";
6
+ import { reader } from "#content-reader";
7
+ function defineForgePressConfig(config) {
8
+ return config;
9
+ }
10
+ function comparable(value) {
11
+ return isRecord(value) && typeof value.collection === "string" && typeof value.id === "string" ? value.id : value;
12
+ }
13
+ function compare(left, right) {
14
+ const first = comparable(left);
15
+ const second = comparable(right);
16
+ if (first === second) return 0;
17
+ if (first === void 0 || first === null) return -1;
18
+ if (second === void 0 || second === null) return 1;
19
+ return first < second ? -1 : 1;
20
+ }
21
+ function ranged(actual, value, accept) {
22
+ return actual !== void 0 && actual !== null && accept(compare(actual, value));
23
+ }
24
+ function matches(entry, { field, op, value }) {
25
+ const actual = entry[field];
26
+ switch (op) {
27
+ case "eq": return comparable(actual) === value;
28
+ case "ne": return comparable(actual) !== value;
29
+ case "gt": return ranged(actual, value, (order) => order > 0);
30
+ case "gte": return ranged(actual, value, (order) => order >= 0);
31
+ case "lt": return ranged(actual, value, (order) => order < 0);
32
+ case "lte": return ranged(actual, value, (order) => order <= 0);
33
+ case "in": return Array.isArray(value) && value.includes(comparable(actual));
34
+ case "contains": return Array.isArray(actual) ? actual.some((item) => comparable(item) === value) : typeof actual === "string" && typeof value === "string" && actual.includes(value);
35
+ }
36
+ }
37
+ function evaluate(entries, plan) {
38
+ let result = plan.where.length > 0 ? entries.filter((entry) => plan.where.every((clause) => matches(entry, clause))) : [...entries];
39
+ if (plan.sort.length > 0) result.sort((left, right) => {
40
+ for (const { field, dir } of plan.sort) {
41
+ const order = compare(left[field], right[field]);
42
+ if (order !== 0) return dir === "asc" ? order : -order;
43
+ }
44
+ return 0;
45
+ });
46
+ if (plan.offset > 0) result = result.slice(plan.offset);
47
+ if (plan.limit !== void 0) result = result.slice(0, plan.limit);
48
+ return result;
49
+ }
50
+ const META = [
51
+ "id",
52
+ "createdAt",
53
+ "updatedAt"
54
+ ];
55
+ function refsOf(value) {
56
+ return (Array.isArray(value) ? value : [value]).filter((item) => isRecord(item) && typeof item.collection === "string" && typeof item.id === "string");
57
+ }
58
+ function listing(names) {
59
+ return names.map(quote).join(", ");
60
+ }
61
+ var Builder = class {
62
+ loader;
63
+ collection;
64
+ plan = {
65
+ where: [],
66
+ sort: [],
67
+ offset: 0,
68
+ with: []
69
+ };
70
+ chosen;
71
+ constructor(loader, collection) {
72
+ this.loader = loader;
73
+ this.collection = collection;
74
+ }
75
+ where(field, operatorOrValue, value) {
76
+ this.plan.where.push(arguments.length >= 3 ? {
77
+ field,
78
+ op: operatorOrValue,
79
+ value
80
+ } : {
81
+ field,
82
+ op: "eq",
83
+ value: operatorOrValue
84
+ });
85
+ return this;
86
+ }
87
+ sort(field, direction = "asc") {
88
+ this.plan.sort.push({
89
+ field,
90
+ dir: direction
91
+ });
92
+ return this;
93
+ }
94
+ limit(count) {
95
+ this.plan.limit = count;
96
+ return this;
97
+ }
98
+ offset(count) {
99
+ this.plan.offset = count;
100
+ return this;
101
+ }
102
+ locale(locale) {
103
+ this.chosen = locale;
104
+ return this;
105
+ }
106
+ with(field) {
107
+ if (!this.plan.with.includes(field)) this.plan.with.push(field);
108
+ return this;
109
+ }
110
+ pick(...fields) {
111
+ this.plan.pick = fields;
112
+ return this;
113
+ }
114
+ needsLocale(snapshot, reason) {
115
+ const example = snapshot.index.locales[0] ?? "en";
116
+ return /* @__PURE__ */ new Error(`[forgepress] ${reason}, so query(${quote(this.collection)}) needs .locale(), such as .locale(${quote(example)})`);
117
+ }
118
+ async select(snapshot, limit) {
119
+ const { index } = snapshot;
120
+ const { collection, chosen: locale, plan } = this;
121
+ const found = index.collections[collection];
122
+ if (!found) throw new Error(`[forgepress] the content output has no collection ${quote(collection)}`);
123
+ if (locale !== void 0 && !index.locales.includes(locale)) throw new Error(`[forgepress] ${quote(locale)} is not a locale of this site; ${index.locales.length > 0 ? `use ${listing(index.locales)}` : "it has none"}`);
124
+ if (found.localized && locale === void 0) throw this.needsLocale(snapshot, `${quote(collection)} is translated`);
125
+ const manifest = await snapshot.manifest(collection, locale);
126
+ const unlinked = plan.with.filter((field) => manifest.links[field] === void 0);
127
+ if (unlinked.length > 0) throw new Error(`[forgepress] .with() loads relation and dynamic fields, and ${listing(unlinked)} ${unlinked.length === 1 ? "is not one" : "are not"} in ${quote(collection)}`);
128
+ const listed = /* @__PURE__ */ new Set([...META, ...manifest.indexed]);
129
+ const unindexed = [...new Set([...plan.where, ...plan.sort].map((clause) => clause.field))].filter((field) => !listed.has(field));
130
+ if (unindexed.length > 0 && index.dev) console.warn(`[forgepress] query(${quote(collection)}) filters or sorts by ${listing(unindexed)}, which ${unindexed.length === 1 ? "is" : "are"} not indexed, so every entry is loaded. Add index: true to ${unindexed.length === 1 ? "the field" : "the fields"} in the schema`);
131
+ const selected = evaluate(unindexed.length > 0 ? await snapshot.entries(collection, locale, manifest.entries.map((entry) => entry.id)) : manifest.entries, limit === void 0 ? plan : {
132
+ ...plan,
133
+ limit: Math.min(plan.limit ?? limit, limit)
134
+ });
135
+ const listedOnly = plan.pick !== void 0 && [...plan.pick, ...plan.with].every((field) => listed.has(field));
136
+ const rows = unindexed.length > 0 || listedOnly ? selected : await snapshot.entries(collection, locale, selected.map((entry) => entry.id));
137
+ const linked = plan.with.length > 0 ? await this.link(snapshot, manifest, rows) : rows;
138
+ const { pick } = plan;
139
+ const picked = pick === void 0 ? linked : linked.map((row) => Object.fromEntries(pick.filter((field) => row[field] !== void 0).map((field) => [field, row[field]])));
140
+ return structuredClone(picked);
141
+ }
142
+ async link(snapshot, manifest, rows) {
143
+ const { index } = snapshot;
144
+ const locale = this.chosen;
145
+ const wanted = /* @__PURE__ */ new Map();
146
+ for (const field of this.plan.with) for (const ref of rows.flatMap((row) => refsOf(row[field]))) {
147
+ if (index.collections[ref.collection]?.localized && locale === void 0) throw this.needsLocale(snapshot, `.with(${quote(field)}) loads ${quote(ref.collection)} entries, which are translated`);
148
+ wanted.set(ref.collection, (wanted.get(ref.collection) ?? /* @__PURE__ */ new Set()).add(ref.id));
149
+ }
150
+ const loaded = /* @__PURE__ */ new Map();
151
+ await Promise.all([...wanted].map(async ([collection, ids]) => {
152
+ for (const entry of await snapshot.entries(collection, locale, [...ids])) loaded.set(entryKey(collection, entry.id), entry);
153
+ }));
154
+ const find = (ref) => loaded.get(entryKey(ref.collection, ref.id));
155
+ return rows.map((row) => {
156
+ const next = { ...row };
157
+ for (const field of this.plan.with) {
158
+ const value = row[field];
159
+ if (value === void 0) continue;
160
+ if (manifest.links[field] === "dynamic") next[field] = refsOf(value).flatMap((ref) => [find(ref)].flatMap((entry) => entry ? [{
161
+ collection: ref.collection,
162
+ id: ref.id,
163
+ entry
164
+ }] : []));
165
+ else if (Array.isArray(value)) next[field] = refsOf(value).flatMap((ref) => find(ref) ?? []);
166
+ else next[field] = refsOf(value).map(find)[0];
167
+ }
168
+ return next;
169
+ });
170
+ }
171
+ execute(limit) {
172
+ return this.loader.run((snapshot) => this.select(snapshot, limit));
173
+ }
174
+ async first() {
175
+ return (await this.execute(1))[0];
176
+ }
177
+ then(onFulfilled, onRejected) {
178
+ return this.execute().then(onFulfilled, onRejected);
179
+ }
180
+ catch(onRejected) {
181
+ return this.execute().catch(onRejected);
182
+ }
183
+ finally(onFinally) {
184
+ return this.execute().finally(onFinally);
185
+ }
186
+ get [Symbol.toStringTag]() {
187
+ return "ForgePressQuery";
188
+ }
189
+ };
190
+ var MissingFile = class extends Error {};
191
+ function manifestPath(index, collection, locale) {
192
+ const found = index.collections[collection];
193
+ if (!found) throw new Error(`[forgepress] the content output has no collection ${quote(collection)}`);
194
+ if (!found.localized) return found.manifest;
195
+ const path = locale === void 0 ? void 0 : found.manifests[locale];
196
+ if (path === void 0) throw new Error(`[forgepress] the content output has no ${quote(collection)} entries in locale ${quote(locale)}`);
197
+ return path;
198
+ }
199
+ function createLoader(read) {
200
+ const files = /* @__PURE__ */ new Map();
201
+ function file(path) {
202
+ let pending = files.get(path);
203
+ if (!pending) {
204
+ pending = read(path).then((value) => {
205
+ if (value === void 0) throw new MissingFile(path);
206
+ return value;
207
+ });
208
+ pending.catch(() => files.delete(path));
209
+ files.set(path, pending);
210
+ }
211
+ return pending;
212
+ }
213
+ async function snapshot() {
214
+ const index = await read(OUTPUT_INDEX);
215
+ if (index === void 0) throw new Error(`[forgepress] there is no content output yet: ${OUTPUT_INDEX} is missing. Run forgepress build, or start the dev server with the forgepress plugin`);
216
+ if (index.version !== 1) throw new Error(`[forgepress] the content output has format version ${quote(index.version)}, but this version of forgepress reads version 1`);
217
+ const manifest = (collection, locale) => file(manifestPath(index, collection, locale));
218
+ return {
219
+ index,
220
+ manifest,
221
+ entries: async (collection, locale, ids) => {
222
+ const { files: paths } = await manifest(collection, locale);
223
+ return Promise.all(ids.flatMap((id) => paths[id] === void 0 ? [] : [file(paths[id])]));
224
+ }
225
+ };
226
+ }
227
+ return { async run(task) {
228
+ try {
229
+ return await task(await snapshot());
230
+ } catch (error) {
231
+ if (!(error instanceof MissingFile)) throw error;
232
+ }
233
+ try {
234
+ return await task(await snapshot());
235
+ } catch (error) {
236
+ if (error instanceof MissingFile) throw new Error(`[forgepress] the content output links ${error.message}, but the file doesn't exist`);
237
+ throw error;
238
+ }
239
+ } };
240
+ }
241
+ function createClient(options = {}) {
242
+ const loader = createLoader(overlaid(options.url === void 0 ? reader : fetchReader(options.url)));
243
+ const query = (collection) => new Builder(loader, collection);
244
+ return { query };
245
+ }
246
+ const query = createClient().query;
247
+ export { createClient, defineForgePressConfig, query };
@@ -0,0 +1,5 @@
1
+ import "../_chunks/entry.mjs";
2
+ import "../_chunks/config.mjs";
3
+ export declare function previewing(): boolean;
4
+ export declare function onPreviewChange(listener: () => void): () => void;
5
+ export declare function enablePreview(): boolean;
@@ -0,0 +1,244 @@
1
+ import { overlayContent } from "../_chunks/overlay.mjs";
2
+ import { errorMessage } from "../_chunks/error.mjs";
3
+ const TAG = "forgepress-preview";
4
+ const HIDING = "forgepress-preview-hiding";
5
+ const LABELS = {
6
+ loading: "Loading preview…",
7
+ ready: "Preview",
8
+ failed: "Preview unavailable"
9
+ };
10
+ const STYLE = `
11
+ :host {
12
+ all: initial;
13
+ position: fixed;
14
+ left: 16px;
15
+ bottom: 16px;
16
+ z-index: 2147483647;
17
+ }
18
+
19
+ .badge {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: 8px;
23
+ padding: 4px 4px 4px 12px;
24
+ border-radius: 999px;
25
+ background: #13110e;
26
+ color: #fffcf8;
27
+ font: 500 13px/20px ui-sans-serif, system-ui, sans-serif;
28
+ box-shadow: 0 4px 16px rgb(19 17 14 / 0.3);
29
+ }
30
+
31
+ .dot {
32
+ width: 8px;
33
+ height: 8px;
34
+ border-radius: 50%;
35
+ background: #ffd27a;
36
+ }
37
+
38
+ [data-status="loading"] .dot {
39
+ animation: pulse 1s ease-in-out infinite;
40
+ }
41
+
42
+ [data-status="failed"] .dot {
43
+ background: #f87171;
44
+ }
45
+
46
+ button {
47
+ all: unset;
48
+ padding: 2px 10px;
49
+ border-radius: 999px;
50
+ color: #ffd27a;
51
+ cursor: pointer;
52
+ }
53
+
54
+ button:hover {
55
+ background: rgb(255 252 248 / 0.12);
56
+ }
57
+
58
+ button:focus-visible {
59
+ outline: 2px solid #ffd27a;
60
+ outline-offset: 2px;
61
+ }
62
+
63
+ @keyframes pulse {
64
+ 50% {
65
+ opacity: 0.4;
66
+ }
67
+ }
68
+
69
+ @media (prefers-reduced-motion: reduce) {
70
+ [data-status="loading"] .dot {
71
+ animation: none;
72
+ }
73
+ }
74
+ `;
75
+ function createBadge(turnOff) {
76
+ let host;
77
+ let badge;
78
+ let label;
79
+ let latest;
80
+ function hideInEditor() {
81
+ if (document.getElementById(HIDING)) return;
82
+ const style = document.createElement("style");
83
+ style.id = HIDING;
84
+ style.textContent = `body:has([data-forgepress-editor]) > ${TAG} { display: none !important; }`;
85
+ document.head.append(style);
86
+ }
87
+ function mount() {
88
+ hideInEditor();
89
+ host = document.createElement(TAG);
90
+ badge = document.createElement("div");
91
+ label = document.createElement("span");
92
+ const root = host.attachShadow({ mode: "open" });
93
+ const style = document.createElement("style");
94
+ const dot = document.createElement("span");
95
+ const button = document.createElement("button");
96
+ style.textContent = STYLE;
97
+ badge.className = "badge";
98
+ badge.setAttribute("role", "status");
99
+ dot.className = "dot";
100
+ button.type = "button";
101
+ button.textContent = "Turn off";
102
+ button.setAttribute("aria-label", "Turn off preview");
103
+ button.addEventListener("click", turnOff);
104
+ badge.append(dot, label, button);
105
+ root.append(style, badge);
106
+ document.body.append(host);
107
+ }
108
+ function show(state) {
109
+ latest = state;
110
+ if (!document.body) {
111
+ document.addEventListener("DOMContentLoaded", () => latest && show(latest), { once: true });
112
+ return;
113
+ }
114
+ if (!host?.isConnected) mount();
115
+ badge.dataset.status = state.status;
116
+ label.textContent = LABELS[state.status];
117
+ badge.title = state.status === "failed" ? `${state.error}. The site shows published content.` : "";
118
+ }
119
+ return {
120
+ show,
121
+ hide: () => {
122
+ latest = void 0;
123
+ host?.remove();
124
+ }
125
+ };
126
+ }
127
+ const SETTINGS_KEY = "forgepress:preview";
128
+ const OFF_KEY = "forgepress:preview:off";
129
+ const CHANNEL = "forgepress:preview";
130
+ const listeners = /* @__PURE__ */ new Set();
131
+ let version = 0;
132
+ let channel;
133
+ let listening = false;
134
+ function read$1(key) {
135
+ try {
136
+ return localStorage.getItem(key);
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+ function write(key, value) {
142
+ try {
143
+ if (value === void 0) localStorage.removeItem(key);
144
+ else localStorage.setItem(key, value);
145
+ } catch {}
146
+ }
147
+ function emit() {
148
+ version += 1;
149
+ for (const listener of [...listeners]) listener();
150
+ }
151
+ function listen() {
152
+ if (listening || typeof window === "undefined") return;
153
+ listening = true;
154
+ window.addEventListener("storage", (event) => {
155
+ if (event.key === null || event.key === SETTINGS_KEY || event.key === OFF_KEY) emit();
156
+ });
157
+ if (typeof BroadcastChannel !== "undefined") {
158
+ channel = new BroadcastChannel(CHANNEL);
159
+ channel.addEventListener("message", emit);
160
+ }
161
+ }
162
+ function previewSettings() {
163
+ const text = read$1(SETTINGS_KEY);
164
+ try {
165
+ return text ? JSON.parse(text) : void 0;
166
+ } catch {
167
+ return;
168
+ }
169
+ }
170
+ function previewEnabled() {
171
+ return read$1(OFF_KEY) === null;
172
+ }
173
+ function previewing() {
174
+ return typeof window !== "undefined" && previewEnabled() && read$1(SETTINGS_KEY) !== null;
175
+ }
176
+ function previewVersion() {
177
+ return version;
178
+ }
179
+ function onPreviewChange(listener) {
180
+ listen();
181
+ listeners.add(listener);
182
+ return () => {
183
+ listeners.delete(listener);
184
+ };
185
+ }
186
+ function setPreviewEnabled(enabled) {
187
+ if (previewEnabled() === enabled) return;
188
+ write(OFF_KEY, enabled ? void 0 : "1");
189
+ emit();
190
+ }
191
+ let enabled = false;
192
+ let snapshot;
193
+ let selected;
194
+ const badge = createBadge(() => setPreviewEnabled(false));
195
+ function readerFor(settings) {
196
+ const key = JSON.stringify(settings);
197
+ if (selected?.key !== key) selected = {
198
+ key,
199
+ reader: import("../_chunks/reader2.mjs").then((module) => module.createPreviewReader(settings))
200
+ };
201
+ return selected.reader;
202
+ }
203
+ function load() {
204
+ const version = previewVersion();
205
+ if (snapshot?.version === version) return snapshot.files;
206
+ const settings = previewSettings();
207
+ const current = {
208
+ version,
209
+ files: (settings ? readerFor(settings).then((reader) => reader.build()) : Promise.reject(/* @__PURE__ */ new Error("[forgepress] sign in to the editor to preview unpublished content"))).then((files) => {
210
+ if (snapshot === current) badge.show({ status: "ready" });
211
+ return files;
212
+ }, (cause) => {
213
+ if (snapshot === current) badge.show({
214
+ status: "failed",
215
+ error: errorMessage(cause)
216
+ });
217
+ })
218
+ };
219
+ snapshot = current;
220
+ badge.show({ status: "loading" });
221
+ return current.files;
222
+ }
223
+ async function read(path, base) {
224
+ if (!previewing()) return base(path);
225
+ const files = await load();
226
+ if (!files) return base(path);
227
+ const text = files.get(path);
228
+ return text === void 0 ? void 0 : JSON.parse(text);
229
+ }
230
+ function refresh() {
231
+ if (previewing()) load();
232
+ else badge.hide();
233
+ }
234
+ function enablePreview() {
235
+ if (typeof window === "undefined") return false;
236
+ if (!enabled) {
237
+ enabled = true;
238
+ overlayContent(read);
239
+ onPreviewChange(refresh);
240
+ refresh();
241
+ }
242
+ return previewing();
243
+ }
244
+ export { enablePreview, onPreviewChange, previewing };
@@ -0,0 +1,3 @@
1
+ import { ContentReader } from "../_chunks/client.mjs";
2
+ export declare function fetchReader(url: string): ContentReader;
3
+ export declare const reader: ContentReader;
@@ -0,0 +1,2 @@
1
+ import { fetchReader, reader } from "../_chunks/fetch.mjs";
2
+ export { fetchReader, reader };
@@ -0,0 +1,18 @@
1
+ import { MediaConfig } from "./_chunks/config.mjs";
2
+ import { Plugin } from "./_chunks/libs/rolldown.mjs";
3
+ import { UnpluginFactory } from "unplugin";
4
+ export interface Options {
5
+ root?: string;
6
+ write?: boolean;
7
+ media?: MediaConfig;
8
+ }
9
+ export declare const unpluginFactory: UnpluginFactory<Options | undefined>;
10
+ export declare const unplugin: import("unplugin").UnpluginInstance<Options | undefined, boolean>;
11
+ export declare const vitePlugin: (options?: Options | undefined) => import("unplugin").VitePlugin<any> | import("unplugin").VitePlugin<any>[];
12
+ export declare const rollupPlugin: (options?: Options | undefined) => import("unplugin").RollupPlugin<any> | import("unplugin").RollupPlugin<any>[];
13
+ export declare const rolldownPlugin: (options?: Options | undefined) => Plugin<any> | Plugin<any>[];
14
+ export declare const webpackPlugin: (options?: Options | undefined) => WebpackPluginInstance;
15
+ export declare const rspackPlugin: (options?: Options | undefined) => RspackPluginInstance;
16
+ export declare const farmPlugin: (options?: Options | undefined) => JsPlugin;
17
+ export declare const bunPlugin: (options?: Options | undefined) => BunPlugin;
18
+ export { unplugin as default };