@eclipsa/content 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/internal.ts ADDED
@@ -0,0 +1,514 @@
1
+ import fg from 'fast-glob'
2
+ import * as fs from 'node:fs/promises'
3
+ import { createRequire } from 'node:module'
4
+ import path from 'node:path'
5
+ import YAML from 'yaml'
6
+ import type { StandardSchemaIssue, StandardSchemaV1 } from 'eclipsa'
7
+ import { highlightHtml } from './highlight.ts'
8
+ import { buildContentSearchIndex, resolveContentSearchOptions } from './search.ts'
9
+ import type { CollectionEntry, ContentFilter, ContentRuntimeModule } from './mod.ts'
10
+ import { CONTENT_COLLECTION_MARKER } from './types.ts'
11
+ import type {
12
+ AnyCollection,
13
+ BaseContentEntry,
14
+ ContentMarkdownOptions,
15
+ ContentComponentProps,
16
+ ContentEntryReference,
17
+ ContentHeading,
18
+ ContentLoader,
19
+ ContentLoaderContext,
20
+ ContentLoaderObject,
21
+ ContentSearchDocument,
22
+ ContentSearchIndex,
23
+ ResolvedContentSearchOptions,
24
+ ContentSourceEntry,
25
+ DefinedCollection,
26
+ GlobLoader,
27
+ RenderedContent,
28
+ ResolvedContentEntries,
29
+ } from './types.ts'
30
+
31
+ interface ParsedFrontmatter {
32
+ body: string
33
+ data: Record<string, unknown>
34
+ }
35
+
36
+ interface ResolvedManifest {
37
+ collections: Map<AnyCollection, BaseContentEntry[]>
38
+ markdownByCollectionName: Map<string, ContentMarkdownOptions | undefined>
39
+ searchByCollectionName: Map<string, ResolvedContentSearchOptions>
40
+ entriesByCollection: Map<AnyCollection, Map<string, BaseContentEntry>>
41
+ }
42
+
43
+ interface CreateContentRuntimeOptions {
44
+ collectionsModule: Record<string, unknown>
45
+ configPath: string
46
+ root: string
47
+ }
48
+
49
+ const MARKDOWN_EXTENSION_RE = /\.md$/i
50
+ const require = createRequire(import.meta.url)
51
+ let markdownTransform: typeof import('@ox-content/napi').transform | null = null
52
+
53
+ export class ContentCollectionError extends Error {
54
+ constructor(message: string) {
55
+ super(message)
56
+ this.name = 'ContentCollectionError'
57
+ }
58
+ }
59
+
60
+ const normalizeSlashes = (value: string) => value.replaceAll('\\', '/')
61
+
62
+ const formatIssuePath = (pathValue: StandardSchemaIssue['path']) => {
63
+ if (!pathValue || pathValue.length === 0) {
64
+ return ''
65
+ }
66
+ return pathValue
67
+ .map((segment) =>
68
+ typeof segment === 'object' && segment !== null && 'key' in segment
69
+ ? String(segment.key)
70
+ : String(segment),
71
+ )
72
+ .join('.')
73
+ }
74
+
75
+ const createSchemaError = (
76
+ collection: string,
77
+ filePath: string,
78
+ issues: readonly StandardSchemaIssue[],
79
+ ) => {
80
+ const detail = issues
81
+ .map((issue) => {
82
+ const issuePath = formatIssuePath(issue.path)
83
+ return issuePath === '' ? issue.message : `${issuePath}: ${issue.message}`
84
+ })
85
+ .join('; ')
86
+ return new ContentCollectionError(
87
+ `Invalid frontmatter in collection "${collection}" for ${filePath}: ${detail}`,
88
+ )
89
+ }
90
+
91
+ const parseFrontmatter = (source: string): ParsedFrontmatter => {
92
+ if (!source.startsWith('---')) {
93
+ return {
94
+ body: source,
95
+ data: {},
96
+ }
97
+ }
98
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u.exec(source)
99
+ if (!match) {
100
+ return {
101
+ body: source,
102
+ data: {},
103
+ }
104
+ }
105
+ const raw = YAML.parse(match[1] ?? '')
106
+ if (raw == null) {
107
+ return {
108
+ body: source.slice(match[0].length),
109
+ data: {},
110
+ }
111
+ }
112
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
113
+ throw new ContentCollectionError('Markdown frontmatter must resolve to an object.')
114
+ }
115
+ return {
116
+ body: source.slice(match[0].length),
117
+ data: { ...(raw as Record<string, unknown>) },
118
+ }
119
+ }
120
+
121
+ const normalizeIdSegment = (segment: string) =>
122
+ segment
123
+ .trim()
124
+ .replaceAll(/\s+/g, '-')
125
+ .replaceAll(/[^a-zA-Z0-9/_-]+/g, '-')
126
+ .replaceAll(/-+/g, '-')
127
+ .replaceAll(/^[-/]+|[-/]+$/g, '')
128
+
129
+ const normalizeEntryId = (value: string) =>
130
+ normalizeSlashes(value).split('/').map(normalizeIdSegment).filter(Boolean).join('/')
131
+
132
+ const toEntryIdFromRelativePath = (relativePath: string) => {
133
+ const withoutExt = normalizeSlashes(relativePath).replace(MARKDOWN_EXTENSION_RE, '')
134
+ const segments = withoutExt.split('/').filter(Boolean)
135
+ if (segments[segments.length - 1] === 'index' && segments.length > 1) {
136
+ segments.pop()
137
+ }
138
+ return normalizeEntryId(segments.join('/')) || 'index'
139
+ }
140
+
141
+ const validateData = async (
142
+ collection: string,
143
+ schema: StandardSchemaV1<any, any> | undefined,
144
+ filePath: string,
145
+ data: Record<string, unknown>,
146
+ ) => {
147
+ if (!schema) {
148
+ return data
149
+ }
150
+ const result = await schema['~standard'].validate(data)
151
+ if ('issues' in result && result.issues !== undefined) {
152
+ throw createSchemaError(collection, filePath, result.issues)
153
+ }
154
+ return result.value as Record<string, unknown>
155
+ }
156
+
157
+ const resolveGlobLoaderEntries = async (
158
+ collection: string,
159
+ loader: GlobLoader,
160
+ context: ContentLoaderContext,
161
+ ): Promise<ContentSourceEntry[]> => {
162
+ const baseDir = path.resolve(path.dirname(context.configPath), loader.base)
163
+ const matches = await fg(loader.pattern, {
164
+ absolute: true,
165
+ cwd: baseDir,
166
+ onlyFiles: true,
167
+ })
168
+ return Promise.all(
169
+ matches.map(async (filePath) => {
170
+ const source = await fs.readFile(filePath, 'utf8')
171
+ const relativePath = normalizeSlashes(path.relative(baseDir, filePath))
172
+ const parsed = parseFrontmatter(source)
173
+ const slug = typeof parsed.data.slug === 'string' ? parsed.data.slug : undefined
174
+ delete parsed.data.slug
175
+ return {
176
+ body: parsed.body,
177
+ data: parsed.data,
178
+ filePath,
179
+ id: slug ? normalizeEntryId(slug) : toEntryIdFromRelativePath(relativePath),
180
+ } satisfies ContentSourceEntry
181
+ }),
182
+ )
183
+ }
184
+
185
+ const resolveLoaderEntries = async (
186
+ collection: string,
187
+ loader: ContentLoader,
188
+ context: ContentLoaderContext,
189
+ ) => {
190
+ if ((loader as GlobLoader).kind === 'glob') {
191
+ return resolveGlobLoaderEntries(collection, loader as GlobLoader, context)
192
+ }
193
+ return [...(await (loader as ContentLoaderObject).load(context))]
194
+ }
195
+
196
+ const normalizeResolvedEntry = async (
197
+ collection: string,
198
+ schema: StandardSchemaV1<any, any> | undefined,
199
+ entry: ContentSourceEntry,
200
+ index: number,
201
+ ) => {
202
+ const parsed =
203
+ entry.data === undefined
204
+ ? parseFrontmatter(entry.body)
205
+ : {
206
+ body: entry.body,
207
+ data: { ...entry.data },
208
+ }
209
+ const filePath = entry.filePath ?? `${collection}:${entry.id ?? index}`
210
+ const slug = typeof parsed.data.slug === 'string' ? parsed.data.slug : undefined
211
+ delete parsed.data.slug
212
+ const id =
213
+ normalizeEntryId(entry.id ?? slug ?? `${collection}-${index}`) || `${collection}-${index}`
214
+ return {
215
+ body: parsed.body,
216
+ collection,
217
+ data: await validateData(collection, schema, filePath, parsed.data),
218
+ filePath,
219
+ id,
220
+ } satisfies BaseContentEntry
221
+ }
222
+
223
+ const isDefinedCollection = (value: unknown): value is DefinedCollection<any> =>
224
+ typeof value === 'object' &&
225
+ value !== null &&
226
+ CONTENT_COLLECTION_MARKER in value &&
227
+ (value as Record<string, unknown>)[CONTENT_COLLECTION_MARKER] === true
228
+
229
+ export const resolveCollections = async ({
230
+ collectionsModule,
231
+ configPath,
232
+ root,
233
+ }: CreateContentRuntimeOptions): Promise<ResolvedManifest> => {
234
+ const byCollection = new Map<AnyCollection, BaseContentEntry[]>()
235
+ const entriesByCollection = new Map<AnyCollection, Map<string, BaseContentEntry>>()
236
+ const markdownByCollectionName = new Map<string, ContentMarkdownOptions | undefined>()
237
+ const searchByCollectionName = new Map<string, ResolvedContentSearchOptions>()
238
+ const definedCollections = Object.entries(collectionsModule).filter(
239
+ (entry): entry is [string, DefinedCollection<any>] => isDefinedCollection(entry[1]),
240
+ )
241
+ for (const [collectionName, definition] of definedCollections) {
242
+ const context: ContentLoaderContext = {
243
+ collection: collectionName,
244
+ configPath,
245
+ root,
246
+ }
247
+ const rawEntries = await resolveLoaderEntries(collectionName, definition.loader, context)
248
+ const resolvedEntries = await Promise.all(
249
+ rawEntries.map((entry, index) =>
250
+ normalizeResolvedEntry(collectionName, definition.schema, entry, index),
251
+ ),
252
+ )
253
+ resolvedEntries.sort((left, right) => left.id.localeCompare(right.id))
254
+ const entriesById = new Map<string, BaseContentEntry>()
255
+ for (const entry of resolvedEntries) {
256
+ if (entriesById.has(entry.id)) {
257
+ throw new ContentCollectionError(
258
+ `Duplicate content id "${entry.id}" in collection "${collectionName}".`,
259
+ )
260
+ }
261
+ entriesById.set(entry.id, entry)
262
+ }
263
+ byCollection.set(definition, resolvedEntries)
264
+ entriesByCollection.set(definition, entriesById)
265
+ markdownByCollectionName.set(collectionName, definition.markdown)
266
+ searchByCollectionName.set(collectionName, resolveContentSearchOptions(definition.search))
267
+ }
268
+ return {
269
+ collections: byCollection,
270
+ markdownByCollectionName,
271
+ searchByCollectionName,
272
+ entriesByCollection,
273
+ }
274
+ }
275
+
276
+ const createContentRenderer =
277
+ (html: string) =>
278
+ (props: Omit<ContentComponentProps, 'html'> = {}) => ({
279
+ isStatic: false,
280
+ props: {
281
+ ...props,
282
+ dangerouslySetInnerHTML: html,
283
+ },
284
+ type: props.as ?? 'article',
285
+ })
286
+
287
+ const resolveOxContentNapiPath = () => {
288
+ const resolvePaths = [
289
+ process.cwd(),
290
+ path.join(process.cwd(), 'node_modules', '@eclipsa', 'content'),
291
+ ]
292
+ try {
293
+ return require.resolve('@ox-content/napi', { paths: resolvePaths })
294
+ } catch {
295
+ return '@ox-content/napi'
296
+ }
297
+ }
298
+
299
+ const loadMarkdownTransform = async () => {
300
+ markdownTransform ??= (require(resolveOxContentNapiPath()) as typeof import('@ox-content/napi'))
301
+ .transform
302
+ return markdownTransform
303
+ }
304
+
305
+ const decodeHtmlEntities = (value: string) =>
306
+ value
307
+ .replaceAll('&amp;', '&')
308
+ .replaceAll('&lt;', '<')
309
+ .replaceAll('&gt;', '>')
310
+ .replaceAll('&quot;', '"')
311
+ .replaceAll('&#39;', "'")
312
+ .replaceAll('&nbsp;', ' ')
313
+
314
+ const stripHtml = (html: string) =>
315
+ decodeHtmlEntities(
316
+ html
317
+ .replaceAll(/<style[\s\S]*?<\/style>/gu, ' ')
318
+ .replaceAll(/<script[\s\S]*?<\/script>/gu, ' ')
319
+ .replaceAll(/<[^>]+>/gu, ' ')
320
+ .replaceAll(/\s+/gu, ' ')
321
+ .trim(),
322
+ )
323
+
324
+ const extractMarkdownCode = (source: string) => {
325
+ const codeBlocks = new Set<string>()
326
+ for (const match of source.matchAll(/```[\t ]*[^\n\r]*\r?\n([\s\S]*?)```/gu)) {
327
+ const code = match[1]?.trim()
328
+ if (code) {
329
+ codeBlocks.add(code)
330
+ }
331
+ }
332
+ for (const match of source.matchAll(/`([^`\n\r]+)`/gu)) {
333
+ const code = match[1]?.trim()
334
+ if (code) {
335
+ codeBlocks.add(code)
336
+ }
337
+ }
338
+ return [...codeBlocks]
339
+ }
340
+
341
+ const resolveSearchUrl = (base: string, entry: BaseContentEntry) => {
342
+ const normalizedBase = base === '' ? '/' : base.endsWith('/') ? base : `${base}/`
343
+ return `${normalizedBase}${entry.collection}/${entry.id}`.replaceAll(/\/+/g, '/')
344
+ }
345
+
346
+ const transformMarkdownEntry = async (entry: BaseContentEntry) => {
347
+ const transform = await loadMarkdownTransform()
348
+ const result = transform(entry.body, {
349
+ autolinks: true,
350
+ footnotes: true,
351
+ gfm: true,
352
+ sourcePath: entry.filePath,
353
+ strikethrough: true,
354
+ tables: true,
355
+ taskLists: true,
356
+ tocMaxDepth: 6,
357
+ })
358
+ if (result.errors.length > 0) {
359
+ throw new ContentCollectionError(
360
+ `Failed to render markdown for ${entry.filePath}: ${result.errors.join('; ')}`,
361
+ )
362
+ }
363
+ return result
364
+ }
365
+
366
+ const createSearchDocument = async (
367
+ entry: BaseContentEntry,
368
+ base: string,
369
+ ): Promise<ContentSearchDocument> => {
370
+ const result = await transformMarkdownEntry(entry)
371
+ const headings = result.toc.map((heading) => heading.text)
372
+ const title =
373
+ typeof entry.data.title === 'string'
374
+ ? entry.data.title
375
+ : (result.toc.find((heading) => heading.depth === 1)?.text ?? entry.id)
376
+
377
+ return {
378
+ body: stripHtml(result.html),
379
+ code: extractMarkdownCode(entry.body),
380
+ collection: entry.collection,
381
+ headings,
382
+ id: entry.id,
383
+ title,
384
+ url: resolveSearchUrl(base, entry),
385
+ }
386
+ }
387
+
388
+ const renderMarkdown = async (
389
+ entry: BaseContentEntry,
390
+ markdownOptions: ContentMarkdownOptions | undefined,
391
+ ): Promise<RenderedContent> => {
392
+ const result = await transformMarkdownEntry(entry)
393
+ const headings = result.toc.map(
394
+ (heading) =>
395
+ ({
396
+ depth: heading.depth,
397
+ slug: heading.slug,
398
+ text: heading.text,
399
+ }) satisfies ContentHeading,
400
+ )
401
+ const html = await highlightHtml(result.html, markdownOptions?.highlight)
402
+ return {
403
+ Content: createContentRenderer(html),
404
+ headings,
405
+ html,
406
+ }
407
+ }
408
+
409
+ export const createContentSearch = async ({
410
+ collectionsModule,
411
+ configPath,
412
+ root,
413
+ base,
414
+ }: CreateContentRuntimeOptions & {
415
+ base: string
416
+ }): Promise<{
417
+ index: ContentSearchIndex
418
+ options: ResolvedContentSearchOptions
419
+ }> => {
420
+ const manifest = await resolveCollections({
421
+ collectionsModule,
422
+ configPath,
423
+ root,
424
+ })
425
+ const documents: ContentSearchDocument[] = []
426
+ let resolvedOptions = resolveContentSearchOptions(false)
427
+
428
+ for (const entries of manifest.collections.values()) {
429
+ const collectionName = entries[0]?.collection
430
+ if (!collectionName) {
431
+ continue
432
+ }
433
+ const searchOptions = manifest.searchByCollectionName.get(collectionName)
434
+ if (!searchOptions?.enabled) {
435
+ continue
436
+ }
437
+ if (!resolvedOptions.enabled) {
438
+ resolvedOptions = searchOptions
439
+ }
440
+ for (const entry of entries) {
441
+ documents.push(await createSearchDocument(entry, base))
442
+ }
443
+ }
444
+
445
+ return {
446
+ index: buildContentSearchIndex(documents, resolvedOptions),
447
+ options: resolvedOptions,
448
+ }
449
+ }
450
+
451
+ export const createContentRuntime = ({
452
+ collectionsModule,
453
+ configPath,
454
+ root,
455
+ }: CreateContentRuntimeOptions): ContentRuntimeModule => {
456
+ let manifestPromise: Promise<ResolvedManifest> | null = null
457
+ const renderCache = new Map<string, RenderedContent>()
458
+ const getManifest = () => {
459
+ manifestPromise ??= resolveCollections({
460
+ collectionsModule,
461
+ configPath,
462
+ root,
463
+ })
464
+ return manifestPromise
465
+ }
466
+ return {
467
+ async getCollection<Collection extends AnyCollection>(
468
+ collection: Collection,
469
+ filter?: ContentFilter<Collection>,
470
+ ) {
471
+ const manifest = await getManifest()
472
+ const entries = (manifest.collections.get(collection) ?? []) as CollectionEntry<Collection>[]
473
+ if (!filter) {
474
+ return [...entries]
475
+ }
476
+ const filtered: CollectionEntry<Collection>[] = []
477
+ for (const entry of entries) {
478
+ if (await filter(entry)) {
479
+ filtered.push(entry)
480
+ }
481
+ }
482
+ return filtered
483
+ },
484
+ async getEntries<Entries extends readonly ContentEntryReference<any>[]>(entries: Entries) {
485
+ const manifest = await getManifest()
486
+ return entries.map((entry) => {
487
+ const collectionEntries = manifest.entriesByCollection.get(entry.collection)
488
+ return collectionEntries?.get(entry.id)
489
+ }) as ResolvedContentEntries<Entries>
490
+ },
491
+ async getEntry<Collection extends AnyCollection>(collection: Collection, id: string) {
492
+ const manifest = await getManifest()
493
+ return manifest.entriesByCollection.get(collection)?.get(id) as
494
+ | CollectionEntry<Collection>
495
+ | undefined
496
+ },
497
+ async render<Collection extends AnyCollection>(entry: CollectionEntry<Collection>) {
498
+ const key = `${entry.collection}:${entry.id}`
499
+ const cached = renderCache.get(key)
500
+ if (cached) {
501
+ return cached
502
+ }
503
+ const manifest = await getManifest()
504
+ const rendered = await renderMarkdown(
505
+ entry,
506
+ manifest.markdownByCollectionName.get(entry.collection),
507
+ )
508
+ renderCache.set(key, rendered)
509
+ return rendered
510
+ },
511
+ }
512
+ }
513
+
514
+ export { parseFrontmatter, toEntryIdFromRelativePath }
package/mod.ts ADDED
@@ -0,0 +1,124 @@
1
+ import type { StandardSchemaV1 } from 'eclipsa'
2
+ import { CONTENT_COLLECTION_MARKER } from './types.ts'
3
+ import type {
4
+ AnyCollection,
5
+ CollectionEntry,
6
+ ContentCollectionDefinition,
7
+ ContentComponentProps,
8
+ ContentFilter,
9
+ ContentHighlightOptions,
10
+ ContentMarkdownOptions,
11
+ ContentEntryReference,
12
+ DefinedCollection,
13
+ GlobLoader,
14
+ GlobLoaderOptions,
15
+ ResolvedContentEntries,
16
+ RenderedContent,
17
+ } from './types.ts'
18
+
19
+ const ensureServerOnly = () => {
20
+ if (typeof window !== 'undefined') {
21
+ throw new Error('@eclipsa/content query APIs are server-only.')
22
+ }
23
+ }
24
+
25
+ const loadRuntime = async (): Promise<ContentRuntimeModule> => {
26
+ ensureServerOnly()
27
+ return import('virtual:eclipsa-content:runtime')
28
+ }
29
+
30
+ export interface ContentRuntimeModule {
31
+ getCollection<Collection extends AnyCollection>(
32
+ collection: Collection,
33
+ filter?: ContentFilter<Collection>,
34
+ ): Promise<CollectionEntry<Collection>[]>
35
+ getEntries<Entries extends readonly ContentEntryReference<any>[]>(
36
+ entries: Entries,
37
+ ): Promise<ResolvedContentEntries<Entries>>
38
+ getEntry<Collection extends AnyCollection>(
39
+ collection: Collection,
40
+ id: string,
41
+ ): Promise<CollectionEntry<Collection> | undefined>
42
+ render<Collection extends AnyCollection>(
43
+ entry: CollectionEntry<Collection>,
44
+ ): Promise<RenderedContent>
45
+ }
46
+
47
+ export type {
48
+ AnyCollection,
49
+ BaseContentEntry,
50
+ CollectionEntry,
51
+ ContentCollectionDefinition,
52
+ ContentComponentProps,
53
+ ContentFilter,
54
+ ContentHighlightOptions,
55
+ ContentMarkdownOptions,
56
+ ContentEntryReference,
57
+ ContentHeading,
58
+ ContentLoader,
59
+ ContentLoaderContext,
60
+ ContentLoaderObject,
61
+ ContentSearchDocument,
62
+ ContentSearchField,
63
+ ContentSearchIndex,
64
+ ContentSearchOptions,
65
+ ContentSearchPosting,
66
+ ContentSearchQueryOptions,
67
+ ContentSearchResult,
68
+ ContentSourceEntry,
69
+ DefinedCollection,
70
+ GlobLoader,
71
+ GlobLoaderOptions,
72
+ InferCollectionData,
73
+ ResolvedContentSearchOptions,
74
+ RenderedContent,
75
+ ResolvedContentEntries,
76
+ } from './types.ts'
77
+
78
+ export type { StandardSchemaV1 } from 'eclipsa'
79
+
80
+ export const defineCollection = <
81
+ Schema extends StandardSchemaV1<any, any> | undefined = StandardSchemaV1<any, any> | undefined,
82
+ >(
83
+ definition: ContentCollectionDefinition<Schema>,
84
+ ): DefinedCollection<Schema> =>
85
+ ({
86
+ ...definition,
87
+ [CONTENT_COLLECTION_MARKER]: true,
88
+ }) as DefinedCollection<Schema>
89
+
90
+ export const glob = (options: GlobLoaderOptions): GlobLoader => ({
91
+ base: options.base,
92
+ kind: 'glob',
93
+ pattern: options.pattern,
94
+ })
95
+
96
+ export const Content = ({ as = 'article', html, ...props }: ContentComponentProps) => ({
97
+ isStatic: false,
98
+ props: {
99
+ ...props,
100
+ dangerouslySetInnerHTML: html,
101
+ },
102
+ type: as,
103
+ })
104
+
105
+ export const getCollection = async <Collection extends AnyCollection>(
106
+ collection: Collection,
107
+ filter?: ContentFilter<Collection>,
108
+ ): Promise<CollectionEntry<Collection>[]> =>
109
+ (await loadRuntime()).getCollection(collection, filter) as Promise<CollectionEntry<Collection>[]>
110
+
111
+ export const getEntry = async <Collection extends AnyCollection>(
112
+ collection: Collection,
113
+ id: string,
114
+ ): Promise<CollectionEntry<Collection> | undefined> =>
115
+ (await loadRuntime()).getEntry(collection, id) as Promise<CollectionEntry<Collection> | undefined>
116
+
117
+ export const getEntries = async <Entries extends readonly ContentEntryReference<any>[]>(
118
+ entries: Entries,
119
+ ): Promise<ResolvedContentEntries<Entries>> =>
120
+ (await loadRuntime()).getEntries(entries) as Promise<ResolvedContentEntries<Entries>>
121
+
122
+ export const render = async <Collection extends AnyCollection>(
123
+ entry: CollectionEntry<Collection>,
124
+ ): Promise<RenderedContent> => (await loadRuntime()).render(entry)
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@eclipsa/content",
3
+ "private": false,
4
+ "version": "0.0.0",
5
+ "homepage": "https://github.com/pnsk-lab/eclipsa",
6
+ "bugs": {
7
+ "url": "https://github.com/pnsk-lab/eclipsa/issues"
8
+ },
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/pnsk-lab/eclipsa.git",
13
+ "directory": "packages/content"
14
+ },
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./mod.ts",
19
+ "import": "./mod.ts"
20
+ },
21
+ "./vite": {
22
+ "types": "./vite.ts",
23
+ "import": "./vite.ts"
24
+ },
25
+ "./internal": {
26
+ "types": "./internal.ts",
27
+ "import": "./internal.ts"
28
+ }
29
+ },
30
+ "publishConfig": {
31
+ "exports": {
32
+ ".": {
33
+ "types": "./mod.d.mts",
34
+ "import": "./mod.mjs"
35
+ },
36
+ "./vite": {
37
+ "types": "./vite.d.mts",
38
+ "import": "./vite.mjs"
39
+ },
40
+ "./internal": {
41
+ "types": "./internal.d.mts",
42
+ "import": "./internal.mjs"
43
+ }
44
+ }
45
+ },
46
+ "scripts": {
47
+ "build": "vp pack && bun ../../scripts/release/write-dist-package-json.ts",
48
+ "pack": "vp pack && bun ../../scripts/release/write-dist-package-json.ts",
49
+ "test": "vp test --run",
50
+ "typecheck": "bun x tsc -p ../../tsconfig.json --noEmit"
51
+ },
52
+ "dependencies": {
53
+ "@ox-content/napi": "^0.17.0",
54
+ "eclipsa": "0.2.0-alpha.0",
55
+ "fast-glob": "^3.3.2",
56
+ "shiki": "^4.0.2",
57
+ "yaml": "^2.8.1"
58
+ },
59
+ "peerDependencies": {
60
+ "vite": "*"
61
+ }
62
+ }