@kenjura/ursa 0.95.0 → 0.97.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/CHANGELOG.md +48 -0
- package/README.md +114 -16
- package/bin/ursa.js +14 -1
- package/meta/templates/default-template/content-hooks.js +45 -0
- package/meta/templates/default-template/index.html +1 -0
- package/meta/templates/default-template/menu.js +18 -1
- package/meta/templates/default-template/search.js +11 -0
- package/meta/templates/default-template/sticky.js +7 -1
- package/meta/templates/default-template/toc-generator.js +58 -38
- package/meta/templates/default-template/widgets.js +4 -0
- package/package.json +1 -2
- package/src/dev.js +13 -23
- package/src/helper/__test__/contentHash.test.js +16 -6
- package/src/helper/__test__/mdxRenderer.test.js +159 -0
- package/src/helper/__test__/sourceTimestamps.test.js +0 -0
- package/src/helper/assetBundler.js +93 -19
- package/src/helper/automenu.js +36 -11
- package/src/helper/build/__test__/autoIndex.test.js +2 -132
- package/src/helper/build/__test__/graph.test.js +259 -3
- package/src/helper/build/__test__/pass.test.js +553 -0
- package/src/helper/build/autoIndex.js +2 -371
- package/src/helper/build/excludeFilter.js +1 -2
- package/src/helper/build/footer.js +27 -14
- package/src/helper/build/graph.js +575 -152
- package/src/helper/build/index.js +0 -2
- package/src/helper/build/metadata.js +19 -5
- package/src/helper/build/pass.js +497 -0
- package/src/helper/build/precedence.js +174 -0
- package/src/helper/build/site.js +1270 -0
- package/src/helper/build/templates.js +1 -2
- package/src/helper/build/tracedFs.js +247 -0
- package/src/helper/contentHash.js +0 -78
- package/src/helper/customMenu.js +1 -1
- package/src/helper/fileRenderer.js +119 -111
- package/src/helper/findScriptJs.js +1 -1
- package/src/helper/findStyleCss.js +1 -1
- package/src/helper/folderConfig.js +7 -18
- package/src/helper/fullTextIndex.js +41 -29
- package/src/helper/imageProcessor.js +45 -0
- package/src/helper/linkValidator.js +118 -127
- package/src/helper/mdxRenderer.js +225 -26
- package/src/helper/menuLabels.js +30 -5
- package/src/helper/sourceTimestamps.js +139 -0
- package/src/helper/ursaConfig.js +3 -49
- package/src/helper/whitelistFilter.js +1 -2
- package/src/jobs/generate.js +67 -1859
- package/src/serve.js +317 -697
- package/src/helper/__test__/dependencyTracker.test.js +0 -157
- package/src/helper/build/cacheBust.js +0 -141
- package/src/helper/build/navCache.js +0 -145
- package/src/helper/build/watchCache.js +0 -33
- package/src/helper/dependencyTracker.js +0 -384
|
@@ -1,384 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dependency tracker for Ursa's regeneration system.
|
|
3
|
-
*
|
|
4
|
-
* Tracks which documents depend on which files so that when a file changes,
|
|
5
|
-
* we can determine exactly which documents need regeneration.
|
|
6
|
-
*
|
|
7
|
-
* Dependency types:
|
|
8
|
-
* - template: document uses a specific meta template
|
|
9
|
-
* - style: document inherits a specific style.css
|
|
10
|
-
* - script: document inherits a specific script.js
|
|
11
|
-
* - static: document references a static asset (image, font, etc.)
|
|
12
|
-
* - meta-asset: document depends on a meta CSS/JS file (via template bundling)
|
|
13
|
-
*
|
|
14
|
-
* The tracker maintains two indexes:
|
|
15
|
-
* 1. fileToDocuments: Map<dependencyPath, Set<documentPath>> — given a changed file, which documents need regeneration?
|
|
16
|
-
* 2. documentToFiles: Map<documentPath, Set<dependencyPath>> — given a document, what are its dependencies? (for cleanup)
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { dirname, join, relative, resolve } from "path";
|
|
20
|
-
import { existsSync } from "fs";
|
|
21
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
22
|
-
import { getUrsaDir } from "./contentHash.js";
|
|
23
|
-
|
|
24
|
-
const DEP_GRAPH_FILE = "dependency-graph.json";
|
|
25
|
-
const DEP_GRAPH_VERSION = 1;
|
|
26
|
-
|
|
27
|
-
// Static assets in meta (copied verbatim to output/public by copyMetaAssets);
|
|
28
|
-
// these are never embedded in document HTML, so no regeneration is needed.
|
|
29
|
-
const META_STATIC_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|eot|pdf|mp3|mp4|webm|ogg)$/i;
|
|
30
|
-
|
|
31
|
-
export class DependencyTracker {
|
|
32
|
-
constructor() {
|
|
33
|
-
/** @type {Map<string, Set<string>>} dependency file path → set of document paths */
|
|
34
|
-
this.fileToDocuments = new Map();
|
|
35
|
-
/** @type {Map<string, Set<string>>} document path → set of dependency file paths */
|
|
36
|
-
this.documentToFiles = new Map();
|
|
37
|
-
/** @type {string} source directory root (absolute, with trailing slash) */
|
|
38
|
-
this.sourceDir = "";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Initialize with the source directory.
|
|
43
|
-
* @param {string} sourceDir - Absolute path to source directory (with or without trailing slash)
|
|
44
|
-
*/
|
|
45
|
-
init(sourceDir) {
|
|
46
|
-
this.sourceDir = resolve(sourceDir) + "/";
|
|
47
|
-
this.fileToDocuments.clear();
|
|
48
|
-
this.documentToFiles.clear();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Register that a document depends on a file.
|
|
53
|
-
* @param {string} documentPath - Absolute path to the document
|
|
54
|
-
* @param {string} dependencyPath - Absolute path to the dependency file
|
|
55
|
-
*/
|
|
56
|
-
addDependency(documentPath, dependencyPath) {
|
|
57
|
-
// file → documents
|
|
58
|
-
if (!this.fileToDocuments.has(dependencyPath)) {
|
|
59
|
-
this.fileToDocuments.set(dependencyPath, new Set());
|
|
60
|
-
}
|
|
61
|
-
this.fileToDocuments.get(dependencyPath).add(documentPath);
|
|
62
|
-
|
|
63
|
-
// document → files
|
|
64
|
-
if (!this.documentToFiles.has(documentPath)) {
|
|
65
|
-
this.documentToFiles.set(documentPath, new Set());
|
|
66
|
-
}
|
|
67
|
-
this.documentToFiles.get(documentPath).add(dependencyPath);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Register dependencies for a document: template, style.css files, script.js files.
|
|
72
|
-
* @param {string} documentPath - Absolute path to the document
|
|
73
|
-
* @param {{ templateName: string, cssPaths: string[], scriptPaths: string[], metaAssets: string[] }} deps
|
|
74
|
-
*/
|
|
75
|
-
registerDocument(documentPath, { templateName, cssPaths = [], scriptPaths = [], metaAssets = [] } = {}) {
|
|
76
|
-
// Clear old dependencies for this document
|
|
77
|
-
this.clearDocument(documentPath);
|
|
78
|
-
|
|
79
|
-
// Template dependency (use a virtual path so template changes can be looked up)
|
|
80
|
-
if (templateName) {
|
|
81
|
-
this.addDependency(documentPath, `template:${templateName}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Style.css dependencies
|
|
85
|
-
for (const cssPath of cssPaths) {
|
|
86
|
-
this.addDependency(documentPath, cssPath);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Script.js dependencies
|
|
90
|
-
for (const scriptPath of scriptPaths) {
|
|
91
|
-
this.addDependency(documentPath, scriptPath);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Meta template assets (CSS/JS referenced by the template)
|
|
95
|
-
for (const metaAsset of metaAssets) {
|
|
96
|
-
this.addDependency(documentPath, metaAsset);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Clear all dependencies for a document.
|
|
102
|
-
* @param {string} documentPath
|
|
103
|
-
*/
|
|
104
|
-
clearDocument(documentPath) {
|
|
105
|
-
const deps = this.documentToFiles.get(documentPath);
|
|
106
|
-
if (deps) {
|
|
107
|
-
for (const dep of deps) {
|
|
108
|
-
const docSet = this.fileToDocuments.get(dep);
|
|
109
|
-
if (docSet) {
|
|
110
|
-
docSet.delete(documentPath);
|
|
111
|
-
if (docSet.size === 0) this.fileToDocuments.delete(dep);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
this.documentToFiles.delete(documentPath);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Get all documents that depend on a given file.
|
|
120
|
-
* @param {string} filePath - Absolute path to the changed file
|
|
121
|
-
* @returns {Set<string>} Set of document paths that need regeneration
|
|
122
|
-
*/
|
|
123
|
-
getAffectedDocuments(filePath) {
|
|
124
|
-
return this.fileToDocuments.get(filePath) || new Set();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Get all documents that use a specific template.
|
|
129
|
-
* @param {string} templateName - Template name (e.g., "default-template")
|
|
130
|
-
* @returns {Set<string>} Set of document paths
|
|
131
|
-
*/
|
|
132
|
-
getDocumentsUsingTemplate(templateName) {
|
|
133
|
-
return this.getAffectedDocuments(`template:${templateName}`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Determine which documents are affected by a changed file.
|
|
138
|
-
* This is the main entry point for the invalidation logic.
|
|
139
|
-
*
|
|
140
|
-
* @param {string} changedFile - Absolute path to the changed file
|
|
141
|
-
* @param {string} sourceDir - Absolute path to source directory
|
|
142
|
-
* @returns {{ affectedDocuments: string[], reason: string, requiresFullRebuild: boolean }}
|
|
143
|
-
*/
|
|
144
|
-
getInvalidationPlan(changedFile, sourceDir) {
|
|
145
|
-
const normalizedSource = resolve(sourceDir) + "/";
|
|
146
|
-
const relativePath = changedFile.replace(normalizedSource, "");
|
|
147
|
-
const fileName = relativePath.split("/").pop();
|
|
148
|
-
|
|
149
|
-
// 1. Menu/config changes → full rebuild (affects navigation structure)
|
|
150
|
-
if (
|
|
151
|
-
fileName === "menu.md" || fileName === "menu.txt" || fileName === "_menu" ||
|
|
152
|
-
fileName === "config.json" || fileName === "_config"
|
|
153
|
-
) {
|
|
154
|
-
return {
|
|
155
|
-
affectedDocuments: [],
|
|
156
|
-
reason: `Menu/config change: ${relativePath}`,
|
|
157
|
-
requiresFullRebuild: true,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 2. style.css or script.js → affects current folder + all subfolders
|
|
162
|
-
// These are "inherited" files — documents in the folder and all subfolders include them.
|
|
163
|
-
if (
|
|
164
|
-
fileName === "style.css" || fileName === "_style.css" || fileName === "style-ursa.css" ||
|
|
165
|
-
fileName === "script.js" || fileName === "_script.js"
|
|
166
|
-
) {
|
|
167
|
-
// Get all documents directly registered as depending on this file
|
|
168
|
-
const directDeps = this.getAffectedDocuments(changedFile);
|
|
169
|
-
|
|
170
|
-
if (directDeps.size > 0) {
|
|
171
|
-
return {
|
|
172
|
-
affectedDocuments: [...directDeps],
|
|
173
|
-
reason: `Inherited ${fileName} changed: ${relativePath} (${directDeps.size} documents)`,
|
|
174
|
-
requiresFullRebuild: false,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Fallback: if dependency tracker wasn't populated, scope by directory
|
|
179
|
-
const changedDir = dirname(changedFile);
|
|
180
|
-
const allDocs = [...this.documentToFiles.keys()];
|
|
181
|
-
const affected = allDocs.filter((doc) => doc.startsWith(changedDir));
|
|
182
|
-
return {
|
|
183
|
-
affectedDocuments: affected,
|
|
184
|
-
reason: `Inherited ${fileName} changed: ${relativePath} (${affected.length} documents in subtree, fallback)`,
|
|
185
|
-
requiresFullRebuild: false,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// 3. Article file changes → just that document
|
|
190
|
-
if (/\.(md|mdx|txt|yml|yaml)$/.test(fileName)) {
|
|
191
|
-
return {
|
|
192
|
-
affectedDocuments: [changedFile],
|
|
193
|
-
reason: `Article changed: ${relativePath}`,
|
|
194
|
-
requiresFullRebuild: false,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 4. Other static files (images, fonts, etc.) → find documents that reference them
|
|
199
|
-
const directDeps = this.getAffectedDocuments(changedFile);
|
|
200
|
-
if (directDeps.size > 0) {
|
|
201
|
-
return {
|
|
202
|
-
affectedDocuments: [...directDeps],
|
|
203
|
-
reason: `Static asset changed: ${relativePath} (${directDeps.size} referencing documents)`,
|
|
204
|
-
requiresFullRebuild: false,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// If we don't track this file, it's safe to just broadcast reload (dev mode only)
|
|
209
|
-
return {
|
|
210
|
-
affectedDocuments: [],
|
|
211
|
-
reason: `Unknown file changed: ${relativePath} (no registered dependents)`,
|
|
212
|
-
requiresFullRebuild: false,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Determine which documents are affected by a meta file change.
|
|
218
|
-
* @param {string} changedFile - Absolute path to the changed meta file
|
|
219
|
-
* @param {string} metaDir - Absolute path to meta directory
|
|
220
|
-
* @returns {{ affectedDocuments: string[], reason: string, requiresFullRebuild: boolean }}
|
|
221
|
-
*/
|
|
222
|
-
getMetaInvalidationPlan(changedFile, metaDir) {
|
|
223
|
-
const normalizedMeta = resolve(metaDir) + "/";
|
|
224
|
-
const relativePath = changedFile.replace(normalizedMeta, "");
|
|
225
|
-
const fileName = relativePath.split("/").pop();
|
|
226
|
-
|
|
227
|
-
// Template file changed → regenerate all documents using that template
|
|
228
|
-
if (fileName.endsWith(".html")) {
|
|
229
|
-
// New structure: templates/{templateName}/index.html → name is the folder;
|
|
230
|
-
// legacy flat structure: {templateName}.html at the meta root
|
|
231
|
-
const parts = relativePath.split("/");
|
|
232
|
-
const templateName =
|
|
233
|
-
parts[0] === "templates" && parts.length >= 3
|
|
234
|
-
? parts[1]
|
|
235
|
-
: fileName.replace(".html", "");
|
|
236
|
-
const affected = this.getDocumentsUsingTemplate(templateName);
|
|
237
|
-
if (affected.size > 0) {
|
|
238
|
-
return {
|
|
239
|
-
affectedDocuments: [...affected],
|
|
240
|
-
reason: `Template changed: ${templateName} (${affected.size} documents)`,
|
|
241
|
-
requiresFullRebuild: false,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
// If no documents tracked, fall back to full rebuild (safe default)
|
|
245
|
-
return {
|
|
246
|
-
affectedDocuments: [],
|
|
247
|
-
reason: `Template changed: ${templateName} (no tracked documents, full rebuild)`,
|
|
248
|
-
requiresFullRebuild: true,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Meta CSS or JS file changed → all documents are affected
|
|
253
|
-
// (meta assets are bundled into every template)
|
|
254
|
-
if (fileName.endsWith(".css") || fileName.endsWith(".js")) {
|
|
255
|
-
const allDocs = [...this.documentToFiles.keys()];
|
|
256
|
-
return {
|
|
257
|
-
affectedDocuments: allDocs,
|
|
258
|
-
reason: `Meta asset changed: ${relativePath} (affects all ${allDocs.length} documents)`,
|
|
259
|
-
requiresFullRebuild: false,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Static asset in meta (image, font, PDF, media) → copyMetaAssets already
|
|
264
|
-
// re-copied it to output/public; documents reference it by URL, so no
|
|
265
|
-
// document regeneration (and no full rebuild) is needed.
|
|
266
|
-
if (META_STATIC_EXTENSIONS.test(fileName)) {
|
|
267
|
-
return {
|
|
268
|
-
affectedDocuments: [],
|
|
269
|
-
reason: `Meta static asset copied: ${relativePath}`,
|
|
270
|
-
requiresFullRebuild: false,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Other meta file → full rebuild to be safe
|
|
275
|
-
return {
|
|
276
|
-
affectedDocuments: [],
|
|
277
|
-
reason: `Meta file changed: ${relativePath}`,
|
|
278
|
-
requiresFullRebuild: true,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Get stats about the dependency graph.
|
|
284
|
-
* @returns {{ totalDocuments: number, totalDependencies: number, uniqueFiles: number }}
|
|
285
|
-
*/
|
|
286
|
-
getStats() {
|
|
287
|
-
return {
|
|
288
|
-
totalDocuments: this.documentToFiles.size,
|
|
289
|
-
totalDependencies: [...this.documentToFiles.values()].reduce((sum, s) => sum + s.size, 0),
|
|
290
|
-
uniqueFiles: this.fileToDocuments.size,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Serialize the tracker for persistence to .ursa/dependency-graph.json.
|
|
296
|
-
* @returns {{ version: number, sourceDir: string, documents: Object<string, string[]> }}
|
|
297
|
-
*/
|
|
298
|
-
serialize() {
|
|
299
|
-
return {
|
|
300
|
-
version: DEP_GRAPH_VERSION,
|
|
301
|
-
sourceDir: this.sourceDir,
|
|
302
|
-
documents: Object.fromEntries(
|
|
303
|
-
[...this.documentToFiles.entries()].map(([doc, deps]) => [doc, [...deps]])
|
|
304
|
-
),
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Load persisted registrations, merging with the current run: documents
|
|
310
|
-
* already registered in this run keep their (fresher) edges; persisted
|
|
311
|
-
* edges only fill in documents not yet registered (e.g. hash-skipped docs).
|
|
312
|
-
* Rejects data from a different source directory or schema version.
|
|
313
|
-
* @param {object} data - Previously serialized tracker
|
|
314
|
-
* @returns {boolean} Whether the data was loaded
|
|
315
|
-
*/
|
|
316
|
-
load(data) {
|
|
317
|
-
if (!data || data.version !== DEP_GRAPH_VERSION) return false;
|
|
318
|
-
if (data.sourceDir && this.sourceDir && data.sourceDir !== this.sourceDir) return false;
|
|
319
|
-
for (const [doc, deps] of Object.entries(data.documents || {})) {
|
|
320
|
-
if (this.documentToFiles.has(doc)) continue; // live registrations win
|
|
321
|
-
if (!Array.isArray(deps)) continue;
|
|
322
|
-
for (const dep of deps) {
|
|
323
|
-
this.addDependency(doc, dep);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return true;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Drop registrations for documents not in the given set (e.g. deleted or
|
|
331
|
-
* excluded files), so persisted state doesn't accumulate stale entries.
|
|
332
|
-
* @param {Set<string>} keepDocuments - Document paths that should survive
|
|
333
|
-
*/
|
|
334
|
-
prune(keepDocuments) {
|
|
335
|
-
for (const doc of [...this.documentToFiles.keys()]) {
|
|
336
|
-
if (!keepDocuments.has(doc)) this.clearDocument(doc);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Singleton instance
|
|
342
|
-
export const dependencyTracker = new DependencyTracker();
|
|
343
|
-
|
|
344
|
-
/** Path to the persisted dependency graph for a source directory. */
|
|
345
|
-
export function getDependencyGraphPath(sourceDir) {
|
|
346
|
-
return join(getUrsaDir(sourceDir), DEP_GRAPH_FILE);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Load the persisted dependency tracker state from .ursa/dependency-graph.json
|
|
351
|
-
* and merge it into the tracker (current-run registrations win).
|
|
352
|
-
* @param {string} sourceDir - Source directory root
|
|
353
|
-
* @param {DependencyTracker} [tracker] - Defaults to the singleton
|
|
354
|
-
* @returns {Promise<boolean>} Whether a valid graph was loaded
|
|
355
|
-
*/
|
|
356
|
-
export async function loadDependencyTracker(sourceDir, tracker = dependencyTracker) {
|
|
357
|
-
const path = getDependencyGraphPath(sourceDir);
|
|
358
|
-
try {
|
|
359
|
-
if (!existsSync(path)) return false;
|
|
360
|
-
const data = JSON.parse(await readFile(path, "utf8"));
|
|
361
|
-
return tracker.load(data);
|
|
362
|
-
} catch (e) {
|
|
363
|
-
console.warn(`Could not load dependency graph: ${e.message}`);
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Persist the dependency tracker to .ursa/dependency-graph.json so that
|
|
370
|
-
* hash-skipped documents keep their edges across warm starts.
|
|
371
|
-
* @param {string} sourceDir - Source directory root
|
|
372
|
-
* @param {DependencyTracker} [tracker] - Defaults to the singleton
|
|
373
|
-
* @returns {Promise<boolean>} Whether the graph was saved
|
|
374
|
-
*/
|
|
375
|
-
export async function saveDependencyTracker(sourceDir, tracker = dependencyTracker) {
|
|
376
|
-
try {
|
|
377
|
-
await mkdir(getUrsaDir(sourceDir), { recursive: true });
|
|
378
|
-
await writeFile(getDependencyGraphPath(sourceDir), JSON.stringify(tracker.serialize()));
|
|
379
|
-
return true;
|
|
380
|
-
} catch (e) {
|
|
381
|
-
console.warn(`Could not save dependency graph: ${e.message}`);
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
|
-
}
|