forgepress 0.0.0 → 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/dist/THIRD-PARTY-LICENSES.md +42 -0
- package/dist/_chunks/client.d.mts +2 -0
- package/dist/_chunks/config.mjs +79 -0
- package/dist/_chunks/content.mjs +733 -0
- package/dist/_chunks/error.mjs +4 -0
- package/dist/_chunks/fetch.mjs +13 -0
- package/dist/_chunks/files.mjs +32 -0
- package/dist/_chunks/libs/diff.mjs +485 -0
- package/dist/_chunks/locate.mjs +7 -0
- package/dist/_chunks/media.mjs +282 -0
- package/dist/_chunks/once.mjs +16 -0
- package/dist/_chunks/output.mjs +104 -0
- package/dist/_chunks/overlay.mjs +8 -0
- package/dist/_chunks/plugin.mjs +87 -0
- package/dist/_chunks/preview.mjs +248 -0
- package/dist/_chunks/project.d.mts +7 -0
- package/dist/_chunks/reader.mjs +19 -0
- package/dist/_chunks/reader2.mjs +797 -0
- package/dist/_chunks/references.mjs +56 -0
- package/dist/_chunks/resolve.d.mts +2 -0
- package/dist/_chunks/response.mjs +5 -0
- package/dist/_chunks/routes.mjs +9 -0
- package/dist/_chunks/serialize.mjs +82 -0
- package/dist/_chunks/settings.mjs +328 -0
- package/dist/_chunks/settings2.mjs +2 -0
- package/dist/_chunks/types.d.mts +230 -0
- package/dist/_chunks/types2.d.mts +25 -0
- package/dist/_chunks/value.mjs +139 -0
- package/dist/cli/bin.d.mts +1 -0
- package/dist/cli/bin.mjs +61 -0
- package/dist/disk/reader.d.mts +4 -0
- package/dist/disk/reader.mjs +2 -0
- package/dist/editor/index.d.mts +3 -0
- package/dist/editor/index.mjs +61951 -0
- package/dist/index.d.mts +104 -0
- package/dist/index.mjs +237 -0
- package/dist/next/preview.d.mts +1 -0
- package/dist/next/preview.mjs +3 -0
- package/dist/next/reload.d.mts +1 -0
- package/dist/next/reload.mjs +20 -0
- package/dist/next/settings.d.mts +12 -0
- package/dist/next/settings.mjs +2 -0
- package/dist/plugin/next.d.mts +11 -0
- package/dist/plugin/next.mjs +213 -0
- package/dist/plugin/nuxt.d.mts +7 -0
- package/dist/plugin/nuxt.mjs +52 -0
- package/dist/plugin/watcher.d.mts +1 -0
- package/dist/plugin/watcher.mjs +23 -0
- package/dist/preview/index.d.mts +4 -0
- package/dist/preview/index.mjs +2 -0
- package/dist/preview/react.d.mts +1 -0
- package/dist/preview/react.mjs +34 -0
- package/dist/query/fetch.d.mts +3 -0
- package/dist/query/fetch.mjs +2 -0
- package/dist/unplugin.d.mts +13 -0
- package/dist/unplugin.mjs +2 -0
- package/package.json +146 -2
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Entry, EntryMeta, EntryRef, EntryStatus, ForgePressConfig, ForgePressEntry, ForgePressOutput, ForgePressSchema, ForgePressSchemaRegistry, OutputConfig, OutputMeta, OutputOf, RegisteredSchema, SchemaLocale } from "./_chunks/types.mjs";
|
|
2
|
+
import { OutputCollection, OutputEntry, OutputIndex, OutputManifest } from "./_chunks/types2.mjs";
|
|
3
|
+
export declare function defineForgePressConfig(config: ForgePressConfig): ForgePressConfig;
|
|
4
|
+
type Operator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'contains';
|
|
5
|
+
type CollectionName<TSchema extends ForgePressSchema> = keyof TSchema['collections'] & string;
|
|
6
|
+
type Fields<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>> = TSchema['collections'][TName]['fields'];
|
|
7
|
+
type FieldName<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>> = keyof Fields<TSchema, TName> & string;
|
|
8
|
+
type Simplify<TValue> = { [TKey in keyof TValue]: TValue[TKey]; } & {};
|
|
9
|
+
type Kind<TField> = TField extends {
|
|
10
|
+
type: 'number';
|
|
11
|
+
} ? 'number' : TField extends {
|
|
12
|
+
type: 'text' | 'richtext';
|
|
13
|
+
} ? 'text' : TField extends {
|
|
14
|
+
type: 'relation';
|
|
15
|
+
multiple: true;
|
|
16
|
+
} ? 'links' : TField extends {
|
|
17
|
+
type: 'relation';
|
|
18
|
+
} ? 'link' : TField extends {
|
|
19
|
+
type: 'dynamic';
|
|
20
|
+
} ? 'links' : 'none';
|
|
21
|
+
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';
|
|
22
|
+
interface Operands {
|
|
23
|
+
text: {
|
|
24
|
+
eq: string;
|
|
25
|
+
ne: string;
|
|
26
|
+
in: readonly string[];
|
|
27
|
+
contains: string;
|
|
28
|
+
};
|
|
29
|
+
number: {
|
|
30
|
+
eq: number;
|
|
31
|
+
ne: number;
|
|
32
|
+
gt: number;
|
|
33
|
+
gte: number;
|
|
34
|
+
lt: number;
|
|
35
|
+
lte: number;
|
|
36
|
+
in: readonly number[];
|
|
37
|
+
};
|
|
38
|
+
date: {
|
|
39
|
+
eq: string;
|
|
40
|
+
ne: string;
|
|
41
|
+
gt: string;
|
|
42
|
+
gte: string;
|
|
43
|
+
lt: string;
|
|
44
|
+
lte: string;
|
|
45
|
+
in: readonly string[];
|
|
46
|
+
};
|
|
47
|
+
link: {
|
|
48
|
+
eq: string;
|
|
49
|
+
ne: string;
|
|
50
|
+
in: readonly string[];
|
|
51
|
+
};
|
|
52
|
+
links: {
|
|
53
|
+
contains: string;
|
|
54
|
+
};
|
|
55
|
+
none: Record<never, never>;
|
|
56
|
+
}
|
|
57
|
+
type Keys<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>> = 'id' | 'createdAt' | 'updatedAt' | FieldName<TSchema, TName>;
|
|
58
|
+
type OperatorOf<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>, TKey> = keyof Operands[KindOf<TSchema, TName, TKey>] & Operator;
|
|
59
|
+
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;
|
|
60
|
+
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>];
|
|
61
|
+
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>];
|
|
62
|
+
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>];
|
|
63
|
+
type Linkable<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>> = { [TKey in FieldName<TSchema, TName>]: Fields<TSchema, TName>[TKey] extends {
|
|
64
|
+
type: 'relation' | 'dynamic';
|
|
65
|
+
} ? TKey : never; }[FieldName<TSchema, TName>];
|
|
66
|
+
type LinkedBlock<TSchema extends ForgePressSchema, TTarget> = TTarget extends CollectionName<TSchema> ? {
|
|
67
|
+
collection: TTarget;
|
|
68
|
+
id: string;
|
|
69
|
+
entry: OutputOf<TSchema, TTarget>;
|
|
70
|
+
} : never;
|
|
71
|
+
type Linked<TSchema extends ForgePressSchema, TField> = TField extends {
|
|
72
|
+
type: 'relation';
|
|
73
|
+
collection: infer TTarget extends CollectionName<TSchema>;
|
|
74
|
+
} ? TField extends {
|
|
75
|
+
multiple: true;
|
|
76
|
+
} ? OutputOf<TSchema, TTarget>[] : OutputOf<TSchema, TTarget> : TField extends {
|
|
77
|
+
type: 'dynamic';
|
|
78
|
+
collections: readonly (infer TTarget)[];
|
|
79
|
+
} ? LinkedBlock<TSchema, TTarget>[] : never;
|
|
80
|
+
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; })>;
|
|
81
|
+
type SiteLocale<TSchema extends ForgePressSchema> = [SchemaLocale<TSchema>] extends [never] ? string : SchemaLocale<TSchema>;
|
|
82
|
+
interface QueryBuilder<TSchema extends ForgePressSchema, TName extends CollectionName<TSchema>, TResult> extends Promise<TResult[]> {
|
|
83
|
+
where: {
|
|
84
|
+
<TKey extends Comparable<TSchema, TName>>(field: TKey, value: Operand<TSchema, TName, TKey, 'eq'>): QueryBuilder<TSchema, TName, TResult>;
|
|
85
|
+
<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>;
|
|
86
|
+
};
|
|
87
|
+
sort: (field: Sortable<TSchema, TName>, direction?: 'asc' | 'desc') => QueryBuilder<TSchema, TName, TResult>;
|
|
88
|
+
limit: (count: number) => QueryBuilder<TSchema, TName, TResult>;
|
|
89
|
+
offset: (count: number) => QueryBuilder<TSchema, TName, TResult>;
|
|
90
|
+
locale: (locale: SiteLocale<TSchema>) => QueryBuilder<TSchema, TName, TResult>;
|
|
91
|
+
with: <TKey extends Linkable<TSchema, TName> & keyof TResult>(field: TKey) => QueryBuilder<TSchema, TName, WithLinked<TResult, TKey, Linked<TSchema, Fields<TSchema, TName>[TKey]>>>;
|
|
92
|
+
pick: <TKey extends keyof TResult & string>(...fields: TKey[]) => QueryBuilder<TSchema, TName, Simplify<Pick<TResult, TKey>>>;
|
|
93
|
+
first: () => Promise<TResult | undefined>;
|
|
94
|
+
}
|
|
95
|
+
type Query<TSchema extends ForgePressSchema> = <TName extends CollectionName<TSchema>>(collection: TName) => QueryBuilder<TSchema, TName, OutputOf<TSchema, TName>>;
|
|
96
|
+
interface ClientOptions {
|
|
97
|
+
url?: string;
|
|
98
|
+
}
|
|
99
|
+
interface Client<TSchema extends ForgePressSchema = RegisteredSchema> {
|
|
100
|
+
query: Query<TSchema>;
|
|
101
|
+
}
|
|
102
|
+
export declare function createClient<TSchema extends ForgePressSchema = RegisteredSchema>(options?: ClientOptions): Client<TSchema>;
|
|
103
|
+
export declare const query: Query<RegisteredSchema>;
|
|
104
|
+
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,237 @@
|
|
|
1
|
+
import { OUTPUT_META_KEYS, pick, quote } from "./_chunks/value.mjs";
|
|
2
|
+
import { entryKey, isEntryRef } from "./_chunks/references.mjs";
|
|
3
|
+
import { OUTPUT_INDEX } from "./_chunks/response.mjs";
|
|
4
|
+
import { keyed } from "./_chunks/once.mjs";
|
|
5
|
+
import { fetchReader } from "./_chunks/fetch.mjs";
|
|
6
|
+
import { overlaid } from "./_chunks/overlay.mjs";
|
|
7
|
+
import { reader } from "#content-reader";
|
|
8
|
+
function defineForgePressConfig(config) {
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
11
|
+
function comparable(value) {
|
|
12
|
+
return isEntryRef(value) ? value.id : value;
|
|
13
|
+
}
|
|
14
|
+
function compare(left, right) {
|
|
15
|
+
const first = comparable(left);
|
|
16
|
+
const second = comparable(right);
|
|
17
|
+
if (first === second) return 0;
|
|
18
|
+
if (first === void 0 || first === null) return -1;
|
|
19
|
+
if (second === void 0 || second === null) return 1;
|
|
20
|
+
return first < second ? -1 : 1;
|
|
21
|
+
}
|
|
22
|
+
function ranged(actual, value, accept) {
|
|
23
|
+
return actual !== void 0 && actual !== null && accept(compare(actual, value));
|
|
24
|
+
}
|
|
25
|
+
function matches(entry, { field, op, value }) {
|
|
26
|
+
const actual = entry[field];
|
|
27
|
+
switch (op) {
|
|
28
|
+
case "eq": return comparable(actual) === value;
|
|
29
|
+
case "ne": return comparable(actual) !== value;
|
|
30
|
+
case "gt": return ranged(actual, value, (order) => order > 0);
|
|
31
|
+
case "gte": return ranged(actual, value, (order) => order >= 0);
|
|
32
|
+
case "lt": return ranged(actual, value, (order) => order < 0);
|
|
33
|
+
case "lte": return ranged(actual, value, (order) => order <= 0);
|
|
34
|
+
case "in": return Array.isArray(value) && value.includes(comparable(actual));
|
|
35
|
+
case "contains": return Array.isArray(actual) ? actual.some((item) => comparable(item) === value) : typeof actual === "string" && typeof value === "string" && actual.includes(value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function evaluate(entries, plan) {
|
|
39
|
+
let result = plan.where.length > 0 ? entries.filter((entry) => plan.where.every((clause) => matches(entry, clause))) : [...entries];
|
|
40
|
+
if (plan.sort.length > 0) result.sort((left, right) => {
|
|
41
|
+
for (const { field, dir } of plan.sort) {
|
|
42
|
+
const order = compare(left[field], right[field]);
|
|
43
|
+
if (order !== 0) return dir === "asc" ? order : -order;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
});
|
|
47
|
+
if (plan.offset > 0) result = result.slice(plan.offset);
|
|
48
|
+
if (plan.limit !== void 0) result = result.slice(0, plan.limit);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function refsOf(value) {
|
|
52
|
+
return (Array.isArray(value) ? value : [value]).filter(isEntryRef);
|
|
53
|
+
}
|
|
54
|
+
function listing(names) {
|
|
55
|
+
return names.map(quote).join(", ");
|
|
56
|
+
}
|
|
57
|
+
var Builder = class {
|
|
58
|
+
loader;
|
|
59
|
+
collection;
|
|
60
|
+
plan = {
|
|
61
|
+
where: [],
|
|
62
|
+
sort: [],
|
|
63
|
+
offset: 0,
|
|
64
|
+
with: []
|
|
65
|
+
};
|
|
66
|
+
chosen;
|
|
67
|
+
constructor(loader, collection) {
|
|
68
|
+
this.loader = loader;
|
|
69
|
+
this.collection = collection;
|
|
70
|
+
}
|
|
71
|
+
where(field, operatorOrValue, value) {
|
|
72
|
+
this.plan.where.push(arguments.length >= 3 ? {
|
|
73
|
+
field,
|
|
74
|
+
op: operatorOrValue,
|
|
75
|
+
value
|
|
76
|
+
} : {
|
|
77
|
+
field,
|
|
78
|
+
op: "eq",
|
|
79
|
+
value: operatorOrValue
|
|
80
|
+
});
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
sort(field, direction = "asc") {
|
|
84
|
+
this.plan.sort.push({
|
|
85
|
+
field,
|
|
86
|
+
dir: direction
|
|
87
|
+
});
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
limit(count) {
|
|
91
|
+
this.plan.limit = count;
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
offset(count) {
|
|
95
|
+
this.plan.offset = count;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
locale(locale) {
|
|
99
|
+
this.chosen = locale;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
with(field) {
|
|
103
|
+
if (!this.plan.with.includes(field)) this.plan.with.push(field);
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
pick(...fields) {
|
|
107
|
+
this.plan.pick = fields;
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
needsLocale(snapshot, reason) {
|
|
111
|
+
const example = snapshot.index.locales[0] ?? "en";
|
|
112
|
+
return /* @__PURE__ */ new Error(`[forgepress] ${reason}, so query(${quote(this.collection)}) needs .locale(), such as .locale(${quote(example)})`);
|
|
113
|
+
}
|
|
114
|
+
async select(snapshot, limit) {
|
|
115
|
+
const { index } = snapshot;
|
|
116
|
+
const { collection, chosen: locale, plan } = this;
|
|
117
|
+
const found = index.collections[collection];
|
|
118
|
+
if (!found) throw new Error(`[forgepress] the content output has no collection ${quote(collection)}`);
|
|
119
|
+
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"}`);
|
|
120
|
+
if (found.localized && locale === void 0) throw this.needsLocale(snapshot, `${quote(collection)} is translated`);
|
|
121
|
+
const manifest = await snapshot.manifest(collection, locale);
|
|
122
|
+
const unlinked = plan.with.filter((field) => manifest.links[field] === void 0);
|
|
123
|
+
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)}`);
|
|
124
|
+
const listed = /* @__PURE__ */ new Set([...OUTPUT_META_KEYS, ...manifest.indexed]);
|
|
125
|
+
const unindexed = [...new Set([...plan.where, ...plan.sort].map((clause) => clause.field))].filter((field) => !listed.has(field));
|
|
126
|
+
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`);
|
|
127
|
+
const selected = evaluate(unindexed.length > 0 ? await snapshot.entries(collection, locale, manifest.entries.map((entry) => entry.id)) : manifest.entries, limit === void 0 ? plan : {
|
|
128
|
+
...plan,
|
|
129
|
+
limit: Math.min(plan.limit ?? limit, limit)
|
|
130
|
+
});
|
|
131
|
+
const listedOnly = plan.pick !== void 0 && [...plan.pick, ...plan.with].every((field) => listed.has(field));
|
|
132
|
+
const rows = unindexed.length > 0 || listedOnly ? selected : await snapshot.entries(collection, locale, selected.map((entry) => entry.id));
|
|
133
|
+
const linked = plan.with.length > 0 ? await this.link(snapshot, manifest, rows) : rows;
|
|
134
|
+
const fields = plan.pick;
|
|
135
|
+
const picked = fields === void 0 ? linked : linked.map((row) => pick(row, fields));
|
|
136
|
+
return structuredClone(picked);
|
|
137
|
+
}
|
|
138
|
+
async link(snapshot, manifest, rows) {
|
|
139
|
+
const { index } = snapshot;
|
|
140
|
+
const locale = this.chosen;
|
|
141
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
142
|
+
for (const field of this.plan.with) for (const ref of rows.flatMap((row) => refsOf(row[field]))) {
|
|
143
|
+
if (index.collections[ref.collection]?.localized && locale === void 0) throw this.needsLocale(snapshot, `.with(${quote(field)}) loads ${quote(ref.collection)} entries, which are translated`);
|
|
144
|
+
wanted.set(ref.collection, (wanted.get(ref.collection) ?? /* @__PURE__ */ new Set()).add(ref.id));
|
|
145
|
+
}
|
|
146
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
147
|
+
await Promise.all([...wanted].map(async ([collection, ids]) => {
|
|
148
|
+
for (const entry of await snapshot.entries(collection, locale, [...ids])) loaded.set(entryKey(collection, entry.id), entry);
|
|
149
|
+
}));
|
|
150
|
+
const find = (ref) => loaded.get(entryKey(ref.collection, ref.id));
|
|
151
|
+
return rows.map((row) => {
|
|
152
|
+
const next = { ...row };
|
|
153
|
+
for (const field of this.plan.with) {
|
|
154
|
+
const value = row[field];
|
|
155
|
+
if (value === void 0) continue;
|
|
156
|
+
if (manifest.links[field] === "dynamic") next[field] = refsOf(value).flatMap((ref) => [find(ref)].flatMap((entry) => entry ? [{
|
|
157
|
+
collection: ref.collection,
|
|
158
|
+
id: ref.id,
|
|
159
|
+
entry
|
|
160
|
+
}] : []));
|
|
161
|
+
else if (Array.isArray(value)) next[field] = refsOf(value).flatMap((ref) => find(ref) ?? []);
|
|
162
|
+
else next[field] = refsOf(value).map(find)[0];
|
|
163
|
+
}
|
|
164
|
+
return next;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
execute(limit) {
|
|
168
|
+
return this.loader.run((snapshot) => this.select(snapshot, limit));
|
|
169
|
+
}
|
|
170
|
+
async first() {
|
|
171
|
+
return (await this.execute(1))[0];
|
|
172
|
+
}
|
|
173
|
+
then(onFulfilled, onRejected) {
|
|
174
|
+
return this.execute().then(onFulfilled, onRejected);
|
|
175
|
+
}
|
|
176
|
+
catch(onRejected) {
|
|
177
|
+
return this.execute().catch(onRejected);
|
|
178
|
+
}
|
|
179
|
+
finally(onFinally) {
|
|
180
|
+
return this.execute().finally(onFinally);
|
|
181
|
+
}
|
|
182
|
+
get [Symbol.toStringTag]() {
|
|
183
|
+
return "ForgePressQuery";
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
var MissingFile = class extends Error {};
|
|
187
|
+
function manifestPath(index, collection, locale) {
|
|
188
|
+
const found = index.collections[collection];
|
|
189
|
+
if (!found) throw new Error(`[forgepress] the content output has no collection ${quote(collection)}`);
|
|
190
|
+
if (!found.localized) return found.manifest;
|
|
191
|
+
const path = locale === void 0 ? void 0 : found.manifests[locale];
|
|
192
|
+
if (path === void 0) throw new Error(`[forgepress] the content output has no ${quote(collection)} entries in locale ${quote(locale)}`);
|
|
193
|
+
return path;
|
|
194
|
+
}
|
|
195
|
+
function createLoader(read) {
|
|
196
|
+
const files = keyed();
|
|
197
|
+
function file(path) {
|
|
198
|
+
return files(path, () => read(path).then((value) => {
|
|
199
|
+
if (value === void 0) throw new MissingFile(path);
|
|
200
|
+
return value;
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
async function snapshot() {
|
|
204
|
+
const index = await read(OUTPUT_INDEX);
|
|
205
|
+
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`);
|
|
206
|
+
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`);
|
|
207
|
+
const manifest = (collection, locale) => file(manifestPath(index, collection, locale));
|
|
208
|
+
return {
|
|
209
|
+
index,
|
|
210
|
+
manifest,
|
|
211
|
+
entries: async (collection, locale, ids) => {
|
|
212
|
+
const { files: paths } = await manifest(collection, locale);
|
|
213
|
+
return Promise.all(ids.flatMap((id) => paths[id] === void 0 ? [] : [file(paths[id])]));
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return { async run(task) {
|
|
218
|
+
try {
|
|
219
|
+
return await task(await snapshot());
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (!(error instanceof MissingFile)) throw error;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
return await task(await snapshot());
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error instanceof MissingFile) throw new Error(`[forgepress] the content output links ${error.message}, but the file doesn't exist`);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
} };
|
|
230
|
+
}
|
|
231
|
+
function createClient(options = {}) {
|
|
232
|
+
const loader = createLoader(overlaid(options.url === void 0 ? reader : fetchReader(options.url)));
|
|
233
|
+
const query = (collection) => new Builder(loader, collection);
|
|
234
|
+
return { query };
|
|
235
|
+
}
|
|
236
|
+
const query = createClient().query;
|
|
237
|
+
export { createClient, defineForgePressConfig, query };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EVENTS } from "../_chunks/routes.mjs";
|
|
2
|
+
import { settings_default } from "../_chunks/settings2.mjs";
|
|
3
|
+
const RETRY = 1e3;
|
|
4
|
+
const LONGEST_RETRY = 3e4;
|
|
5
|
+
function connect(delay) {
|
|
6
|
+
const socket = new WebSocket(`${settings_default.devServer.replace(/^http/, "ws")}${EVENTS}`);
|
|
7
|
+
let opened = false;
|
|
8
|
+
socket.addEventListener("open", () => {
|
|
9
|
+
opened = true;
|
|
10
|
+
});
|
|
11
|
+
socket.addEventListener("message", (event) => {
|
|
12
|
+
if (event.data === "reload") location.reload();
|
|
13
|
+
});
|
|
14
|
+
socket.addEventListener("close", () => {
|
|
15
|
+
const next = opened ? RETRY : Math.min(delay * 2, LONGEST_RETRY);
|
|
16
|
+
setTimeout(connect, next, next);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
connect(RETRY);
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ContentConfig, ProviderConfig, ResolvedMedia } from "../_chunks/types.mjs";
|
|
2
|
+
import "../_chunks/resolve.mjs";
|
|
3
|
+
interface EditorSettings {
|
|
4
|
+
local: boolean;
|
|
5
|
+
devServer: string;
|
|
6
|
+
provider?: ProviderConfig | undefined;
|
|
7
|
+
format?: ContentConfig | undefined;
|
|
8
|
+
contentPath: string;
|
|
9
|
+
media: ResolvedMedia;
|
|
10
|
+
}
|
|
11
|
+
declare const _default: EditorSettings;
|
|
12
|
+
export { _default as default };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Options } from "../_chunks/project.mjs";
|
|
2
|
+
import { NextConfig } from "next";
|
|
3
|
+
export interface NextOptions extends Options {
|
|
4
|
+
preview?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export type NextConfigFunction = (phase: string, context: {
|
|
7
|
+
defaultConfig: NextConfig;
|
|
8
|
+
}) => NextConfig | Promise<NextConfig>;
|
|
9
|
+
export declare function withForgePress(nextConfig: NextConfig | NextConfigFunction, options?: NextOptions): (phase: string, context: {
|
|
10
|
+
defaultConfig: NextConfig;
|
|
11
|
+
}) => Promise<NextConfig>;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { isRecord } from "../_chunks/value.mjs";
|
|
2
|
+
import { keyed } from "../_chunks/once.mjs";
|
|
3
|
+
import { errorMessage } from "../_chunks/error.mjs";
|
|
4
|
+
import { buildOutput } from "../_chunks/output.mjs";
|
|
5
|
+
import { toPosix } from "../_chunks/files.mjs";
|
|
6
|
+
import { EVENTS } from "../_chunks/routes.mjs";
|
|
7
|
+
import { SETTINGS_ID, createDevContent, editorSettings, loadProject, syncTypes } from "../_chunks/settings.mjs";
|
|
8
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { createServer } from "node:http";
|
|
13
|
+
import { Worker, isMainThread } from "node:worker_threads";
|
|
14
|
+
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
15
|
+
import { WebSocketServer } from "ws";
|
|
16
|
+
const DEVELOPMENT = "phase-development-server";
|
|
17
|
+
const BUILD = "phase-production-build";
|
|
18
|
+
const SETTINGS_MODULE = "forgepress/next/settings";
|
|
19
|
+
const SETTINGS_VARIABLE = "__FORGEPRESS_SETTINGS__";
|
|
20
|
+
const PREVIEW_MODULE = "forgepress/next/preview";
|
|
21
|
+
const RELOAD_MODULE = "forgepress/next/reload";
|
|
22
|
+
const VUE_FLAGS = {
|
|
23
|
+
__VUE_OPTIONS_API__: true,
|
|
24
|
+
__VUE_PROD_DEVTOOLS__: false,
|
|
25
|
+
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
|
|
26
|
+
};
|
|
27
|
+
const SERVERS = Symbol.for("forgepress:next:servers");
|
|
28
|
+
const LOCAL_ORIGIN = /^https?:\/\/(?:(?:[^/:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/;
|
|
29
|
+
const PREFLIGHT = {
|
|
30
|
+
"access-control-allow-methods": "GET, POST, DELETE",
|
|
31
|
+
"access-control-allow-headers": "content-type",
|
|
32
|
+
"access-control-max-age": "600"
|
|
33
|
+
};
|
|
34
|
+
const INDENT = /^[ \t]+(?=\S)/m;
|
|
35
|
+
const DIST = `${dirname(dirname(fileURLToPath(import.meta.url)))}${sep}`;
|
|
36
|
+
const WATCHER = new URL(`./watcher${extname(fileURLToPath(import.meta.url))}`, import.meta.url);
|
|
37
|
+
function dynamicConfigImport(warning) {
|
|
38
|
+
return Boolean(warning.module?.resource?.startsWith(DIST) && warning.message?.includes("the request of a dependency is an expression"));
|
|
39
|
+
}
|
|
40
|
+
const logger = {
|
|
41
|
+
info: (message) => process.stdout.write(`${message}\n`),
|
|
42
|
+
warn: (message) => console.warn(message),
|
|
43
|
+
error: (message) => console.error(message)
|
|
44
|
+
};
|
|
45
|
+
function localPage(request) {
|
|
46
|
+
const { origin } = request.headers;
|
|
47
|
+
const site = request.headers["sec-fetch-site"];
|
|
48
|
+
if (origin !== void 0) return LOCAL_ORIGIN.test(origin);
|
|
49
|
+
return site === void 0 || site === "none" || site === "same-origin";
|
|
50
|
+
}
|
|
51
|
+
const LOCAL_PAGES = {
|
|
52
|
+
accepts: localPage,
|
|
53
|
+
refusal: "the dev endpoint only accepts requests from pages opened on localhost"
|
|
54
|
+
};
|
|
55
|
+
function end(response, status, text) {
|
|
56
|
+
response.statusCode = status;
|
|
57
|
+
response.end(text);
|
|
58
|
+
}
|
|
59
|
+
function servers() {
|
|
60
|
+
const scope = globalThis;
|
|
61
|
+
scope[SERVERS] ??= keyed();
|
|
62
|
+
return scope[SERVERS];
|
|
63
|
+
}
|
|
64
|
+
function formatting(text) {
|
|
65
|
+
const indent = INDENT.exec(text)?.[0] ?? " ";
|
|
66
|
+
return {
|
|
67
|
+
insertSpaces: !indent.startsWith(" "),
|
|
68
|
+
tabSize: indent.length,
|
|
69
|
+
eol: text.includes("\r\n") ? "\r\n" : "\n"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function includeContent(root, config, tsconfig) {
|
|
73
|
+
const file = resolve(root, tsconfig);
|
|
74
|
+
if (!existsSync(file)) return;
|
|
75
|
+
const folder = toPosix(relative(dirname(file), join(root, config.paths.dir)));
|
|
76
|
+
const pattern = `${folder}/**/*.ts`;
|
|
77
|
+
if (!folder.split("/").some((segment) => segment.startsWith("."))) return;
|
|
78
|
+
const text = readFileSync(file, "utf8");
|
|
79
|
+
const errors = [];
|
|
80
|
+
const parsed = parse(text, errors, { allowTrailingComma: true });
|
|
81
|
+
if (errors.length > 0 || !isRecord(parsed) || !Array.isArray(parsed.include)) {
|
|
82
|
+
logger.warn(`[forgepress] add ${JSON.stringify(pattern)} to "include" in ${basename(file)}, so TypeScript knows the schema`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (parsed.include.includes(pattern)) return;
|
|
86
|
+
writeFileSync(file, applyEdits(text, modify(text, ["include", parsed.include.length], pattern, {
|
|
87
|
+
isArrayInsertion: true,
|
|
88
|
+
formattingOptions: formatting(text)
|
|
89
|
+
})));
|
|
90
|
+
logger.info(`[forgepress] added ${JSON.stringify(pattern)} to "include" in ${basename(file)}, so TypeScript knows the schema`);
|
|
91
|
+
}
|
|
92
|
+
function watchContent(dir, schema, changed) {
|
|
93
|
+
const watcher = new Worker(WATCHER, { workerData: {
|
|
94
|
+
dir,
|
|
95
|
+
schema
|
|
96
|
+
} });
|
|
97
|
+
let watching = false;
|
|
98
|
+
return new Promise((ready, fail) => {
|
|
99
|
+
watcher.on("message", (file) => {
|
|
100
|
+
if (file !== null) return changed(file);
|
|
101
|
+
watching = true;
|
|
102
|
+
watcher.unref();
|
|
103
|
+
ready();
|
|
104
|
+
});
|
|
105
|
+
watcher.on("error", (error) => {
|
|
106
|
+
if (watching) logger.error(`[forgepress] stopped watching the content folder: ${errorMessage(error)}`);
|
|
107
|
+
else fail(error);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function listen(server) {
|
|
112
|
+
return new Promise((done, fail) => {
|
|
113
|
+
server.once("error", fail);
|
|
114
|
+
server.listen(0, "127.0.0.1", () => done(`127.0.0.1:${server.address().port}`));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async function startDevServer(root, config, write) {
|
|
118
|
+
const pages = new WebSocketServer({ noServer: true });
|
|
119
|
+
const dev = createDevContent(root, config, logger, () => {
|
|
120
|
+
for (const page of pages.clients) page.send("reload");
|
|
121
|
+
}, LOCAL_PAGES);
|
|
122
|
+
await watchContent(join(root, config.paths.dir), join(root, config.paths.schema), dev.changed);
|
|
123
|
+
await dev.refresh();
|
|
124
|
+
const server = createServer();
|
|
125
|
+
const host = await listen(server);
|
|
126
|
+
server.on("request", (request, response) => {
|
|
127
|
+
const { origin } = request.headers;
|
|
128
|
+
if (request.headers.host !== host) return end(response, 403);
|
|
129
|
+
if (origin !== void 0) {
|
|
130
|
+
response.setHeader("access-control-allow-origin", origin);
|
|
131
|
+
response.setHeader("vary", "origin");
|
|
132
|
+
}
|
|
133
|
+
if (request.method === "OPTIONS") {
|
|
134
|
+
if (!LOCAL_PAGES.accepts(request)) return end(response, 403, LOCAL_PAGES.refusal);
|
|
135
|
+
for (const [name, value] of Object.entries(PREFLIGHT)) response.setHeader(name, value);
|
|
136
|
+
return end(response, 204);
|
|
137
|
+
}
|
|
138
|
+
if (!write) return end(response, 404);
|
|
139
|
+
dev.endpoint(request, response, () => end(response, 404));
|
|
140
|
+
});
|
|
141
|
+
server.on("upgrade", (request, socket, head) => {
|
|
142
|
+
if (request.headers.host !== host || request.url !== EVENTS || !LOCAL_PAGES.accepts(request)) {
|
|
143
|
+
socket.destroy();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
socket.unref();
|
|
147
|
+
pages.handleUpgrade(request, socket, head, (page) => page.on("error", () => page.terminate()));
|
|
148
|
+
});
|
|
149
|
+
server.unref();
|
|
150
|
+
return `http://${host}`;
|
|
151
|
+
}
|
|
152
|
+
function devServer(root, config, write) {
|
|
153
|
+
return servers()(root, () => startDevServer(root, config, write));
|
|
154
|
+
}
|
|
155
|
+
async function prepare(phase, next, options) {
|
|
156
|
+
const project = await loadProject(process.cwd(), options);
|
|
157
|
+
const { root, config } = project;
|
|
158
|
+
const serving = phase === DEVELOPMENT && isMainThread;
|
|
159
|
+
if (isMainThread && (phase === DEVELOPMENT || phase === BUILD)) {
|
|
160
|
+
syncTypes(root, config);
|
|
161
|
+
includeContent(root, config, next.typescript?.tsconfigPath ?? "tsconfig.json");
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
...project,
|
|
165
|
+
local: phase === DEVELOPMENT && options.write !== false,
|
|
166
|
+
server: serving ? await devServer(root, config, options.write !== false) : void 0
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function extend(next, project, options) {
|
|
170
|
+
const extended = {
|
|
171
|
+
compiler: {
|
|
172
|
+
...next.compiler,
|
|
173
|
+
define: {
|
|
174
|
+
...VUE_FLAGS,
|
|
175
|
+
...next.compiler?.define,
|
|
176
|
+
[SETTINGS_VARIABLE]: JSON.stringify(editorSettings(project.local, project.config, project.server))
|
|
177
|
+
},
|
|
178
|
+
runAfterProductionCompile: async (metadata) => {
|
|
179
|
+
await buildOutput(project.root, project.config);
|
|
180
|
+
await next.compiler?.runAfterProductionCompile?.(metadata);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
turbopack: {
|
|
184
|
+
...next.turbopack,
|
|
185
|
+
resolveAlias: {
|
|
186
|
+
...next.turbopack?.resolveAlias,
|
|
187
|
+
[SETTINGS_ID]: SETTINGS_MODULE
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
webpack: (config, context) => {
|
|
191
|
+
const result = next.webpack ? next.webpack(config, context) : config;
|
|
192
|
+
result.plugins = [...result.plugins ?? [], new context.webpack.NormalModuleReplacementPlugin(/^virtual:forgepress\/settings$/, SETTINGS_MODULE)];
|
|
193
|
+
result.ignoreWarnings = [...result.ignoreWarnings ?? [], dynamicConfigImport];
|
|
194
|
+
return result;
|
|
195
|
+
},
|
|
196
|
+
instrumentationClientInject: [
|
|
197
|
+
...next.instrumentationClientInject ?? [],
|
|
198
|
+
...options.preview === false ? [] : [PREVIEW_MODULE],
|
|
199
|
+
...project.server ? [RELOAD_MODULE] : []
|
|
200
|
+
]
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
...next,
|
|
204
|
+
...extended
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function withForgePress(nextConfig, options = {}) {
|
|
208
|
+
return async (phase, context) => {
|
|
209
|
+
const found = typeof nextConfig === "function" ? await nextConfig(phase, context) : nextConfig;
|
|
210
|
+
return extend(found, await prepare(phase, found, options), options);
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export { withForgePress };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { toPosix } from "../_chunks/files.mjs";
|
|
2
|
+
import { loadProject } from "../_chunks/settings.mjs";
|
|
3
|
+
import { configFile } from "../_chunks/config.mjs";
|
|
4
|
+
import { vitePlugin } from "../_chunks/plugin.mjs";
|
|
5
|
+
import { join, relative } from "node:path";
|
|
6
|
+
import { addPluginTemplate, addVitePlugin, defineNuxtModule } from "@nuxt/kit";
|
|
7
|
+
const PREVIEW_PLUGIN = `import { defineNuxtPlugin, refreshNuxtData, usePreviewMode } from '#app'
|
|
8
|
+
import { enablePreview, onPreviewChange, previewing } from 'forgepress/preview'
|
|
9
|
+
|
|
10
|
+
export default defineNuxtPlugin({
|
|
11
|
+
name: 'forgepress:preview',
|
|
12
|
+
setup(nuxtApp) {
|
|
13
|
+
enablePreview()
|
|
14
|
+
|
|
15
|
+
const { enabled } = usePreviewMode({ shouldEnable: previewing })
|
|
16
|
+
|
|
17
|
+
onPreviewChange(() => nuxtApp.runWithContext(() => {
|
|
18
|
+
enabled.value = previewing()
|
|
19
|
+
|
|
20
|
+
return refreshNuxtData()
|
|
21
|
+
}))
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
`;
|
|
25
|
+
const forgepress = defineNuxtModule({
|
|
26
|
+
meta: {
|
|
27
|
+
name: "forgepress",
|
|
28
|
+
configKey: "forgepress"
|
|
29
|
+
},
|
|
30
|
+
async setup(options, nuxt) {
|
|
31
|
+
const { root, config } = await loadProject(nuxt.options.rootDir, options);
|
|
32
|
+
addVitePlugin(vitePlugin({
|
|
33
|
+
...options,
|
|
34
|
+
root
|
|
35
|
+
}));
|
|
36
|
+
nuxt.hook("prepare:types", ({ tsConfig, nodeTsConfig }) => {
|
|
37
|
+
tsConfig.include ??= [];
|
|
38
|
+
tsConfig.include.push(toPosix(relative(nuxt.options.buildDir, join(root, config.paths.dir, "**/*.ts"))));
|
|
39
|
+
const file = configFile(root);
|
|
40
|
+
if (file) {
|
|
41
|
+
nodeTsConfig.include ??= [];
|
|
42
|
+
nodeTsConfig.include.push(toPosix(relative(nuxt.options.buildDir, join(root, file))));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
if (options.preview !== false) addPluginTemplate({
|
|
46
|
+
filename: "forgepress/preview.client.mjs",
|
|
47
|
+
getContents: () => PREVIEW_PLUGIN,
|
|
48
|
+
mode: "client"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
export { forgepress as default };
|