create-sprinkles 0.2.3
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/bin.mjs +295 -0
- package/dist/index.d.mts +46 -0
- package/dist/index.mjs +180 -0
- package/package.json +45 -0
- package/templates/react-router-convex/.env.local +2 -0
- package/templates/react-router-convex/convex/schema.ts +9 -0
- package/templates/react-router-rsc/app/home.tsx.hbs +31 -0
- package/templates/react-router-rsc/app/root.tsx.hbs +55 -0
- package/templates/react-router-rsc/tsconfig.json.hbs +31 -0
- package/templates/react-router-rsc/workers/entry.rsc.tsx +44 -0
- package/templates/react-router-rsc/workers/entry.ssr.tsx +41 -0
- package/templates/react-router-rsc/wrangler.rsc.jsonc.hbs +25 -0
- package/templates/react-router-rsc/wrangler.ssr.jsonc.hbs +14 -0
- package/templates/react-router-rsc-content-layer/app/content.config.ts.hbs +26 -0
- package/templates/react-router-rsc-content-layer/content-layer/api.ts +350 -0
- package/templates/react-router-rsc-content-layer/content-layer/codegen.ts +89 -0
- package/templates/react-router-rsc-content-layer/content-layer/config.ts +20 -0
- package/templates/react-router-rsc-content-layer/content-layer/digest.ts +6 -0
- package/templates/react-router-rsc-content-layer/content-layer/frontmatter.ts +19 -0
- package/templates/react-router-rsc-content-layer/content-layer/loaders/file.ts +55 -0
- package/templates/react-router-rsc-content-layer/content-layer/loaders/glob.ts +82 -0
- package/templates/react-router-rsc-content-layer/content-layer/loaders/index.ts +2 -0
- package/templates/react-router-rsc-content-layer/content-layer/plugin.ts +419 -0
- package/templates/react-router-rsc-content-layer/content-layer/resolve-hook.js +12 -0
- package/templates/react-router-rsc-content-layer/content-layer/runtime.ts +73 -0
- package/templates/react-router-rsc-content-layer/content-layer/store.ts +59 -0
- package/templates/react-router-spa/app/home.tsx.hbs +7 -0
- package/templates/react-router-spa/app/root.tsx.hbs +60 -0
- package/templates/react-router-spa/tsconfig.json.hbs +26 -0
- package/templates/react-router-spa/wrangler.jsonc.hbs +9 -0
- package/templates/react-router-ssr/app/home.tsx.hbs +21 -0
- package/templates/react-router-ssr/app/root.tsx.hbs +105 -0
- package/templates/react-router-ssr/convex/schema.ts +7 -0
- package/templates/react-router-ssr/tsconfig.json.hbs +28 -0
- package/templates/react-router-ssr/wrangler.jsonc.hbs +13 -0
- package/templates/react-router-ssr-convex/app/lib/client.ts +19 -0
- package/templates/react-router-ssr-convex/app/tanstack-query-integration/middleware.ts +18 -0
- package/templates/react-router-ssr-convex/app/tanstack-query-integration/query-preloader.ts +125 -0
- package/templates/react-shared/app/routes.ts.hbs +3 -0
- package/templates/react-shared/app/styles/tailwind.css +1 -0
- package/templates/react-shared/react-compiler.plugin.ts.hbs +10 -0
- package/templates/react-shared/react-router.config.ts.hbs +9 -0
- package/templates/shared/.gitignore.hbs +23 -0
- package/templates/shared/.node-version +1 -0
- package/templates/shared/.vscode/extensions.json.hbs +8 -0
- package/templates/shared/.vscode/settings.json.hbs +72 -0
- package/templates/shared/AGENTS.md.hbs +599 -0
- package/templates/shared/README.md.hbs +24 -0
- package/templates/shared/package.json.hbs +41 -0
- package/templates/shared/vite.config.ts.hbs +384 -0
- package/templates/ts-package/src/index.ts +3 -0
- package/templates/ts-package/tests/index.test.ts +9 -0
- package/templates/ts-package/tsconfig.json +18 -0
- package/templates/ts-package-cli/bin/index.ts.hbs +1 -0
- package/templates/ts-package-cli/src/cli.ts.hbs +37 -0
- package/templates/ts-package-generator/bin/create.ts.hbs +2 -0
- package/templates/ts-package-generator/src/template.ts.hbs +22 -0
- package/templates/ts-package-sea/sea-config.json.hbs +2 -0
- package/templates/ts-package-sea/src/sea-entry.ts.hbs +4 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": [
|
|
3
|
+
"**/*.ts",
|
|
4
|
+
"**/*.tsx",
|
|
5
|
+
{{#if hasContentLayer}}
|
|
6
|
+
"./.react-router/types/**/*",
|
|
7
|
+
"./.sprinkles/content-layer/**/*"
|
|
8
|
+
{{else}}
|
|
9
|
+
"./.react-router/types/**/*"
|
|
10
|
+
{{/if}}
|
|
11
|
+
],
|
|
12
|
+
"compilerOptions": {
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"verbatimModuleSyntax": true,
|
|
19
|
+
"noEmit": true,
|
|
20
|
+
"moduleResolution": "Bundler",
|
|
21
|
+
"module": "ESNext",
|
|
22
|
+
"target": "ESNext",
|
|
23
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
24
|
+
"types": ["vite/client", "@vitejs/plugin-rsc/types", "@types/node"],
|
|
25
|
+
"jsx": "react-jsx",
|
|
26
|
+
"rootDirs": [".", "./.react-router/types"],
|
|
27
|
+
"paths": {
|
|
28
|
+
"~/*": ["./app/*"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTemporaryReferenceSet,
|
|
3
|
+
decodeAction,
|
|
4
|
+
decodeFormState,
|
|
5
|
+
decodeReply,
|
|
6
|
+
loadServerAction,
|
|
7
|
+
renderToReadableStream,
|
|
8
|
+
} from "@vitejs/plugin-rsc/rsc";
|
|
9
|
+
import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
|
|
10
|
+
// @ts-expect-error no types
|
|
11
|
+
import routes from "virtual:react-router/unstable_rsc/routes";
|
|
12
|
+
// @ts-expect-error no types
|
|
13
|
+
import basename from "virtual:react-router/unstable_rsc/basename";
|
|
14
|
+
|
|
15
|
+
export function fetchServer(request: Request) {
|
|
16
|
+
return matchRSCServerRequest({
|
|
17
|
+
basename,
|
|
18
|
+
createTemporaryReferenceSet,
|
|
19
|
+
decodeAction,
|
|
20
|
+
decodeFormState,
|
|
21
|
+
decodeReply,
|
|
22
|
+
generateResponse(match, options) {
|
|
23
|
+
return new Response(renderToReadableStream(match.payload, options), {
|
|
24
|
+
status: match.statusCode,
|
|
25
|
+
headers: match.headers,
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
loadServerAction,
|
|
29
|
+
request,
|
|
30
|
+
routes,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (import.meta.hot) {
|
|
35
|
+
import.meta.hot.accept();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handler: ExportedHandler<Env> = {
|
|
39
|
+
fetch(request) {
|
|
40
|
+
return fetchServer(request);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default handler;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
|
|
2
|
+
import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge";
|
|
3
|
+
import {
|
|
4
|
+
unstable_RSCStaticRouter as RSCStaticRouter,
|
|
5
|
+
unstable_routeRSCServerRequest as routeRSCServerRequest,
|
|
6
|
+
} from "react-router";
|
|
7
|
+
|
|
8
|
+
export async function generateHTML(request: Request, serverResponse: Response): Promise<Response> {
|
|
9
|
+
return await routeRSCServerRequest({
|
|
10
|
+
// The incoming request.
|
|
11
|
+
request,
|
|
12
|
+
// The React Server response.
|
|
13
|
+
serverResponse,
|
|
14
|
+
// Provide the React Server touchpoints.
|
|
15
|
+
createFromReadableStream,
|
|
16
|
+
// Render the router to HTML.
|
|
17
|
+
async renderHTML(getPayload) {
|
|
18
|
+
const payload = await getPayload();
|
|
19
|
+
const formState = payload.type === "render" ? await payload.formState : undefined;
|
|
20
|
+
|
|
21
|
+
const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent(
|
|
22
|
+
"index",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return await renderHTMLToReadableStream(<RSCStaticRouter getPayload={getPayload} />, {
|
|
26
|
+
bootstrapScriptContent,
|
|
27
|
+
// @ts-expect-error - no types for this yet
|
|
28
|
+
formState,
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handler: ExportedHandler<Env> = {
|
|
35
|
+
async fetch(request, env) {
|
|
36
|
+
let serverResponse = await env.RSC.fetch(request);
|
|
37
|
+
return generateHTML(request, serverResponse);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default handler;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "{{repository}}",
|
|
4
|
+
"main": "workers/entry.rsc.tsx",
|
|
5
|
+
"assets": {
|
|
6
|
+
"directory": "./build/client"
|
|
7
|
+
},
|
|
8
|
+
"services": [
|
|
9
|
+
{
|
|
10
|
+
"binding": "RSC",
|
|
11
|
+
"service": "{{repository}}"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"workers_dev": true,
|
|
15
|
+
"compatibility_date": "2026-02-28",
|
|
16
|
+
"compatibility_flags": ["nodejs_als", "nodejs_compat"],
|
|
17
|
+
"observability": {
|
|
18
|
+
"logs": {
|
|
19
|
+
"enabled": true,
|
|
20
|
+
"head_sampling_rate": 1,
|
|
21
|
+
"invocation_logs": true,
|
|
22
|
+
"persist": true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "{{repository}}-ssr",
|
|
4
|
+
"main": "workers/entry.ssr.tsx",
|
|
5
|
+
"workers_dev": true,
|
|
6
|
+
"services": [
|
|
7
|
+
{
|
|
8
|
+
"binding": "RSC",
|
|
9
|
+
"service": "{{repository}}"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"compatibility_date": "2026-02-28",
|
|
13
|
+
"compatibility_flags": ["nodejs_als", "nodejs_compat"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as s from "@remix-run/data-schema";
|
|
2
|
+
import { defineCollection } from "sprinkles:content";
|
|
3
|
+
import { file, glob } from "../content-layer/loaders/index.ts";
|
|
4
|
+
|
|
5
|
+
let posts = defineCollection({
|
|
6
|
+
loader: glob({
|
|
7
|
+
pattern: "**/[^_]*.{md,mdx}",
|
|
8
|
+
base: "app/content/posts",
|
|
9
|
+
}),
|
|
10
|
+
schema: s.object({
|
|
11
|
+
title: s.string(),
|
|
12
|
+
description: s.string(),
|
|
13
|
+
publishedOn: s.string(),
|
|
14
|
+
isDraft: s.defaulted(s.boolean(), false),
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let settings = defineCollection({
|
|
19
|
+
loader: file("app/content/settings.jsonc"),
|
|
20
|
+
schema: s.object({
|
|
21
|
+
id: s.string(),
|
|
22
|
+
label: s.string(),
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export let collections = { posts, settings };
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import type { Schema } from "@remix-run/data-schema";
|
|
2
|
+
import type { ComponentType } from "react";
|
|
3
|
+
import type { FSWatcher } from "vite-plus";
|
|
4
|
+
|
|
5
|
+
function todo(): never {
|
|
6
|
+
throw new Error("Not implemented");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DataEntry<D extends Record<string, unknown> = Record<string, unknown>> {
|
|
10
|
+
/**
|
|
11
|
+
* An identifier for the entry, which must be unique within the collection. This is used to
|
|
12
|
+
* look up the entry in the store and is the key used with `getEntry` for that collection.
|
|
13
|
+
*/
|
|
14
|
+
id: string;
|
|
15
|
+
/**
|
|
16
|
+
* The actual data for the entry. When a user accesses the collection, this will have TypeScript
|
|
17
|
+
* types generated according to the collection schema.
|
|
18
|
+
*
|
|
19
|
+
* It is the loader's responsibility to use `parseData` to validate and parse the data before
|
|
20
|
+
* storing it in the data store: no validation is done when getting or setting the data.
|
|
21
|
+
*/
|
|
22
|
+
data: D;
|
|
23
|
+
/**
|
|
24
|
+
* A path to the file that is the source of this entry, relative to the root of the site. This
|
|
25
|
+
* only applies to file-based loaders and is used to resolve paths such as images or other assets.
|
|
26
|
+
*
|
|
27
|
+
* If not set, then any fields in the schema that use the `image()` helper will be treated as
|
|
28
|
+
* public paths and not transformed.
|
|
29
|
+
*/
|
|
30
|
+
filePath?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The raw body of the entry, if applicable. If the entry includes rendered content, then this
|
|
33
|
+
* field can be used to store the raw source. This is optional and is not used internally.
|
|
34
|
+
*/
|
|
35
|
+
body?: string;
|
|
36
|
+
/**
|
|
37
|
+
* An optional content digest for the entry. This can be used to check if the data has changed.
|
|
38
|
+
*
|
|
39
|
+
* When setting an entry, the entry will only update if the digest does not match an existing
|
|
40
|
+
* entry with the same ID.
|
|
41
|
+
*
|
|
42
|
+
* The format of the digest is up to the loader, but it must be a string that changes when the
|
|
43
|
+
* data changes. This can be done with the `generateDigest` function.
|
|
44
|
+
*/
|
|
45
|
+
digest?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Stores an object with an entry's rendered content and metadata if it has been rendered to HTML.
|
|
48
|
+
* For example, this can be used to store the rendered content of a Markdown entry, or HTML from
|
|
49
|
+
* a CMS.
|
|
50
|
+
*
|
|
51
|
+
* If this field is provided, then the `render()` function and `<Content />` component are available
|
|
52
|
+
* to render the entry in a page.
|
|
53
|
+
*/
|
|
54
|
+
rendered?: RenderedContent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DataStore<D extends Record<string, unknown> = Record<string, unknown>> {
|
|
58
|
+
/**
|
|
59
|
+
* Get an entry from the store by its ID. Returns `undefined` if the entry does not exist.
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const existingEntry = store.get("my-entry");
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
get: (key: string) => DataEntry<D> | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Used after data has been validated and parsed to add an entry to the store, returning `true`
|
|
68
|
+
* if the entry was set. This returns `false` when the `digest` property determines that an
|
|
69
|
+
* entry has not changed and should not be updated.
|
|
70
|
+
*/
|
|
71
|
+
set: (entry: DataEntry) => boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Get all entries in the collection as an array of key-value pairs.
|
|
74
|
+
*/
|
|
75
|
+
entries: () => [id: string, DataEntry<D>][];
|
|
76
|
+
/**
|
|
77
|
+
* Get all the keys of the entries in the collection.
|
|
78
|
+
*/
|
|
79
|
+
keys: () => string[];
|
|
80
|
+
/**
|
|
81
|
+
* Get all entries in the collection as an array.
|
|
82
|
+
*/
|
|
83
|
+
values: () => DataEntry<D>[];
|
|
84
|
+
/**
|
|
85
|
+
* Delete an entry from the store by its ID.
|
|
86
|
+
*/
|
|
87
|
+
delete: (key: string) => void;
|
|
88
|
+
/**
|
|
89
|
+
* Clear all entries from the collection.
|
|
90
|
+
*/
|
|
91
|
+
clear: () => void;
|
|
92
|
+
/**
|
|
93
|
+
* Check if an entry exists in the store by its ID.
|
|
94
|
+
*/
|
|
95
|
+
has: (key: string) => boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface MetaStore {
|
|
99
|
+
get: (key: string) => string | undefined;
|
|
100
|
+
set: (key: string, value: string) => void;
|
|
101
|
+
delete: (key: string) => void;
|
|
102
|
+
has: (key: string) => boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ParseDataOptions<Data extends Record<string, unknown>> {
|
|
106
|
+
/**
|
|
107
|
+
* The ID of the entry. Unique per collection
|
|
108
|
+
*/
|
|
109
|
+
id: string;
|
|
110
|
+
/**
|
|
111
|
+
* The raw, unvalidated data of the entry
|
|
112
|
+
*/
|
|
113
|
+
data: Data;
|
|
114
|
+
/**
|
|
115
|
+
* An optional file path, where the entry represents a local file.
|
|
116
|
+
*/
|
|
117
|
+
filePath?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface MarkdownHeading {
|
|
121
|
+
depth: number;
|
|
122
|
+
slug: string;
|
|
123
|
+
text: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface RenderedContent {
|
|
127
|
+
/**
|
|
128
|
+
* Rendered HTML string. If present then `render(entry)` will return a React component
|
|
129
|
+
* that renders this HTML.
|
|
130
|
+
*/
|
|
131
|
+
html: string;
|
|
132
|
+
metadata?: {
|
|
133
|
+
/**
|
|
134
|
+
* Any images that are present in this entry. Relative to the DataEntry filePath.
|
|
135
|
+
*/
|
|
136
|
+
imagePaths?: string[];
|
|
137
|
+
/**
|
|
138
|
+
* Any headings that are present in this file. Returned as `headings` from `render()`
|
|
139
|
+
*/
|
|
140
|
+
headings?: MarkdownHeading[];
|
|
141
|
+
/**
|
|
142
|
+
* Raw frontmatter, parsed from the file. This may include data from remark plugins.
|
|
143
|
+
*/
|
|
144
|
+
frontmatter?: Record<string, any>;
|
|
145
|
+
/**
|
|
146
|
+
* Any other metadata that is present in this file.
|
|
147
|
+
*/
|
|
148
|
+
[key: string]: unknown;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface LoaderContext<D extends Record<string, unknown> = Record<string, unknown>> {
|
|
153
|
+
/**
|
|
154
|
+
* The unique name of the collection. This is the key in the collections object in
|
|
155
|
+
* the app/content.config.ts file.
|
|
156
|
+
*/
|
|
157
|
+
collection: string;
|
|
158
|
+
/**
|
|
159
|
+
* A database to store the actual data. Use this to update the store with new entries.
|
|
160
|
+
*/
|
|
161
|
+
store: DataStore<D>;
|
|
162
|
+
/**
|
|
163
|
+
* A key-value store scoped to the collection, designed for things like sync tokens and
|
|
164
|
+
* last-modified times. This metadata is persisted between builds alongside the collection
|
|
165
|
+
* data but is only available inside the loader.
|
|
166
|
+
*
|
|
167
|
+
* ```ts
|
|
168
|
+
* const lastModified = meta.get("lastModified");
|
|
169
|
+
* // ...
|
|
170
|
+
* meta.set("lastModified", new Date().toISOString());
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
meta: MetaStore;
|
|
174
|
+
/**
|
|
175
|
+
* Validates and parses the data according to the collection schema. Pass data to this function
|
|
176
|
+
* to validate and parse it before storing it in the data store.
|
|
177
|
+
*/
|
|
178
|
+
parseData: <Data extends Record<string, unknown>>(
|
|
179
|
+
props: ParseDataOptions<Data>,
|
|
180
|
+
) => Promise<Data>;
|
|
181
|
+
/** */
|
|
182
|
+
renderMarkdown: (markdown: string) => Promise<RenderedContent>;
|
|
183
|
+
/**
|
|
184
|
+
* Generates a non-cryptographic content digest of an object or string. This can be used
|
|
185
|
+
* to track if the data has changed by setting the `digest` field of an entry.
|
|
186
|
+
*/
|
|
187
|
+
generateDigest: (data: Record<string, unknown> | string) => string;
|
|
188
|
+
/**
|
|
189
|
+
* When running in dev mode, this is a filesystem watcher that can be used to trigger updates.
|
|
190
|
+
*/
|
|
191
|
+
watcher: FSWatcher;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ContentLoader {
|
|
195
|
+
/**
|
|
196
|
+
* A unique name for the loader, used in logs and for conditional loading.
|
|
197
|
+
*/
|
|
198
|
+
name: string;
|
|
199
|
+
/**
|
|
200
|
+
* An async function that is called at build time to load data and update the store.
|
|
201
|
+
*/
|
|
202
|
+
load: (context: LoaderContext) => Promise<void> | void;
|
|
203
|
+
/**
|
|
204
|
+
* An optional data schema that defines the shape of the entries.
|
|
205
|
+
* It is used to both validate the data and also to generate TypeScript types for the collection.
|
|
206
|
+
*
|
|
207
|
+
* If a function is provided, it will be called at build time before load() to generate the
|
|
208
|
+
* schema. You can use this to dynamically generate the schema based on the configuration
|
|
209
|
+
* options or by introspecting an API.
|
|
210
|
+
*/
|
|
211
|
+
schema:
|
|
212
|
+
| Schema<unknown>
|
|
213
|
+
| Promise<Schema<unknown>>
|
|
214
|
+
| (() => Schema<unknown> | Promise<Schema<unknown>>);
|
|
215
|
+
/**
|
|
216
|
+
* Returns file paths or directories that the loader watches for changes. Used by the content
|
|
217
|
+
* layer plugin to detect new, changed, or deleted content files during development.
|
|
218
|
+
*
|
|
219
|
+
* - File loaders should return the specific file path
|
|
220
|
+
* - Glob loaders should return the base directory
|
|
221
|
+
*/
|
|
222
|
+
getWatchedPaths?: () => string[];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The `context` object that `defineCollection` uses for the function shape of `schema`. This type
|
|
227
|
+
* can be useful when building reusable schemas for multiple collections.
|
|
228
|
+
*/
|
|
229
|
+
export interface SchemaContext {
|
|
230
|
+
/**
|
|
231
|
+
* The `image()` schema helper that allows you to validate local images in Content Collections
|
|
232
|
+
*/
|
|
233
|
+
image: () => Schema<unknown, string>;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface Collection {
|
|
237
|
+
loader: ContentLoader;
|
|
238
|
+
schema: Schema<unknown> | ((context: SchemaContext) => Schema<unknown>);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function defineCollection<
|
|
242
|
+
S extends Schema<any, any> | ((ctx: SchemaContext) => Schema<any, any>),
|
|
243
|
+
>(_input: { loader: ContentLoader; schema: S }): { loader: ContentLoader; schema: S } {
|
|
244
|
+
todo();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function reference(
|
|
248
|
+
_collection: string,
|
|
249
|
+
): Schema<string, { collection: ContentEntryMap[keyof ContentEntryMap]; id: string }> {
|
|
250
|
+
todo();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// TODO: This should be generated by the Vite plugin based on the schema types of each exported collection
|
|
254
|
+
export type ContentEntryMap = {};
|
|
255
|
+
|
|
256
|
+
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
|
257
|
+
|
|
258
|
+
export type CollectionKey = keyof ContentEntryMap;
|
|
259
|
+
export type CollectionEntry<C extends CollectionKey> = Flatten<ContentEntryMap[C]>;
|
|
260
|
+
|
|
261
|
+
export function getCollection<C extends keyof ContentEntryMap, E extends CollectionEntry<C>>(
|
|
262
|
+
collection: C,
|
|
263
|
+
filter?: (entry: CollectionEntry<C>) => entry is E,
|
|
264
|
+
): Promise<E[]>;
|
|
265
|
+
export function getCollection<C extends keyof ContentEntryMap>(
|
|
266
|
+
collection: C,
|
|
267
|
+
filter?: (entry: CollectionEntry<C>) => unknown,
|
|
268
|
+
): Promise<CollectionEntry<C>[]>;
|
|
269
|
+
export function getCollection<C extends keyof ContentEntryMap, E extends CollectionEntry<C>>(
|
|
270
|
+
_collection: C,
|
|
271
|
+
_filter?: (entry: CollectionEntry<C>) => unknown,
|
|
272
|
+
): Promise<E[]> {
|
|
273
|
+
todo();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export type ReferenceContentEntry<
|
|
277
|
+
C extends keyof ContentEntryMap,
|
|
278
|
+
E extends ContentEntryMap[C] | (string & {}) = string,
|
|
279
|
+
> = {
|
|
280
|
+
collection: C;
|
|
281
|
+
slug: E;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export function getEntry<C extends keyof ContentEntryMap, E extends ContentEntryMap[C]>(
|
|
285
|
+
collection: C,
|
|
286
|
+
slug: E,
|
|
287
|
+
): Promise<CollectionEntry<C>>;
|
|
288
|
+
export function getEntry<C extends keyof ContentEntryMap, E extends ContentEntryMap[C]>(
|
|
289
|
+
entry: ReferenceContentEntry<C, E>,
|
|
290
|
+
): Promise<CollectionEntry<C>>;
|
|
291
|
+
export function getEntry<C extends keyof ContentEntryMap, E extends ContentEntryMap[C]>(
|
|
292
|
+
_collection: C | ReferenceContentEntry<C, E>,
|
|
293
|
+
_slug?: E,
|
|
294
|
+
): Promise<CollectionEntry<C>> {
|
|
295
|
+
todo();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function getEntries<C extends keyof ContentEntryMap, E extends ContentEntryMap[C]>(
|
|
299
|
+
_entries: ReferenceContentEntry<C, E>[],
|
|
300
|
+
): Promise<CollectionEntry<C>[]> {
|
|
301
|
+
todo();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface RenderedEntry {
|
|
305
|
+
Content: ComponentType;
|
|
306
|
+
headings: MarkdownHeading[];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function render<C extends keyof ContentEntryMap>(
|
|
310
|
+
_entry: ContentEntryMap[C][string],
|
|
311
|
+
): Promise<RenderedEntry> {
|
|
312
|
+
todo();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface GenerateIdOptions {
|
|
316
|
+
/** The path to the entry file, relative to the base directory. */
|
|
317
|
+
entry: string;
|
|
318
|
+
/** The base directory URL. */
|
|
319
|
+
base: URL;
|
|
320
|
+
/** The parsed, unvalidated data of the entry. */
|
|
321
|
+
data: Record<string, unknown>;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
interface GlobOptions {
|
|
325
|
+
/** The glob pattern to match files, relative to the base directory */
|
|
326
|
+
pattern: string | string[];
|
|
327
|
+
/** The base directory to resolve the glob pattern from. Relative to the root directory, or an absolute file URL. Defaults to `.` */
|
|
328
|
+
base?: string | URL;
|
|
329
|
+
/**
|
|
330
|
+
* Function that generates an ID for an entry. Default implementation generates a slug from the entry path.
|
|
331
|
+
* @returns The ID of the entry. Must be unique per collection.
|
|
332
|
+
**/
|
|
333
|
+
generateId?: (options: GenerateIdOptions) => string;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function glob(_globOptions: GlobOptions): ContentLoader {
|
|
337
|
+
todo();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
interface FileOptions {
|
|
341
|
+
/**
|
|
342
|
+
* The parsing function to use for this data
|
|
343
|
+
* @default jsr:@std/jsonc.parse or jsr:@std/yaml.parse, depending on the extension of the file
|
|
344
|
+
* */
|
|
345
|
+
parser?: (text: string) => Record<string, Record<string, unknown>> | Record<string, unknown>[];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function file(_fileName: string, _options?: FileOptions): ContentLoader {
|
|
349
|
+
todo();
|
|
350
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Schema } from "@remix-run/data-schema";
|
|
2
|
+
|
|
3
|
+
interface CollectionInfo {
|
|
4
|
+
schema: Schema<unknown>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function generateTypes(
|
|
8
|
+
collections: Record<string, CollectionInfo>,
|
|
9
|
+
configPath: string,
|
|
10
|
+
): string {
|
|
11
|
+
const collectionEntries = Object.entries(collections);
|
|
12
|
+
|
|
13
|
+
const entryMapEntries = collectionEntries
|
|
14
|
+
.map(([name]) => {
|
|
15
|
+
return ` ${name}: {
|
|
16
|
+
[id: string]: import("${CONTENT_LAYER_API_PATH}").DataEntry<
|
|
17
|
+
import("@remix-run/data-schema").InferOutput<
|
|
18
|
+
_ResolveSchema<_Collections["${name}"]["schema"]>
|
|
19
|
+
>
|
|
20
|
+
> & {
|
|
21
|
+
collection: "${name}";
|
|
22
|
+
};
|
|
23
|
+
};`;
|
|
24
|
+
})
|
|
25
|
+
.join("\n");
|
|
26
|
+
|
|
27
|
+
return `// Auto-generated by content-layer
|
|
28
|
+
// Do not edit this file manually
|
|
29
|
+
|
|
30
|
+
declare module "sprinkles:content" {
|
|
31
|
+
type _Collections = typeof import("${configPath}").collections;
|
|
32
|
+
type _ResolveSchema<T> = T extends (...args: any[]) => infer R ? R : T;
|
|
33
|
+
|
|
34
|
+
interface ContentEntryMap {
|
|
35
|
+
${entryMapEntries}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
|
39
|
+
type CollectionKey = keyof ContentEntryMap;
|
|
40
|
+
type CollectionEntry<C extends CollectionKey> = Flatten<ContentEntryMap[C]>;
|
|
41
|
+
|
|
42
|
+
type ReferenceContentEntry<
|
|
43
|
+
C extends keyof ContentEntryMap,
|
|
44
|
+
E extends ContentEntryMap[C] | (string & {}) = string,
|
|
45
|
+
> = {
|
|
46
|
+
collection: C;
|
|
47
|
+
id: E;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function getCollection<C extends CollectionKey>(
|
|
51
|
+
collection: C,
|
|
52
|
+
filter?: (entry: CollectionEntry<C>) => unknown,
|
|
53
|
+
): Promise<CollectionEntry<C>[]>;
|
|
54
|
+
|
|
55
|
+
export function getEntry<C extends CollectionKey>(
|
|
56
|
+
collection: C,
|
|
57
|
+
slug: string,
|
|
58
|
+
): Promise<CollectionEntry<C> | undefined>;
|
|
59
|
+
|
|
60
|
+
export function getEntry<C extends CollectionKey>(
|
|
61
|
+
entry: ReferenceContentEntry<C>,
|
|
62
|
+
): Promise<CollectionEntry<C> | undefined>;
|
|
63
|
+
|
|
64
|
+
export function getEntries<C extends CollectionKey>(
|
|
65
|
+
entries: ReferenceContentEntry<C>[],
|
|
66
|
+
): Promise<CollectionEntry<C>[]>;
|
|
67
|
+
|
|
68
|
+
export function render<C extends CollectionKey>(
|
|
69
|
+
entry: CollectionEntry<C>,
|
|
70
|
+
): Promise<import("${CONTENT_LAYER_API_PATH}").RenderedEntry>;
|
|
71
|
+
|
|
72
|
+
export function defineCollection<
|
|
73
|
+
S extends
|
|
74
|
+
| import("@remix-run/data-schema").Schema<any, any>
|
|
75
|
+
| ((
|
|
76
|
+
ctx: import("${CONTENT_LAYER_API_PATH}").SchemaContext,
|
|
77
|
+
) => import("@remix-run/data-schema").Schema<any, any>),
|
|
78
|
+
>(
|
|
79
|
+
input: { loader: import("${CONTENT_LAYER_API_PATH}").ContentLoader; schema: S },
|
|
80
|
+
): { loader: import("${CONTENT_LAYER_API_PATH}").ContentLoader; schema: S };
|
|
81
|
+
|
|
82
|
+
export function reference(
|
|
83
|
+
collection: string,
|
|
84
|
+
): import("@remix-run/data-schema").Schema<string, { collection: string; id: string }>;
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const CONTENT_LAYER_API_PATH = "../../content-layer/api.ts";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Schema } from "@remix-run/data-schema";
|
|
2
|
+
|
|
3
|
+
import { createSchema, fail } from "@remix-run/data-schema";
|
|
4
|
+
|
|
5
|
+
import type { ContentLoader, SchemaContext } from "./api.ts";
|
|
6
|
+
|
|
7
|
+
export function defineCollection<
|
|
8
|
+
S extends Schema<any, any> | ((ctx: SchemaContext) => Schema<any, any>),
|
|
9
|
+
>(input: { loader: ContentLoader; schema: S }): { loader: ContentLoader; schema: S } {
|
|
10
|
+
return input;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function reference(collection: string): Schema<string, { collection: string; id: string }> {
|
|
14
|
+
return createSchema((value, context) => {
|
|
15
|
+
if (typeof value !== "string") {
|
|
16
|
+
return fail("Expected a string reference ID", context.path);
|
|
17
|
+
}
|
|
18
|
+
return { value: { collection, id: value } };
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { parse as parseYaml } from "@std/yaml";
|
|
2
|
+
|
|
3
|
+
export interface FrontmatterResult {
|
|
4
|
+
data: Record<string, unknown>;
|
|
5
|
+
body: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
9
|
+
|
|
10
|
+
export function parseFrontmatter(content: string): FrontmatterResult {
|
|
11
|
+
const match = FRONTMATTER_RE.exec(content);
|
|
12
|
+
if (!match) {
|
|
13
|
+
return { body: content, data: {} };
|
|
14
|
+
}
|
|
15
|
+
const raw = match[1];
|
|
16
|
+
const body = content.slice(match[0].length).replace(/^\r?\n/, "");
|
|
17
|
+
const data = parseYaml(raw) as Record<string, unknown>;
|
|
18
|
+
return { body, data };
|
|
19
|
+
}
|