dexin-content 0.1.1 → 0.2.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/LICENSE +202 -21
- package/bin/dexin-content.mjs +6 -0
- package/cli/build.ts +188 -0
- package/cli/check.ts +124 -0
- package/cli/main.ts +207 -0
- package/cli/package.ts +137 -0
- package/cli/serve.ts +135 -0
- package/cli/validate.ts +136 -0
- package/{collection.ts → core/artifact/collection.ts} +297 -289
- package/{diff.ts → core/artifact/diff.ts} +125 -124
- package/{query.ts → core/artifact/query.ts} +56 -56
- package/{store.ts → core/artifact/store.ts} +128 -128
- package/core/{compiler.ts → compiler/compiler.ts} +24 -21
- package/core/compiler/discovery.ts +116 -0
- package/core/{frontmatter.ts → parser/frontmatter.ts} +145 -144
- package/core/{markdown.ts → parser/markdown.ts} +68 -36
- package/core/types/index.ts +131 -0
- package/core/types/lessonAST.ts +264 -0
- package/core/validator/integrity.ts +199 -0
- package/domains/document/identity.ts +27 -0
- package/domains/document/index.ts +5 -0
- package/domains/document/parser.ts +105 -0
- package/domains/lesson/blocks.ts +205 -0
- package/domains/lesson/identity.ts +40 -0
- package/domains/lesson/index.ts +7 -0
- package/domains/lesson/parser.ts +200 -0
- package/index.ts +57 -8
- package/package.json +82 -68
- package/core/discovery.ts +0 -110
- package/core/types.ts +0 -162
|
@@ -1,289 +1,297 @@
|
|
|
1
|
-
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// dexin-content/collection.ts
|
|
3
|
-
//
|
|
4
|
-
// Collection declaration + source adaptation + discovery +
|
|
5
|
-
// batch compile orchestration.
|
|
6
|
-
//
|
|
7
|
-
// The batch compileCollections() wraps the formal per-file
|
|
8
|
-
// core/compiler.ts compile() — it discovers files, builds
|
|
9
|
-
// CompileInput per file, calls compile, and writes PositiveArtifact
|
|
10
|
-
// docs + ContentIndex to the store.
|
|
11
|
-
//
|
|
12
|
-
// grep-zero: no business vocabulary appears anywhere in this
|
|
13
|
-
// module — including error messages.
|
|
14
|
-
// ─────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
import { readdir, readFile } from 'node:fs/promises'
|
|
17
|
-
import { join } from 'node:path'
|
|
18
|
-
import { createHash } from 'node:crypto'
|
|
19
|
-
import type {
|
|
20
|
-
PositiveArtifact,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
import
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// dexin-content/collection.ts
|
|
3
|
+
//
|
|
4
|
+
// Collection declaration + source adaptation + discovery +
|
|
5
|
+
// batch compile orchestration (API-FREEZE §3.1–§3.4).
|
|
6
|
+
//
|
|
7
|
+
// The batch compileCollections() wraps the formal per-file
|
|
8
|
+
// core/compiler.ts compile() — it discovers files, builds
|
|
9
|
+
// CompileInput per file, calls compile, and writes PositiveArtifact
|
|
10
|
+
// docs + ContentIndex to the store.
|
|
11
|
+
//
|
|
12
|
+
// grep-zero: no business vocabulary appears anywhere in this
|
|
13
|
+
// module — including error messages.
|
|
14
|
+
// ─────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { createHash } from 'node:crypto'
|
|
19
|
+
import type {
|
|
20
|
+
PositiveArtifact,
|
|
21
|
+
Identity,
|
|
22
|
+
DomainName,
|
|
23
|
+
Meta,
|
|
24
|
+
ParseError
|
|
25
|
+
} from '../types'
|
|
26
|
+
import type { DomainParserRegistry, CompileInput } from '../compiler/compiler'
|
|
27
|
+
import { compile } from '../compiler/compiler'
|
|
28
|
+
import { buildDocumentIdentity } from '../compiler/discovery'
|
|
29
|
+
import { splitFrontmatter, parseFrontmatter, validateSchema } from '../parser/frontmatter'
|
|
30
|
+
import type { ArtifactStore, ContentIndex, IndexEntry } from './store'
|
|
31
|
+
|
|
32
|
+
// ── Collection types (API-FREEZE §3.1) ──
|
|
33
|
+
|
|
34
|
+
export interface CollectionDefinition {
|
|
35
|
+
/** Collection name (appears in identity.collection + IndexEntry.collection). */
|
|
36
|
+
name: string
|
|
37
|
+
/** Directory under content root; .md files collected recursively. */
|
|
38
|
+
source: string
|
|
39
|
+
/** Domain tag; defaults to 'document' when omitted. */
|
|
40
|
+
domain?: DomainName | string
|
|
41
|
+
/** Frontmatter schema (reuses core Schema; validator-only, no meta projection). */
|
|
42
|
+
schema?: { required?: string[]; types?: Record<string, 'string' | 'number' | 'boolean'> }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ResolvedCollection extends CollectionDefinition {
|
|
46
|
+
domain: string
|
|
47
|
+
sourceRoot: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Declaration passthrough (declarative API semantic); resolution in resolveCollections. */
|
|
51
|
+
export function defineCollection (def: CollectionDefinition): CollectionDefinition {
|
|
52
|
+
return def
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Resolve definitions: fill domain default + bind absolute sourceRoot. */
|
|
56
|
+
export function resolveCollections (
|
|
57
|
+
defs: CollectionDefinition[],
|
|
58
|
+
contentRoot: string
|
|
59
|
+
): ResolvedCollection[] {
|
|
60
|
+
return defs.map(d => ({
|
|
61
|
+
...d,
|
|
62
|
+
domain: d.domain ?? 'document',
|
|
63
|
+
sourceRoot: join(contentRoot, d.source)
|
|
64
|
+
}))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── SourceAdapter (API-FREEZE §3.2) ──
|
|
68
|
+
|
|
69
|
+
export interface SourceAdapter {
|
|
70
|
+
list(): Promise<string[]>
|
|
71
|
+
read(relPath: string): Promise<string>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Local filesystem source. Reads .md files recursively from root.
|
|
76
|
+
* CRLF protection (R-d): read() throws LINE_ENDING_CONTAMINATION
|
|
77
|
+
* on CRLF so the batch path enjoys the same guard as run-p0.
|
|
78
|
+
*/
|
|
79
|
+
export function createLocalSource (root: string): SourceAdapter {
|
|
80
|
+
async function walk (dir: string, base: string): Promise<string[]> {
|
|
81
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
82
|
+
const out: string[] = []
|
|
83
|
+
for (const e of entries) {
|
|
84
|
+
const rel = base ? `${base}/${e.name}` : e.name
|
|
85
|
+
if (e.isDirectory()) out.push(...await walk(join(dir, e.name), rel))
|
|
86
|
+
else if (e.isFile() && e.name.endsWith('.md')) out.push(rel)
|
|
87
|
+
}
|
|
88
|
+
return out.sort()
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
list: () => walk(root, ''),
|
|
92
|
+
read: async (rel) => {
|
|
93
|
+
const raw = await readFile(join(root, rel), 'utf8')
|
|
94
|
+
const src = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw
|
|
95
|
+
if (src.includes('\r')) {
|
|
96
|
+
const err = new Error(
|
|
97
|
+
`[LINE_ENDING_CONTAMINATION] Source file '${rel}' contains CRLF line endings. ` +
|
|
98
|
+
`Normalize to LF before processing.`
|
|
99
|
+
) as ParseError
|
|
100
|
+
err.code = 'LINE_ENDING_CONTAMINATION'
|
|
101
|
+
err.file = rel
|
|
102
|
+
throw err
|
|
103
|
+
}
|
|
104
|
+
return src
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Virtual in-memory source (test / fixture use). */
|
|
110
|
+
export function createMemorySource (files: Record<string, string>): SourceAdapter {
|
|
111
|
+
return {
|
|
112
|
+
list: async () => Object.keys(files).sort(),
|
|
113
|
+
read: async (rel) => {
|
|
114
|
+
if (!(rel in files)) throw new Error(`[MemorySource] Not found: ${rel}`)
|
|
115
|
+
return files[rel]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Discovery (API-FREEZE §3.3) ──
|
|
121
|
+
|
|
122
|
+
export interface ContentFile {
|
|
123
|
+
id: string
|
|
124
|
+
path: string
|
|
125
|
+
collection: string
|
|
126
|
+
file: string
|
|
127
|
+
frontmatter: string
|
|
128
|
+
body: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Scan all collections via source. Files not belonging to any
|
|
133
|
+
* collection → throw (fail-fast, no silent drop).
|
|
134
|
+
*
|
|
135
|
+
* Identity derivation: document-domain path convention via
|
|
136
|
+
* buildDocumentIdentity (id = rel minus .md minus trailing /index,
|
|
137
|
+
* path = '/' + id, file = rel, collection = name). Non-document
|
|
138
|
+
* domains are not yet supported in the batch path (T5 lands the
|
|
139
|
+
* structured-ext-A domain discovery rules); a thrown error makes
|
|
140
|
+
* the gap explicit.
|
|
141
|
+
*/
|
|
142
|
+
export async function discover (
|
|
143
|
+
source: SourceAdapter,
|
|
144
|
+
collections: ResolvedCollection[]
|
|
145
|
+
): Promise<ContentFile[]> {
|
|
146
|
+
const byDir = new Map(collections.map(c => [c.source, c]))
|
|
147
|
+
const all = await source.list()
|
|
148
|
+
const files: ContentFile[] = []
|
|
149
|
+
for (const rel of all) {
|
|
150
|
+
const top = rel.split('/')[0]
|
|
151
|
+
const col = byDir.get(top)
|
|
152
|
+
if (!col) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`[discover] File '${rel}' belongs to no collection (top dir '${top}' undefined).`
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
if (col.domain !== 'document') {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`[discover] Domain '${col.domain}' discovery not yet implemented in batch path; ` +
|
|
160
|
+
`only 'document' domain is supported here. Non-document domains require ` +
|
|
161
|
+
`explicit identity construction (see run-p0 FixtureSpec pattern).`
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
const identity = buildDocumentIdentity(rel, col.name)
|
|
165
|
+
const raw = await source.read(rel)
|
|
166
|
+
const { frontmatter, body } = splitFrontmatter(raw)
|
|
167
|
+
files.push({
|
|
168
|
+
id: identity.id,
|
|
169
|
+
path: identity.path,
|
|
170
|
+
collection: col.name,
|
|
171
|
+
file: identity.file,
|
|
172
|
+
frontmatter,
|
|
173
|
+
body
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
return files
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Batch compile orchestration (API-FREEZE §3.3) ──
|
|
180
|
+
|
|
181
|
+
function checksum (s: string): string {
|
|
182
|
+
return createHash('md5').update(s, 'utf-8').digest('hex').slice(0, 12)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Batch compile: discover → per-file compile → write docs + index to store.
|
|
187
|
+
*
|
|
188
|
+
* For each ContentFile:
|
|
189
|
+
* 1. Build CompileInput (fixture = file.id, identity = buildDocumentIdentity,
|
|
190
|
+
* source = raw, schema = col.schema)
|
|
191
|
+
* 2. Call formal compile(input, registry)
|
|
192
|
+
* 3. On positive → collect PositiveArtifact + IndexEntry
|
|
193
|
+
* On error → collect error message (fail-fast: throw after all files processed)
|
|
194
|
+
*
|
|
195
|
+
* Store receives only positive artifacts. compile failures are aggregated
|
|
196
|
+
* and thrown as a single Error (prototype-aligned behaviour).
|
|
197
|
+
*/
|
|
198
|
+
export async function compileCollections (
|
|
199
|
+
source: SourceAdapter,
|
|
200
|
+
collections: ResolvedCollection[],
|
|
201
|
+
registry: DomainParserRegistry,
|
|
202
|
+
store: ArtifactStore
|
|
203
|
+
): Promise<{ docs: PositiveArtifact[]; index: ContentIndex }> {
|
|
204
|
+
const files = await discover(source, collections)
|
|
205
|
+
const docs: PositiveArtifact[] = []
|
|
206
|
+
const entries: IndexEntry[] = []
|
|
207
|
+
const errors: string[] = []
|
|
208
|
+
|
|
209
|
+
for (const f of files) {
|
|
210
|
+
const col = collections.find(c => c.name === f.collection)!
|
|
211
|
+
const raw = await source.read(f.file)
|
|
212
|
+
const input: CompileInput = {
|
|
213
|
+
fixture: f.id,
|
|
214
|
+
domain: col.domain,
|
|
215
|
+
identity: buildDocumentIdentity(f.file, col.name),
|
|
216
|
+
source: raw,
|
|
217
|
+
file: f.file,
|
|
218
|
+
schema: col.schema
|
|
219
|
+
}
|
|
220
|
+
const result = compile(input, registry)
|
|
221
|
+
if (result.kind !== 'positive') {
|
|
222
|
+
errors.push(` ✗ ${f.file}: ${result.error?.message ?? 'unknown'}`)
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
const artifact = result.artifact as PositiveArtifact
|
|
226
|
+
docs.push(artifact)
|
|
227
|
+
entries.push({
|
|
228
|
+
id: f.id,
|
|
229
|
+
path: f.path,
|
|
230
|
+
collection: f.collection,
|
|
231
|
+
domain: col.domain,
|
|
232
|
+
file: f.file,
|
|
233
|
+
meta: result.meta,
|
|
234
|
+
checksum: checksum(f.body)
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (errors.length > 0) {
|
|
239
|
+
throw new Error(`Compile failed (${errors.length} error(s)):\n${errors.join('\n')}`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const index: ContentIndex = {
|
|
243
|
+
generator: 'dexin-content',
|
|
244
|
+
builtAt: new Date().toISOString(),
|
|
245
|
+
entries
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await store.writeIndex(index)
|
|
249
|
+
for (const doc of docs) {
|
|
250
|
+
await store.writeDoc(doc)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { docs, index }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Incremental recompile (API-FREEZE §3.3, prototype L663 移植) ──
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Detect changed/removed docs by checksum diff against the previous index.
|
|
260
|
+
*
|
|
261
|
+
* Wraps compileCollections (which writes index + all docs to store), then
|
|
262
|
+
* reports which ids changed (checksum differs from prev) or were removed
|
|
263
|
+
* (present in prev, absent in next). Used by the Dev flow (memory store)
|
|
264
|
+
* to drive incremental re-render signals.
|
|
265
|
+
*
|
|
266
|
+
* Adapts prototype's recompileChanged to the formal layer: prototype's
|
|
267
|
+
* `compile(source, collections, domains)` returned { docs, index } without
|
|
268
|
+
* writing to store; our compileCollections writes index + all docs to
|
|
269
|
+
* store AND returns { docs, index }. The changed/removed detection is
|
|
270
|
+
* preserved; for in-memory Dev stores writing all docs is acceptable
|
|
271
|
+
* (ephemeral). Per B1-SPEC T5.4 "最小形态", watcher + preview server
|
|
272
|
+
* are explicitly out of scope.
|
|
273
|
+
*/
|
|
274
|
+
export async function recompileChanged (
|
|
275
|
+
source: SourceAdapter,
|
|
276
|
+
collections: ResolvedCollection[],
|
|
277
|
+
registry: DomainParserRegistry,
|
|
278
|
+
store: ArtifactStore
|
|
279
|
+
): Promise<{ changed: string[]; removed: string[] }> {
|
|
280
|
+
const prev = await store.readIndex()
|
|
281
|
+
const prevMap = new Map((prev?.entries ?? []).map(e => [e.id, e]))
|
|
282
|
+
|
|
283
|
+
const { index } = await compileCollections(source, collections, registry, store)
|
|
284
|
+
|
|
285
|
+
const nextIds = new Set(index.entries.map(e => e.id))
|
|
286
|
+
const changed: string[] = []
|
|
287
|
+
const removed: string[] = []
|
|
288
|
+
|
|
289
|
+
for (const e of index.entries) {
|
|
290
|
+
if (prevMap.get(e.id)?.checksum !== e.checksum) changed.push(e.id)
|
|
291
|
+
}
|
|
292
|
+
for (const id of prevMap.keys()) {
|
|
293
|
+
if (!nextIds.has(id)) removed.push(id)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { changed, removed }
|
|
297
|
+
}
|