@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/.turbo/turbo-build.log +34 -0
- package/.turbo/turbo-test.log +13 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/dist/internal-h0upzIHm.mjs +644 -0
- package/dist/internal-h0upzIHm.mjs.map +1 -0
- package/dist/internal.d.mts +47 -0
- package/dist/internal.mjs +2 -0
- package/dist/mod-P8gKoDsz.d.mts +151 -0
- package/dist/mod.d.mts +2 -0
- package/dist/mod.mjs +34 -0
- package/dist/mod.mjs.map +1 -0
- package/dist/package.json +40 -0
- package/dist/types-rZ-wc23p.mjs +6 -0
- package/dist/types-rZ-wc23p.mjs.map +1 -0
- package/dist/virtual-runtime.d.ts +24 -0
- package/dist/vite.d.mts +7 -0
- package/dist/vite.mjs +195 -0
- package/dist/vite.mjs.map +1 -0
- package/highlight.ts +125 -0
- package/internal.test.ts +263 -0
- package/internal.ts +514 -0
- package/mod.ts +124 -0
- package/package.json +62 -0
- package/search.test.ts +56 -0
- package/search.ts +450 -0
- package/typecheck.ts +103 -0
- package/types.ts +172 -0
- package/virtual-runtime.d.ts +24 -0
- package/vite-config.test.ts +15 -0
- package/vite.config.ts +16 -0
- package/vite.test.ts +283 -0
- package/vite.ts +276 -0
package/vite.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import type { Plugin, PluginOption, ResolvedConfig, ViteDevServer } from 'vite'
|
|
5
|
+
import { createContentSearch } from './internal.ts'
|
|
6
|
+
import { generateContentSearchRuntimeModule, resolveContentSearchOptions } from './search.ts'
|
|
7
|
+
import type { ResolvedContentSearchOptions } from './types.ts'
|
|
8
|
+
|
|
9
|
+
const DEV_APP_INVALIDATORS_KEY = Symbol.for('eclipsa.dev-app-invalidators')
|
|
10
|
+
const CONTENT_HMR_EVENT = 'eclipsa:content-update'
|
|
11
|
+
const VIRTUAL_RUNTIME_ID = 'virtual:eclipsa-content:runtime'
|
|
12
|
+
const RESOLVED_VIRTUAL_RUNTIME_ID = '\0eclipsa-content:runtime'
|
|
13
|
+
const VIRTUAL_SEARCH_ID = 'virtual:eclipsa-content:search'
|
|
14
|
+
const RESOLVED_VIRTUAL_SEARCH_ID = '\0eclipsa-content:search'
|
|
15
|
+
const CONTENT_CONFIG_PATH = 'app/content.config.ts'
|
|
16
|
+
const CONTENT_COLLECTION_MARKER = '__eclipsa_content_collection__'
|
|
17
|
+
const CONTENT_SEARCH_ASSET = '__eclipsa_content_search__.json'
|
|
18
|
+
|
|
19
|
+
const normalizeSlashes = (value: string) => value.replaceAll('\\', '/')
|
|
20
|
+
const stripQuery = (id: string) => id.split('?', 1)[0] ?? id
|
|
21
|
+
|
|
22
|
+
const fileExists = async (filePath: string) => {
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(filePath)
|
|
25
|
+
return true
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const getConfigPath = (root: string) => path.join(root, CONTENT_CONFIG_PATH)
|
|
32
|
+
const getSearchAssetPath = (base: string) => {
|
|
33
|
+
const normalizedBase = base === '' ? '/' : base.endsWith('/') ? base : `${base}/`
|
|
34
|
+
return `${normalizedBase}${CONTENT_SEARCH_ASSET}`
|
|
35
|
+
}
|
|
36
|
+
const isContentConfigId = (root: string, id: string) =>
|
|
37
|
+
normalizeSlashes(path.resolve(stripQuery(id))) === normalizeSlashes(getConfigPath(root))
|
|
38
|
+
|
|
39
|
+
const getNamedCollectionExports = (source: string) =>
|
|
40
|
+
[...source.matchAll(/^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=/gm)].map((match) => match[1]!)
|
|
41
|
+
|
|
42
|
+
const invalidateVirtualRuntime = (server: ViteDevServer) => {
|
|
43
|
+
const graphs = [
|
|
44
|
+
(server as ViteDevServer & { moduleGraph?: any }).moduleGraph,
|
|
45
|
+
...Object.values(
|
|
46
|
+
(server as ViteDevServer & { environments?: Record<string, { moduleGraph?: any }> })
|
|
47
|
+
.environments ?? {},
|
|
48
|
+
).map((environment) => environment.moduleGraph),
|
|
49
|
+
]
|
|
50
|
+
for (const graph of graphs) {
|
|
51
|
+
if (!graph) {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
for (const id of [
|
|
55
|
+
VIRTUAL_RUNTIME_ID,
|
|
56
|
+
RESOLVED_VIRTUAL_RUNTIME_ID,
|
|
57
|
+
VIRTUAL_SEARCH_ID,
|
|
58
|
+
RESOLVED_VIRTUAL_SEARCH_ID,
|
|
59
|
+
]) {
|
|
60
|
+
const mod = graph.getModuleById?.(id)
|
|
61
|
+
if (mod) {
|
|
62
|
+
graph.invalidateModule?.(mod)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const invalidateRegisteredDevApps = (server: ViteDevServer) => {
|
|
69
|
+
const invalidators = (
|
|
70
|
+
server as ViteDevServer & {
|
|
71
|
+
[DEV_APP_INVALIDATORS_KEY]?: Set<() => void>
|
|
72
|
+
}
|
|
73
|
+
)[DEV_APP_INVALIDATORS_KEY]
|
|
74
|
+
if (!invalidators) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
for (const invalidate of invalidators) {
|
|
78
|
+
invalidate()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const shouldInvalidateForFile = (root: string, filePath: string) => {
|
|
83
|
+
const normalizedFilePath = normalizeSlashes(path.resolve(filePath))
|
|
84
|
+
const normalizedRoot = normalizeSlashes(path.resolve(root))
|
|
85
|
+
if (!normalizedFilePath.startsWith(normalizedRoot)) {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
if (normalizedFilePath === normalizeSlashes(path.join(root, CONTENT_CONFIG_PATH))) {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
return normalizedFilePath.endsWith('.md')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const createMissingRuntimeModule = (root: string) => {
|
|
95
|
+
const message = `Missing ${CONTENT_CONFIG_PATH} in ${root}.`
|
|
96
|
+
return `
|
|
97
|
+
const error = new Error(${JSON.stringify(message)});
|
|
98
|
+
export const getCollection = async () => { throw error; };
|
|
99
|
+
export const getEntries = async () => { throw error; };
|
|
100
|
+
export const getEntry = async () => { throw error; };
|
|
101
|
+
export const render = async () => { throw error; };
|
|
102
|
+
`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const createClientRuntimeModule = () => `
|
|
106
|
+
const error = new Error("@eclipsa/content query APIs are server-only.");
|
|
107
|
+
export const getCollection = async () => { throw error; };
|
|
108
|
+
export const getEntries = async () => { throw error; };
|
|
109
|
+
export const getEntry = async () => { throw error; };
|
|
110
|
+
export const render = async () => { throw error; };
|
|
111
|
+
`
|
|
112
|
+
|
|
113
|
+
const createDisabledSearchModule = () => `
|
|
114
|
+
export const searchOptions = ${JSON.stringify(resolveContentSearchOptions(false))};
|
|
115
|
+
export const search = async () => [];
|
|
116
|
+
export default { search, searchOptions };
|
|
117
|
+
`
|
|
118
|
+
|
|
119
|
+
const createClientContentConfigModule = async (configPath: string) => {
|
|
120
|
+
const source = await fs.readFile(configPath, 'utf8')
|
|
121
|
+
const exportNames = getNamedCollectionExports(source)
|
|
122
|
+
if (exportNames.length === 0) {
|
|
123
|
+
return ''
|
|
124
|
+
}
|
|
125
|
+
return exportNames
|
|
126
|
+
.map(
|
|
127
|
+
(name) =>
|
|
128
|
+
`export const ${name} = Object.freeze({ ${JSON.stringify(CONTENT_COLLECTION_MARKER)}: true });`,
|
|
129
|
+
)
|
|
130
|
+
.join('\n')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const createRuntimeModule = (root: string, configPath: string) => `
|
|
134
|
+
import * as collectionsModule from ${JSON.stringify(normalizeSlashes(configPath))};
|
|
135
|
+
import { createContentRuntime } from '@eclipsa/content/internal';
|
|
136
|
+
|
|
137
|
+
const runtime = createContentRuntime({
|
|
138
|
+
collectionsModule,
|
|
139
|
+
configPath: ${JSON.stringify(normalizeSlashes(configPath))},
|
|
140
|
+
root: ${JSON.stringify(normalizeSlashes(root))},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export const getCollection = runtime.getCollection;
|
|
144
|
+
export const getEntries = runtime.getEntries;
|
|
145
|
+
export const getEntry = runtime.getEntry;
|
|
146
|
+
export const render = runtime.render;
|
|
147
|
+
`
|
|
148
|
+
|
|
149
|
+
const loadCollectionsModule = async (configPath: string) => {
|
|
150
|
+
const href = pathToFileURL(configPath).href
|
|
151
|
+
return import(`${href}?t=${Date.now()}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const handleInvalidation = (server: ViteDevServer, root: string, filePath: string) => {
|
|
155
|
+
if (!shouldInvalidateForFile(root, filePath)) {
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
158
|
+
invalidateVirtualRuntime(server)
|
|
159
|
+
invalidateRegisteredDevApps(server)
|
|
160
|
+
;(
|
|
161
|
+
server as ViteDevServer & { ws?: { send?: (event: string, payload?: unknown) => void } }
|
|
162
|
+
).ws?.send?.(CONTENT_HMR_EVENT)
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const contentPlugin = (): Plugin => {
|
|
167
|
+
let config: ResolvedConfig
|
|
168
|
+
let searchStatePromise: Promise<{
|
|
169
|
+
indexJson: string
|
|
170
|
+
options: ResolvedContentSearchOptions
|
|
171
|
+
} | null> | null = null
|
|
172
|
+
|
|
173
|
+
const resolveSearchState = async () => {
|
|
174
|
+
if (searchStatePromise) {
|
|
175
|
+
return searchStatePromise
|
|
176
|
+
}
|
|
177
|
+
searchStatePromise = (async () => {
|
|
178
|
+
const configPath = getConfigPath(config.root)
|
|
179
|
+
if (!(await fileExists(configPath))) {
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
const collectionsModule = await loadCollectionsModule(configPath)
|
|
183
|
+
const result = await createContentSearch({
|
|
184
|
+
base: config.base,
|
|
185
|
+
collectionsModule,
|
|
186
|
+
configPath,
|
|
187
|
+
root: config.root,
|
|
188
|
+
})
|
|
189
|
+
if (!result.options.enabled || result.index.documents.length === 0) {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
indexJson: JSON.stringify(result.index),
|
|
194
|
+
options: result.options,
|
|
195
|
+
}
|
|
196
|
+
})()
|
|
197
|
+
return searchStatePromise
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
enforce: 'pre',
|
|
202
|
+
name: 'vite-plugin-eclipsa-content',
|
|
203
|
+
configResolved(resolvedConfig) {
|
|
204
|
+
config = resolvedConfig
|
|
205
|
+
},
|
|
206
|
+
configureServer(server) {
|
|
207
|
+
const searchPath = getSearchAssetPath(config.base)
|
|
208
|
+
server.middlewares.use(async (req, res, next) => {
|
|
209
|
+
const requestPath = req.url?.split('?', 1)[0] ?? ''
|
|
210
|
+
if (requestPath !== searchPath) {
|
|
211
|
+
next()
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
const state = await resolveSearchState()
|
|
215
|
+
if (!state) {
|
|
216
|
+
res.statusCode = 404
|
|
217
|
+
res.end('Not found')
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
221
|
+
res.end(state.indexJson)
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
hotUpdate(options) {
|
|
225
|
+
searchStatePromise = null
|
|
226
|
+
if (handleInvalidation(options.server, config.root, options.file)) {
|
|
227
|
+
return []
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
resolveId(id) {
|
|
231
|
+
if (id === VIRTUAL_RUNTIME_ID) {
|
|
232
|
+
return RESOLVED_VIRTUAL_RUNTIME_ID
|
|
233
|
+
}
|
|
234
|
+
if (id === VIRTUAL_SEARCH_ID) {
|
|
235
|
+
return RESOLVED_VIRTUAL_SEARCH_ID
|
|
236
|
+
}
|
|
237
|
+
return null
|
|
238
|
+
},
|
|
239
|
+
async load(id) {
|
|
240
|
+
if (id === RESOLVED_VIRTUAL_SEARCH_ID) {
|
|
241
|
+
const state = await resolveSearchState()
|
|
242
|
+
if (!state) {
|
|
243
|
+
return createDisabledSearchModule()
|
|
244
|
+
}
|
|
245
|
+
return generateContentSearchRuntimeModule(getSearchAssetPath(config.base), state.options)
|
|
246
|
+
}
|
|
247
|
+
if (id !== RESOLVED_VIRTUAL_RUNTIME_ID) {
|
|
248
|
+
if (this.environment?.name === 'client' && isContentConfigId(config.root, id)) {
|
|
249
|
+
return createClientContentConfigModule(getConfigPath(config.root))
|
|
250
|
+
}
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
if (this.environment?.name === 'client') {
|
|
254
|
+
return createClientRuntimeModule()
|
|
255
|
+
}
|
|
256
|
+
const configPath = getConfigPath(config.root)
|
|
257
|
+
if (!(await fileExists(configPath))) {
|
|
258
|
+
return createMissingRuntimeModule(config.root)
|
|
259
|
+
}
|
|
260
|
+
return createRuntimeModule(config.root, configPath)
|
|
261
|
+
},
|
|
262
|
+
async generateBundle() {
|
|
263
|
+
const state = await resolveSearchState()
|
|
264
|
+
if (!state) {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
this.emitFile({
|
|
268
|
+
fileName: CONTENT_SEARCH_ASSET,
|
|
269
|
+
source: state.indexJson,
|
|
270
|
+
type: 'asset',
|
|
271
|
+
})
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export const eclipsaContent = (): PluginOption => [contentPlugin()]
|