erudit 4.2.0 → 4.3.0-dev.1
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/app/components/Prose.vue +2 -0
- package/app/components/aside/major/contentNav/items/ContentNavTopic.vue +12 -1
- package/app/components/aside/major/search/SearchResult.vue +16 -2
- package/app/components/aside/minor/contributor/AsideMinorContributor.vue +7 -1
- package/app/components/main/MainStickyHeader.vue +5 -2
- package/app/components/main/MainStickyHeaderPreamble.vue +5 -2
- package/app/components/main/MainTopicPartPage.vue +3 -2
- package/app/components/main/MainTopicPartSwitch.vue +18 -7
- package/app/components/main/connections/Deps.vue +1 -4
- package/app/components/main/connections/MainConnections.vue +9 -3
- package/app/components/main/contentStats/ItemLastChanged.vue +3 -32
- package/app/components/main/contentStats/MainContentStats.vue +3 -4
- package/app/components/preview/Preview.vue +8 -6
- package/app/components/preview/PreviewScreen.vue +9 -7
- package/app/components/preview/screen/Unique.vue +3 -2
- package/app/composables/ads.ts +1 -1
- package/app/composables/analytics.ts +1 -1
- package/app/composables/lastChanged.ts +38 -5
- package/app/composables/og.ts +5 -5
- package/app/composables/scrollUp.ts +3 -1
- package/app/pages/book/[...bookId].vue +3 -2
- package/app/pages/group/[...groupId].vue +3 -2
- package/app/pages/page/[...pageId].vue +4 -2
- package/app/plugins/appSetup/config.ts +1 -0
- package/app/plugins/appSetup/global.ts +3 -0
- package/app/plugins/appSetup/index.ts +4 -1
- package/app/plugins/devReload.client.ts +13 -0
- package/app/router.options.ts +17 -3
- package/app/styles/main.css +2 -2
- package/modules/erudit/dependencies.ts +16 -0
- package/modules/erudit/index.ts +8 -1
- package/modules/erudit/setup/autoImports.ts +143 -0
- package/modules/erudit/setup/elements/globalTemplate.ts +10 -2
- package/modules/erudit/setup/elements/setup.ts +8 -14
- package/modules/erudit/setup/elements/tagsTable.ts +2 -18
- package/modules/erudit/setup/fullRestart.ts +5 -3
- package/modules/erudit/setup/namesTable.ts +33 -0
- package/modules/erudit/setup/problemChecks/setup.ts +60 -0
- package/modules/erudit/setup/problemChecks/shared.ts +4 -0
- package/modules/erudit/setup/problemChecks/template.ts +33 -0
- package/modules/erudit/setup/runtimeConfig.ts +12 -7
- package/nuxt.config.ts +14 -6
- package/package.json +5 -6
- package/server/api/problemScript/[...problemScriptPath].ts +245 -60
- package/server/erudit/build.ts +10 -4
- package/server/erudit/content/nav/build.ts +5 -5
- package/server/erudit/content/nav/front.ts +1 -0
- package/server/erudit/content/repository/deps.ts +33 -3
- package/server/erudit/content/resolve/index.ts +3 -3
- package/server/erudit/content/resolve/utils/contentError.ts +2 -2
- package/server/erudit/content/resolve/utils/insertContentResolved.ts +22 -5
- package/server/erudit/global.ts +5 -1
- package/server/erudit/importer.ts +69 -0
- package/server/erudit/index.ts +2 -2
- package/server/erudit/logger.ts +18 -10
- package/server/erudit/reloadSignal.ts +14 -0
- package/server/routes/_reload.ts +27 -0
- package/shared/types/contentConnections.ts +1 -0
- package/shared/types/frontContentNav.ts +2 -0
|
@@ -4,7 +4,22 @@ import { build, type Plugin } from 'esbuild';
|
|
|
4
4
|
|
|
5
5
|
import { STATIC_ASSET_EXTENSIONS } from '#layers/erudit/server/erudit/prose/transform/extensions';
|
|
6
6
|
import { createGlobalContent } from '@erudit-js/core/content/global';
|
|
7
|
-
import {
|
|
7
|
+
import { eruditGlobalNames } from '#erudit/prose/global';
|
|
8
|
+
import { autoImportNames } from '#erudit/autoImports';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Pure function calls irrelevant to the problem script runtime.
|
|
12
|
+
* Their entire call expressions (including all arguments) are replaced with
|
|
13
|
+
* `undefined` before bundling so esbuild never follows transitive imports
|
|
14
|
+
* inside their arguments (e.g. heavy checker handlers importing `mathjs`).
|
|
15
|
+
*/
|
|
16
|
+
const WIPE_OUT_FUNCTIONS: string[] = [
|
|
17
|
+
'defineProblemChecker',
|
|
18
|
+
'defineContributor',
|
|
19
|
+
'defineTopic',
|
|
20
|
+
'defineBook',
|
|
21
|
+
'definePage',
|
|
22
|
+
];
|
|
8
23
|
|
|
9
24
|
export default defineEventHandler<Promise<string>>(async (event) => {
|
|
10
25
|
// <filepathToScriptFile>.js
|
|
@@ -25,7 +40,7 @@ export default defineEventHandler<Promise<string>>(async (event) => {
|
|
|
25
40
|
$CONTRIBUTOR: '{}',
|
|
26
41
|
},
|
|
27
42
|
jsx: 'automatic',
|
|
28
|
-
plugins: [jsxRuntimePlugin,
|
|
43
|
+
plugins: [jsxRuntimePlugin, sourceTransformPlugin, staticFilesPlugin],
|
|
29
44
|
alias: {
|
|
30
45
|
'#project': ERUDIT.paths.project() + '/',
|
|
31
46
|
'#content': ERUDIT.paths.project('content') + '/',
|
|
@@ -34,6 +49,11 @@ export default defineEventHandler<Promise<string>>(async (event) => {
|
|
|
34
49
|
|
|
35
50
|
let code = buildResult.outputFiles[0]!.text;
|
|
36
51
|
|
|
52
|
+
// Post-build: strip redundant ERUDIT_GLOBAL declarations emitted by esbuild,
|
|
53
|
+
// normalize JSX identifiers, and prepend a selective destructuring preamble
|
|
54
|
+
// with only the global names actually used in the bundle.
|
|
55
|
+
code = normalizeEruditGlobals(code);
|
|
56
|
+
|
|
37
57
|
// Transform $CONTENT patterns to link objects
|
|
38
58
|
code = code.replace(/\$CONTENT(\.[a-zA-Z_$][\w$]*)+/g, (match) => {
|
|
39
59
|
const path = match
|
|
@@ -63,73 +83,192 @@ const jsxRuntimePlugin: Plugin = {
|
|
|
63
83
|
}));
|
|
64
84
|
|
|
65
85
|
build.onLoad({ filter: /.*/, namespace: 'jsx-runtime-shim' }, () => ({
|
|
86
|
+
// Export both the canonical and underscore-prefixed names so that
|
|
87
|
+
// pre-built modules importing `jsx as _jsx` also resolve correctly.
|
|
66
88
|
contents: `
|
|
67
|
-
export const jsx = globalThis.jsx;
|
|
68
|
-
export const jsxs = globalThis.jsxs;
|
|
69
|
-
export const Fragment = globalThis.Fragment;
|
|
89
|
+
export const jsx = globalThis.ERUDIT_GLOBAL.jsx;
|
|
90
|
+
export const jsxs = globalThis.ERUDIT_GLOBAL.jsxs;
|
|
91
|
+
export const Fragment = globalThis.ERUDIT_GLOBAL.Fragment;
|
|
92
|
+
export const _jsx = globalThis.ERUDIT_GLOBAL.jsx;
|
|
93
|
+
export const _jsxs = globalThis.ERUDIT_GLOBAL.jsxs;
|
|
94
|
+
export const _Fragment = globalThis.ERUDIT_GLOBAL.Fragment;
|
|
70
95
|
`,
|
|
71
96
|
loader: 'js',
|
|
72
97
|
}));
|
|
73
98
|
},
|
|
74
99
|
};
|
|
75
100
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
'jsx',
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
// Maps underscore-prefixed JSX names (emitted by some bundlers/transpilers) to
|
|
102
|
+
// the canonical globalThis key where the value actually lives.
|
|
103
|
+
const JSX_UNDERSCORE_ALIASES: Record<string, string> = {
|
|
104
|
+
_jsx: 'jsx',
|
|
105
|
+
_jsxs: 'jsxs',
|
|
106
|
+
_Fragment: 'Fragment',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Names that are available on globalThis.ERUDIT_GLOBAL and should not be bundled as real imports.
|
|
110
|
+
function getGlobalNames(): Set<string> {
|
|
111
|
+
return new Set<string>([
|
|
112
|
+
...eruditGlobalNames,
|
|
113
|
+
// Auto-imported names from erudit config
|
|
114
|
+
...autoImportNames,
|
|
115
|
+
// Underscore-prefixed aliases for JSX
|
|
116
|
+
'_jsx',
|
|
117
|
+
'_jsxs',
|
|
118
|
+
'_Fragment',
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Replace calls to functions listed in `WIPE_OUT_FUNCTIONS` with `undefined`.
|
|
124
|
+
* Uses balanced-parenthesis scanning so nested calls/objects inside arguments
|
|
125
|
+
* are handled correctly.
|
|
126
|
+
*
|
|
127
|
+
* Example:
|
|
128
|
+
* export default defineProblemChecker(def, async (d, i) => { ... });
|
|
129
|
+
* → export default undefined;
|
|
130
|
+
*/
|
|
131
|
+
function wipeOutCalls(source: string): string {
|
|
132
|
+
const pattern = new RegExp(
|
|
133
|
+
'\\b(' + WIPE_OUT_FUNCTIONS.join('|') + ')\\s*\\(',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
let result = '';
|
|
137
|
+
let remaining = source;
|
|
138
|
+
|
|
139
|
+
while (true) {
|
|
140
|
+
const m = pattern.exec(remaining);
|
|
141
|
+
if (!m) {
|
|
142
|
+
result += remaining;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Skip function/method declarations (e.g. `function defineProblemChecker(`)
|
|
147
|
+
const prefix = remaining.slice(Math.max(0, m.index - 20), m.index);
|
|
148
|
+
if (/\bfunction\s*$/.test(prefix)) {
|
|
149
|
+
result += remaining.slice(0, m.index + m[0].length);
|
|
150
|
+
remaining = remaining.slice(m.index + m[0].length);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Append everything before the function name
|
|
155
|
+
result += remaining.slice(0, m.index);
|
|
156
|
+
|
|
157
|
+
// Find the opening paren position (right after the function name + optional whitespace)
|
|
158
|
+
const openParenIndex = m.index + m[0].length - 1;
|
|
159
|
+
let depth = 1;
|
|
160
|
+
let i = openParenIndex + 1;
|
|
161
|
+
|
|
162
|
+
// Scan forward to find the matching closing paren, skipping strings/templates
|
|
163
|
+
while (i < remaining.length && depth > 0) {
|
|
164
|
+
const ch = remaining[i]!;
|
|
165
|
+
|
|
166
|
+
if (ch === '(') {
|
|
167
|
+
depth++;
|
|
168
|
+
} else if (ch === ')') {
|
|
169
|
+
depth--;
|
|
170
|
+
} else if (ch === "'" || ch === '"' || ch === '`') {
|
|
171
|
+
// Skip string/template literal
|
|
172
|
+
const quote = ch;
|
|
173
|
+
i++;
|
|
174
|
+
while (i < remaining.length) {
|
|
175
|
+
const sc = remaining[i]!;
|
|
176
|
+
if (sc === '\\') {
|
|
177
|
+
i += 2; // skip escaped char
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (sc === quote) break;
|
|
181
|
+
i++;
|
|
182
|
+
}
|
|
183
|
+
} else if (ch === '/' && remaining[i + 1] === '/') {
|
|
184
|
+
// Skip single-line comment
|
|
185
|
+
while (i < remaining.length && remaining[i] !== '\n') i++;
|
|
186
|
+
continue;
|
|
187
|
+
} else if (ch === '/' && remaining[i + 1] === '*') {
|
|
188
|
+
// Skip block comment
|
|
189
|
+
i += 2;
|
|
190
|
+
while (i < remaining.length - 1) {
|
|
191
|
+
if (remaining[i] === '*' && remaining[i + 1] === '/') {
|
|
192
|
+
i += 2;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Replace the entire call expression with `undefined`
|
|
204
|
+
result += 'undefined';
|
|
205
|
+
remaining = remaining.slice(i);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Rewrite imports of known globalThis names (JSX runtime + prose tags) from
|
|
213
|
+
* real import statements to `const X = globalThis["X"]` lookups.
|
|
214
|
+
* Non-global imports are left as real imports and bundled normally.
|
|
215
|
+
*/
|
|
216
|
+
function rewriteGlobalImports(
|
|
217
|
+
source: string,
|
|
218
|
+
globalNames: Set<string>,
|
|
219
|
+
): string {
|
|
220
|
+
return source.replace(
|
|
221
|
+
/^import\s+\{([^}]+)\}\s+from\s+(['"])([^'"]+)\2.*$/gm,
|
|
222
|
+
(_match, bindings: string, _quote: string, pkg: string) => {
|
|
223
|
+
const keepParts: string[] = [];
|
|
224
|
+
const shimLines: string[] = [];
|
|
225
|
+
|
|
226
|
+
for (const part of bindings
|
|
227
|
+
.split(',')
|
|
228
|
+
.map((s) => s.trim())
|
|
229
|
+
.filter(Boolean)) {
|
|
230
|
+
// handle "ExportName as LocalName"
|
|
231
|
+
const m = part.match(/^(\w+)(?:\s+as\s+(\w+))?$/);
|
|
232
|
+
if (!m) {
|
|
233
|
+
keepParts.push(part);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const localName = m[2] ?? m[1]!;
|
|
237
|
+
if (globalNames.has(localName)) {
|
|
238
|
+
// Underscore-prefixed JSX names (_jsx, _jsxs, _Fragment) must resolve
|
|
239
|
+
// to the canonical globalThis key (jsx, jsxs, Fragment) because the
|
|
240
|
+
// runtime only registers the un-prefixed versions.
|
|
241
|
+
const globalKey = JSX_UNDERSCORE_ALIASES[localName] ?? localName;
|
|
242
|
+
shimLines.push(
|
|
243
|
+
`var ${localName} = globalThis.ERUDIT_GLOBAL[${JSON.stringify(globalKey)}];`,
|
|
244
|
+
);
|
|
245
|
+
} else {
|
|
246
|
+
keepParts.push(part);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const lines: string[] = [];
|
|
251
|
+
if (keepParts.length > 0)
|
|
252
|
+
lines.push(`import { ${keepParts.join(', ')} } from '${pkg}';`);
|
|
253
|
+
lines.push(...shimLines);
|
|
254
|
+
return lines.join('\n');
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Pre-transform: wipe out pure function calls irrelevant to problem scripts,
|
|
260
|
+
// then rewrite imports of known globalThis tag names → const from globalThis.
|
|
261
|
+
// Applies to every .ts/.tsx/.js/.jsx file esbuild processes (including utility files).
|
|
262
|
+
|
|
263
|
+
const sourceTransformPlugin: Plugin = {
|
|
264
|
+
name: 'source-transform',
|
|
96
265
|
setup(build) {
|
|
266
|
+
const globalNames = getGlobalNames();
|
|
97
267
|
build.onLoad({ filter: /\.[jt]sx?$/ }, (args) => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const transformed = source.replace(
|
|
101
|
-
/^import\s+\{([^}]+)\}\s+from\s+(['"])([^'"]+)\2.*$/gm,
|
|
102
|
-
(_match, bindings: string, _quote: string, pkg: string) => {
|
|
103
|
-
const keepParts: string[] = [];
|
|
104
|
-
const shimLines: string[] = [];
|
|
105
|
-
|
|
106
|
-
for (const part of bindings
|
|
107
|
-
.split(',')
|
|
108
|
-
.map((s) => s.trim())
|
|
109
|
-
.filter(Boolean)) {
|
|
110
|
-
// handle "ExportName as LocalName"
|
|
111
|
-
const m = part.match(/^(\w+)(?:\s+as\s+(\w+))?$/);
|
|
112
|
-
if (!m) {
|
|
113
|
-
keepParts.push(part);
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
const localName = m[2] ?? m[1]!;
|
|
117
|
-
if (globalNames.has(localName)) {
|
|
118
|
-
shimLines.push(
|
|
119
|
-
`const ${localName} = globalThis[${JSON.stringify(localName)}];`,
|
|
120
|
-
);
|
|
121
|
-
} else {
|
|
122
|
-
keepParts.push(part);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
268
|
+
let source = readFileSync(args.path, 'utf8');
|
|
125
269
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
lines.push(`import { ${keepParts.join(', ')} } from '${pkg}';`);
|
|
129
|
-
lines.push(...shimLines);
|
|
130
|
-
return lines.join('\n');
|
|
131
|
-
},
|
|
132
|
-
);
|
|
270
|
+
source = wipeOutCalls(source);
|
|
271
|
+
source = rewriteGlobalImports(source, globalNames);
|
|
133
272
|
|
|
134
273
|
const ext = (args.path.match(/[jt]sx?$/)?.[0] ?? 'js') as
|
|
135
274
|
| 'js'
|
|
@@ -137,13 +276,59 @@ const proseGlobalsPlugin: Plugin = {
|
|
|
137
276
|
| 'ts'
|
|
138
277
|
| 'tsx';
|
|
139
278
|
return {
|
|
140
|
-
contents:
|
|
279
|
+
contents: source,
|
|
141
280
|
loader: ext,
|
|
142
281
|
};
|
|
143
282
|
});
|
|
144
283
|
},
|
|
145
284
|
};
|
|
146
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Post-build pass that:
|
|
288
|
+
* 1. Strips all `var X = globalThis.ERUDIT_GLOBAL[...]` declarations (from
|
|
289
|
+
* rewriteGlobalImports and the jsxRuntimePlugin shim) and the shim comment.
|
|
290
|
+
* 2. Normalizes underscore-prefixed JSX call-site names (_jsx2 → jsx, etc.).
|
|
291
|
+
* 3. Detects which ERUDIT_GLOBAL names are actually referenced in the code.
|
|
292
|
+
* 4. Prepends a single destructuring preamble with only those names.
|
|
293
|
+
*/
|
|
294
|
+
function normalizeEruditGlobals(code: string): string {
|
|
295
|
+
// Strip the jsx-runtime-shim comment
|
|
296
|
+
code = code.replace(/^\/\/ jsx-runtime-shim:jsx-runtime-shim\n/gm, '');
|
|
297
|
+
|
|
298
|
+
// Strip ALL var declarations pulling from globalThis.ERUDIT_GLOBAL
|
|
299
|
+
// var jsx = globalThis.ERUDIT_GLOBAL.jsx;
|
|
300
|
+
// var P = globalThis.ERUDIT_GLOBAL["P"];
|
|
301
|
+
// var _jsx2 = globalThis.ERUDIT_GLOBAL.jsx;
|
|
302
|
+
code = code.replace(
|
|
303
|
+
/^var \w+ = globalThis\.ERUDIT_GLOBAL(?:\.[a-zA-Z_$][\w$]*|\[["'][a-zA-Z_$][\w$]*["']\]);[ \t]*\n?/gm,
|
|
304
|
+
'',
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// Normalize call-site names (_jsxs must precede _jsx to avoid partial match):
|
|
308
|
+
// _jsxs2(…) → jsxs(…)
|
|
309
|
+
// _jsx2(…) → jsx(…)
|
|
310
|
+
// _Fragment2 → Fragment
|
|
311
|
+
code = code.replace(/_jsxs\d*\b/g, 'jsxs');
|
|
312
|
+
code = code.replace(/_jsx\d*\b/g, 'jsx');
|
|
313
|
+
code = code.replace(/_Fragment\d*\b/g, 'Fragment');
|
|
314
|
+
|
|
315
|
+
// Detect which ERUDIT_GLOBAL names are actually used in the code
|
|
316
|
+
const allNames = getGlobalNames();
|
|
317
|
+
const usedNames = [...allNames]
|
|
318
|
+
.filter((n) => /^[a-zA-Z_$]\w*$/.test(n) && !n.startsWith('_'))
|
|
319
|
+
.filter((n) => new RegExp('\\b' + n + '\\b').test(code));
|
|
320
|
+
|
|
321
|
+
if (usedNames.length > 0) {
|
|
322
|
+
code =
|
|
323
|
+
'var { ' +
|
|
324
|
+
usedNames.join(', ') +
|
|
325
|
+
' } = globalThis.ERUDIT_GLOBAL;\n' +
|
|
326
|
+
code;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return code;
|
|
330
|
+
}
|
|
331
|
+
|
|
147
332
|
const staticFilesPlugin: Plugin = {
|
|
148
333
|
name: 'static-files',
|
|
149
334
|
setup(build) {
|
package/server/erudit/build.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
2
|
import chokidar from 'chokidar';
|
|
3
3
|
import { debounce } from 'perfect-debounce';
|
|
4
4
|
import { sn } from 'unslash';
|
|
@@ -11,6 +11,7 @@ import { buildContentNav } from './content/nav/build';
|
|
|
11
11
|
import { requestFullContentResolve, resolveContent } from './content/resolve';
|
|
12
12
|
import { buildGlobalContent } from './content/global/build';
|
|
13
13
|
import { buildNews } from './news/build';
|
|
14
|
+
import { triggerReload } from './reloadSignal';
|
|
14
15
|
|
|
15
16
|
export type EruditServerChangedFiles = Set<string>;
|
|
16
17
|
export type EruditServerBuildError = Error | undefined;
|
|
@@ -28,7 +29,7 @@ export async function buildServerErudit() {
|
|
|
28
29
|
await buildContentNav();
|
|
29
30
|
await buildGlobalContent();
|
|
30
31
|
await resolveContent();
|
|
31
|
-
ERUDIT.log.success(
|
|
32
|
+
ERUDIT.log.success(styleText('green', 'Build Complete!'));
|
|
32
33
|
} catch (buildError) {
|
|
33
34
|
requestFullContentResolve();
|
|
34
35
|
|
|
@@ -73,11 +74,16 @@ export async function tryServerWatchProject() {
|
|
|
73
74
|
const files = Array.from(ERUDIT.changedFiles);
|
|
74
75
|
console.log();
|
|
75
76
|
ERUDIT.log.warn(
|
|
76
|
-
`${
|
|
77
|
-
files
|
|
77
|
+
`${styleText('yellow', 'Rebuilding due to file change(s):')}\n\n` +
|
|
78
|
+
files
|
|
79
|
+
.map((p, i) => styleText('gray', `${i + 1} -`) + ` "${p}"`)
|
|
80
|
+
.join('\n'),
|
|
78
81
|
);
|
|
79
82
|
console.log();
|
|
80
83
|
await buildServerErudit();
|
|
84
|
+
if (!ERUDIT.buildError) {
|
|
85
|
+
triggerReload();
|
|
86
|
+
}
|
|
81
87
|
ERUDIT.changedFiles.clear();
|
|
82
88
|
} finally {
|
|
83
89
|
ERUDIT.changedFiles.clear();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
2
|
import { globSync } from 'glob';
|
|
3
3
|
import { existsSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { contentTypes, type ContentType } from '@erudit-js/core/content/type';
|
|
@@ -132,7 +132,7 @@ export async function buildContentNav() {
|
|
|
132
132
|
|
|
133
133
|
const stats = ERUDIT.contentNav.id2Node.size
|
|
134
134
|
? getContentNavStats(ERUDIT.contentNav.id2Node)
|
|
135
|
-
:
|
|
135
|
+
: styleText('gray', 'empty');
|
|
136
136
|
|
|
137
137
|
ERUDIT.log.success(
|
|
138
138
|
isInitial
|
|
@@ -324,7 +324,7 @@ const duplicateIdValidator: ContentNavValidator = () => {
|
|
|
324
324
|
Content navigation node short ID duplication!
|
|
325
325
|
+ ${seenShortIds.get(navNode.shortId)}
|
|
326
326
|
+ ${navNode.fullId}
|
|
327
|
-
-> ${
|
|
327
|
+
-> ${styleText('yellow', navNode.shortId)}
|
|
328
328
|
`.trim(),
|
|
329
329
|
);
|
|
330
330
|
duplicateIds.push(navNode.fullId);
|
|
@@ -348,7 +348,7 @@ const subsetValidator: ContentNavValidator = () => {
|
|
|
348
348
|
ERUDIT.log.warn(
|
|
349
349
|
`
|
|
350
350
|
Only groups and books can have children!
|
|
351
|
-
-> ${
|
|
351
|
+
-> ${styleText('yellow', navNode.fullId)}
|
|
352
352
|
`.trim(),
|
|
353
353
|
);
|
|
354
354
|
wrongSubsetIds.push(navNode.fullId);
|
|
@@ -368,7 +368,7 @@ const bookCantSkipValidator: ContentNavValidator = () => {
|
|
|
368
368
|
ERUDIT.log.warn(
|
|
369
369
|
`
|
|
370
370
|
Books can not be skipped!
|
|
371
|
-
-> ${navNode.contentRelPath.split('/').slice(0, -1).join('/')}/${
|
|
371
|
+
-> ${navNode.contentRelPath.split('/').slice(0, -1).join('/')}/${styleText('yellow', navNode.contentRelPath.split('/').pop()!)}
|
|
372
372
|
`.trim(),
|
|
373
373
|
);
|
|
374
374
|
wrongBookIds.push(navNode.fullId);
|
|
@@ -4,7 +4,7 @@ export async function getContentDependencies(fullId: string) {
|
|
|
4
4
|
const hardDependencies: ContentHardDep[] = [];
|
|
5
5
|
|
|
6
6
|
const dbHardDependencies = await ERUDIT.db.query.contentDeps.findMany({
|
|
7
|
-
columns: { toFullId: true, hard: true, reason: true },
|
|
7
|
+
columns: { toFullId: true, hard: true, reason: true, uniqueNames: true },
|
|
8
8
|
where: and(
|
|
9
9
|
or(
|
|
10
10
|
eq(ERUDIT.db.schema.contentDeps.fromFullId, fullId),
|
|
@@ -21,13 +21,35 @@ export async function getContentDependencies(fullId: string) {
|
|
|
21
21
|
return map;
|
|
22
22
|
}, new Map<string, string>());
|
|
23
23
|
|
|
24
|
+
// Merge unique names across hard dep rows sharing the same toFullId
|
|
25
|
+
// (can happen when a topic and its children both hard-dep the same target).
|
|
26
|
+
const hardUniqueMap = new Map<string, Set<string>>();
|
|
27
|
+
for (const row of dbHardDependencies) {
|
|
28
|
+
if (!hardUniqueMap.has(row.toFullId)) {
|
|
29
|
+
hardUniqueMap.set(row.toFullId, new Set());
|
|
30
|
+
}
|
|
31
|
+
if (row.uniqueNames) {
|
|
32
|
+
for (const name of row.uniqueNames.split(',')) {
|
|
33
|
+
hardUniqueMap.get(row.toFullId)!.add(name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
const hardToFullIds = ERUDIT.contentNav.orderIds(
|
|
25
39
|
externalToFullIds(dbHardDependencies),
|
|
26
40
|
);
|
|
27
41
|
|
|
28
42
|
for (const toFullId of hardToFullIds) {
|
|
29
43
|
const reason = fullId2Reason.get(toFullId)!;
|
|
30
|
-
const
|
|
44
|
+
const uniquePairs = Array.from(hardUniqueMap.get(toFullId) ?? []).map(
|
|
45
|
+
(uniqueName) => ({ contentFullId: toFullId, uniqueName }),
|
|
46
|
+
);
|
|
47
|
+
const hardDep = await createContentDep(
|
|
48
|
+
'hard',
|
|
49
|
+
toFullId,
|
|
50
|
+
uniquePairs.length > 0 ? uniquePairs : undefined,
|
|
51
|
+
reason,
|
|
52
|
+
);
|
|
31
53
|
if (hardDep) {
|
|
32
54
|
hardDependencies.push(hardDep);
|
|
33
55
|
}
|
|
@@ -166,7 +188,7 @@ async function createContentDep(
|
|
|
166
188
|
async function createContentDep(
|
|
167
189
|
type: 'hard',
|
|
168
190
|
fullId: string,
|
|
169
|
-
uniquePairs: undefined,
|
|
191
|
+
uniquePairs: { contentFullId: string; uniqueName: string }[] | undefined,
|
|
170
192
|
reason: string,
|
|
171
193
|
): Promise<ContentHardDep | undefined>;
|
|
172
194
|
async function createContentDep(
|
|
@@ -189,12 +211,20 @@ async function createContentDep(
|
|
|
189
211
|
]);
|
|
190
212
|
|
|
191
213
|
if (type === 'hard') {
|
|
214
|
+
const hardUniques =
|
|
215
|
+
uniquePairs && uniquePairs.length > 0
|
|
216
|
+
? await resolveUniqueEntries(uniquePairs)
|
|
217
|
+
: undefined;
|
|
218
|
+
|
|
192
219
|
return {
|
|
193
220
|
type: 'hard',
|
|
194
221
|
reason: reason!,
|
|
195
222
|
contentType,
|
|
196
223
|
title,
|
|
197
224
|
link,
|
|
225
|
+
...(hardUniques && hardUniques.length > 0
|
|
226
|
+
? { uniques: hardUniques }
|
|
227
|
+
: {}),
|
|
198
228
|
};
|
|
199
229
|
}
|
|
200
230
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
2
|
import { inArray, or } from 'drizzle-orm';
|
|
3
3
|
import { contentPathToId } from '@erudit-js/core/content/path';
|
|
4
4
|
|
|
@@ -180,7 +180,7 @@ function renderChangedContentTree(ids: Set<string>): string {
|
|
|
180
180
|
const walkTree = (node: Node, depth: number) => {
|
|
181
181
|
if (node.name) {
|
|
182
182
|
const indent = ' '.repeat(Math.max(0, depth - 1));
|
|
183
|
-
lines.push(`${indent}- ${
|
|
183
|
+
lines.push(`${indent}- ${styleText('cyan', node.name)}`);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
const children = Array.from(node.children.values()).sort((a, b) =>
|
|
@@ -199,7 +199,7 @@ function renderChangedContentTree(ids: Set<string>): string {
|
|
|
199
199
|
walkTree(child, 1);
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
const header =
|
|
202
|
+
const header = styleText('gray', 'Changed content:');
|
|
203
203
|
return [header, ...lines].join('\n');
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
2
|
import type { ContentNavNode } from '../../nav/types';
|
|
3
3
|
|
|
4
4
|
export function logContentError(contentNode: ContentNavNode) {
|
|
5
5
|
ERUDIT.log.error(
|
|
6
6
|
`Error parsing ${contentNode.type} ${ERUDIT.log.stress(
|
|
7
7
|
contentNode.fullId,
|
|
8
|
-
)}!\nLocation: ${
|
|
8
|
+
)}!\nLocation: ${styleText('red', ERUDIT.paths.project(`content/${contentNode.contentRelPath}`))}`,
|
|
9
9
|
);
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
2
|
import { sql } from 'drizzle-orm';
|
|
3
3
|
import type { ContentProseType } from '@erudit-js/core/content/prose';
|
|
4
4
|
import { builtValidPaths } from '../../global/build';
|
|
@@ -80,7 +80,24 @@ async function insertContentDeps(
|
|
|
80
80
|
await ERUDIT.db
|
|
81
81
|
.insert(ERUDIT.db.schema.contentDeps)
|
|
82
82
|
.values(contentDeps)
|
|
83
|
-
.
|
|
83
|
+
.onConflictDoUpdate({
|
|
84
|
+
target: [
|
|
85
|
+
ERUDIT.db.schema.contentDeps.fromFullId,
|
|
86
|
+
ERUDIT.db.schema.contentDeps.toFullId,
|
|
87
|
+
],
|
|
88
|
+
set: {
|
|
89
|
+
// Merge unique names from the auto dep into the existing row
|
|
90
|
+
// (which may already be a hard dep that has no uniqueNames yet).
|
|
91
|
+
// Only uniqueNames is updated — hard/reason are left untouched.
|
|
92
|
+
uniqueNames: sql`CASE
|
|
93
|
+
WHEN ${ERUDIT.db.schema.contentDeps.uniqueNames} IS NULL
|
|
94
|
+
THEN excluded.uniqueNames
|
|
95
|
+
WHEN excluded.uniqueNames IS NULL
|
|
96
|
+
THEN ${ERUDIT.db.schema.contentDeps.uniqueNames}
|
|
97
|
+
ELSE ${ERUDIT.db.schema.contentDeps.uniqueNames} || ',' || excluded.uniqueNames
|
|
98
|
+
END`,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
84
101
|
}
|
|
85
102
|
}
|
|
86
103
|
|
|
@@ -93,7 +110,7 @@ function filterTargetMap(
|
|
|
93
110
|
const brokenLinkMessage = (message: string, metas: ContentLinkUsage[]) => {
|
|
94
111
|
let output = `${message} in ${ERUDIT.log.stress(contentFullId)}:\n`;
|
|
95
112
|
for (const { type, label } of metas) {
|
|
96
|
-
output += ` ${
|
|
113
|
+
output += ` ${styleText('gray', '➔')} <${type}>${label}</${type}>\n`;
|
|
97
114
|
}
|
|
98
115
|
return output;
|
|
99
116
|
};
|
|
@@ -104,7 +121,7 @@ function filterTargetMap(
|
|
|
104
121
|
if (storageKey.startsWith('<link:unknown>/')) {
|
|
105
122
|
ERUDIT.log.warn(
|
|
106
123
|
brokenLinkMessage(
|
|
107
|
-
`Unknown link ${
|
|
124
|
+
`Unknown link ${styleText('red', storageKey.replace('<link:unknown>/', ''))}`,
|
|
108
125
|
metas,
|
|
109
126
|
),
|
|
110
127
|
);
|
|
@@ -136,7 +153,7 @@ function filterTargetMap(
|
|
|
136
153
|
} catch {
|
|
137
154
|
ERUDIT.log.warn(
|
|
138
155
|
brokenLinkMessage(
|
|
139
|
-
`Failed to resolve content link ${
|
|
156
|
+
`Failed to resolve content link ${styleText('red', storageKey.replace('<link:global>/', ''))}`,
|
|
140
157
|
metas,
|
|
141
158
|
),
|
|
142
159
|
);
|
package/server/erudit/global.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { EruditServerPaths } from './path';
|
|
|
11
11
|
import type { EruditServerRepository } from './repository';
|
|
12
12
|
|
|
13
13
|
import { registerProseGlobals } from '#erudit/prose/global';
|
|
14
|
+
import { registerAutoImportGlobals } from '#erudit/autoImports';
|
|
14
15
|
|
|
15
16
|
export const ERUDIT: {
|
|
16
17
|
buildError: EruditServerBuildError;
|
|
@@ -27,7 +28,9 @@ export const ERUDIT: {
|
|
|
27
28
|
import: EruditServerImporter;
|
|
28
29
|
} = {} as any;
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
(globalThis as any).ERUDIT_GLOBAL = (globalThis as any).ERUDIT_GLOBAL || {};
|
|
32
|
+
|
|
33
|
+
Object.assign((globalThis as any).ERUDIT_GLOBAL, {
|
|
31
34
|
defineContributor,
|
|
32
35
|
defineSponsor,
|
|
33
36
|
defineCameo,
|
|
@@ -40,3 +43,4 @@ Object.assign(globalThis, {
|
|
|
40
43
|
});
|
|
41
44
|
|
|
42
45
|
registerProseGlobals();
|
|
46
|
+
registerAutoImportGlobals();
|