erudit 4.2.0-dev.1 → 4.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/app/components/aside/minor/news/AsideMinorNews.vue +1 -1
- package/app/components/main/contentStats/MainContentStats.vue +1 -1
- package/app/composables/phrases.ts +2 -0
- package/package.json +4 -4
- package/server/api/problemScript/[...problemScriptPath].ts +13 -5
- package/server/erudit/content/global/build.ts +43 -3
- package/server/erudit/content/repository/deps.ts +12 -3
- package/server/erudit/content/resolve/utils/insertContentResolved.ts +7 -22
|
@@ -98,7 +98,7 @@ const phrase = await usePhrases('news', 'no_news', 'show_more');
|
|
|
98
98
|
<div class="flex h-full w-full flex-col">
|
|
99
99
|
<AsideMinorPlainHeader
|
|
100
100
|
icon="bell"
|
|
101
|
-
:title="phrase
|
|
101
|
+
:title="phrase.news"
|
|
102
102
|
:count="newsTotal === 0 ? undefined : newsTotal"
|
|
103
103
|
/>
|
|
104
104
|
<section v-if="newsItems.length === 0">
|
|
@@ -23,7 +23,6 @@ const lastChangedSource = useLastChangedSource(() => props.contentRelativePath);
|
|
|
23
23
|
class="micro:justify-start gap-small micro:gap-normal flex flex-wrap
|
|
24
24
|
justify-center"
|
|
25
25
|
>
|
|
26
|
-
<ItemLastChanged v-if="lastChangedSource" :source="lastChangedSource" />
|
|
27
26
|
<ItemMaterials
|
|
28
27
|
v-if="stats?.materials"
|
|
29
28
|
:count="stats.materials"
|
|
@@ -36,6 +35,7 @@ const lastChangedSource = useLastChangedSource(() => props.contentRelativePath);
|
|
|
36
35
|
:count
|
|
37
36
|
mode="detailed"
|
|
38
37
|
/>
|
|
38
|
+
<ItemLastChanged v-if="lastChangedSource" :source="lastChangedSource" />
|
|
39
39
|
</div>
|
|
40
40
|
</section>
|
|
41
41
|
<div
|
|
@@ -39,6 +39,7 @@ export function usePhrases<const T extends readonly LanguagePhraseKey[]>(
|
|
|
39
39
|
|
|
40
40
|
const strFunctions = await $fetch<Record<string, string>>(
|
|
41
41
|
'/api/language/functions',
|
|
42
|
+
{ responseType: 'json' },
|
|
42
43
|
);
|
|
43
44
|
|
|
44
45
|
payloadLanguage.functions = strFunctions;
|
|
@@ -69,6 +70,7 @@ export function usePhrases<const T extends readonly LanguagePhraseKey[]>(
|
|
|
69
70
|
try {
|
|
70
71
|
payloadPhraseValue = await $fetch<PayloadLanguagePhraseValue>(
|
|
71
72
|
`/api/language/phrase/${phraseKey}`,
|
|
73
|
+
{ responseType: 'json' },
|
|
72
74
|
);
|
|
73
75
|
|
|
74
76
|
payloadLanguage.phrases[phraseKey] = payloadPhraseValue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "erudit",
|
|
3
|
-
"version": "4.2.0
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "🤓 CMS for perfect educational sites.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@erudit-js/cli": "4.2.0
|
|
28
|
-
"@erudit-js/core": "4.2.0
|
|
29
|
-
"@erudit-js/prose": "4.2.0
|
|
27
|
+
"@erudit-js/cli": "4.2.0",
|
|
28
|
+
"@erudit-js/core": "4.2.0",
|
|
29
|
+
"@erudit-js/prose": "4.2.0",
|
|
30
30
|
"unslash": "^2.0.0",
|
|
31
31
|
"@floating-ui/vue": "^1.1.10",
|
|
32
32
|
"tsprose": "^1.0.0",
|
|
@@ -73,12 +73,20 @@ const jsxRuntimePlugin: Plugin = {
|
|
|
73
73
|
},
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
//
|
|
77
|
-
const
|
|
78
|
-
|
|
76
|
+
// Names that are available on globalThis and should not be bundled as real imports
|
|
77
|
+
const globalNames = new Set<string>([
|
|
78
|
+
// JSX runtime
|
|
79
|
+
'jsx',
|
|
80
|
+
'_jsx',
|
|
81
|
+
'jsxs',
|
|
82
|
+
'_jsxs',
|
|
83
|
+
'Fragment',
|
|
84
|
+
'_Fragment',
|
|
85
|
+
// Prose tag names registered in globalThis
|
|
86
|
+
...Object.values(coreElements).flatMap((el: any) =>
|
|
79
87
|
(el.tags ?? []).map((t: any) => String(t.tagName)),
|
|
80
88
|
),
|
|
81
|
-
);
|
|
89
|
+
]);
|
|
82
90
|
|
|
83
91
|
// Pre-transform: rewrite any import of a known globalThis tag name → const from globalThis.
|
|
84
92
|
// Non-tag imports are left as real imports and bundled normally.
|
|
@@ -106,7 +114,7 @@ const proseGlobalsPlugin: Plugin = {
|
|
|
106
114
|
continue;
|
|
107
115
|
}
|
|
108
116
|
const localName = m[2] ?? m[1]!;
|
|
109
|
-
if (
|
|
117
|
+
if (globalNames.has(localName)) {
|
|
110
118
|
shimLines.push(
|
|
111
119
|
`const ${localName} = globalThis[${JSON.stringify(localName)}];`,
|
|
112
120
|
);
|
|
@@ -11,6 +11,11 @@ const contentRoot = () => ERUDIT.paths.project('content');
|
|
|
11
11
|
|
|
12
12
|
export let builtLinkObject: Record<string, any> | null = null;
|
|
13
13
|
|
|
14
|
+
/** All valid fully-qualified $CONTENT paths — content items, topic parts,
|
|
15
|
+
* public uniques, and internal (underscore) uniques.
|
|
16
|
+
* Used for server-side prose link validation. */
|
|
17
|
+
export let builtValidPaths: Set<string> | null = null;
|
|
18
|
+
|
|
14
19
|
export async function buildGlobalContent() {
|
|
15
20
|
ERUDIT.log.debug.start('Building global content...');
|
|
16
21
|
|
|
@@ -22,8 +27,9 @@ export async function buildGlobalContent() {
|
|
|
22
27
|
return;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
const linkObject = await buildLinkObject();
|
|
30
|
+
const { linkObject, validPaths } = await buildLinkObject();
|
|
26
31
|
builtLinkObject = linkObject;
|
|
32
|
+
builtValidPaths = validPaths;
|
|
27
33
|
|
|
28
34
|
const linkTypes = linkObjectToTypes(linkObject);
|
|
29
35
|
writeFileSync(
|
|
@@ -120,6 +126,7 @@ ${body}
|
|
|
120
126
|
*/
|
|
121
127
|
async function buildLinkObject() {
|
|
122
128
|
const linkTree: any = {};
|
|
129
|
+
const validPaths = new Set<string>();
|
|
123
130
|
|
|
124
131
|
await ERUDIT.contentNav.walk((navItem) => {
|
|
125
132
|
// Navigate to the correct position in the tree based on the full path
|
|
@@ -161,6 +168,11 @@ ${jsdoc}
|
|
|
161
168
|
navItem.contentRelPath,
|
|
162
169
|
),
|
|
163
170
|
};
|
|
171
|
+
|
|
172
|
+
validPaths.add(navItem.fullId);
|
|
173
|
+
for (const name of getAllUniqueNames(moduleContent)) {
|
|
174
|
+
validPaths.add(`${navItem.fullId}/$${name}`);
|
|
175
|
+
}
|
|
164
176
|
} else if (navItem.type === 'topic') {
|
|
165
177
|
const pathToTopicFile = ERUDIT.paths.project(
|
|
166
178
|
`content/${navItem.contentRelPath}/topic.ts`,
|
|
@@ -183,6 +195,8 @@ ${jsdoc}
|
|
|
183
195
|
`.trim(),
|
|
184
196
|
};
|
|
185
197
|
|
|
198
|
+
validPaths.add(navItem.fullId);
|
|
199
|
+
|
|
186
200
|
for (const part of topicParts) {
|
|
187
201
|
try {
|
|
188
202
|
const pathToFile = ERUDIT.paths.project(
|
|
@@ -210,6 +224,11 @@ ${jsdoc}
|
|
|
210
224
|
navItem.contentRelPath,
|
|
211
225
|
),
|
|
212
226
|
};
|
|
227
|
+
|
|
228
|
+
validPaths.add(`${navItem.fullId}/${part}`);
|
|
229
|
+
for (const name of getAllUniqueNames(partContent)) {
|
|
230
|
+
validPaths.add(`${navItem.fullId}/${part}/$${name}`);
|
|
231
|
+
}
|
|
213
232
|
} catch {}
|
|
214
233
|
}
|
|
215
234
|
} else {
|
|
@@ -233,10 +252,12 @@ ${jsdoc}
|
|
|
233
252
|
*/
|
|
234
253
|
`.trim(),
|
|
235
254
|
};
|
|
255
|
+
|
|
256
|
+
validPaths.add(navItem.fullId);
|
|
236
257
|
}
|
|
237
258
|
});
|
|
238
259
|
|
|
239
|
-
return linkTree;
|
|
260
|
+
return { linkObject: linkTree, validPaths };
|
|
240
261
|
}
|
|
241
262
|
|
|
242
263
|
function tryGetTitle(moduleContent: string) {
|
|
@@ -254,6 +275,25 @@ function jsdocLines(lines: any[]) {
|
|
|
254
275
|
.join('\n');
|
|
255
276
|
}
|
|
256
277
|
|
|
278
|
+
/** Returns ALL unique names from a module — both public and internal. Used to
|
|
279
|
+
* populate builtValidPaths for server-side prose link validation. */
|
|
280
|
+
function getAllUniqueNames(moduleContent: string): string[] {
|
|
281
|
+
const uniquesMatch = moduleContent.match(/uniques:\s*\{([^}]*)\}/s);
|
|
282
|
+
if (!uniquesMatch) return [];
|
|
283
|
+
|
|
284
|
+
const names: string[] = [];
|
|
285
|
+
for (const line of uniquesMatch[1]!.split('\n')) {
|
|
286
|
+
if (line.trim().startsWith('//')) continue;
|
|
287
|
+
|
|
288
|
+
const pairMatch =
|
|
289
|
+
line.match(/\[['"](.*?)['"]\]:\s*(\w+)/) ||
|
|
290
|
+
line.match(/['"](.*?)['"]:\s*(\w+)/) ||
|
|
291
|
+
line.match(/(\w+):\s*(\w+)/);
|
|
292
|
+
if (pairMatch) names.push(pairMatch[1]!);
|
|
293
|
+
}
|
|
294
|
+
return names;
|
|
295
|
+
}
|
|
296
|
+
|
|
257
297
|
function tryGetUniquesObject(
|
|
258
298
|
moduleContent: string,
|
|
259
299
|
pathToFile: string,
|
|
@@ -278,7 +318,7 @@ function tryGetUniquesObject(
|
|
|
278
318
|
continue;
|
|
279
319
|
}
|
|
280
320
|
|
|
281
|
-
// Skip uniques starting with underscore
|
|
321
|
+
// Skip uniques starting with underscore (internal — excluded from $CONTENT types)
|
|
282
322
|
if (line.trim().startsWith('_')) {
|
|
283
323
|
continue;
|
|
284
324
|
}
|
|
@@ -230,7 +230,7 @@ async function resolveUniqueEntries(
|
|
|
230
230
|
const results = await Promise.all(
|
|
231
231
|
unique.map(async ({ contentFullId, uniqueName }) => {
|
|
232
232
|
const dbUnique = await ERUDIT.db.query.contentUniques.findFirst({
|
|
233
|
-
columns: { title: true, prose: true },
|
|
233
|
+
columns: { title: true, prose: true, contentProseType: true },
|
|
234
234
|
where: and(
|
|
235
235
|
eq(ERUDIT.db.schema.contentUniques.contentFullId, contentFullId),
|
|
236
236
|
eq(ERUDIT.db.schema.contentUniques.uniqueName, uniqueName),
|
|
@@ -239,15 +239,24 @@ async function resolveUniqueEntries(
|
|
|
239
239
|
|
|
240
240
|
if (!dbUnique) return null;
|
|
241
241
|
|
|
242
|
-
const
|
|
242
|
+
const navNode = ERUDIT.contentNav.getNodeOrThrow(contentFullId);
|
|
243
243
|
const schemaName = dbUnique.prose.schema.name;
|
|
244
244
|
|
|
245
245
|
if (!schemaName) return null;
|
|
246
246
|
|
|
247
|
+
const link =
|
|
248
|
+
navNode.type === 'topic'
|
|
249
|
+
? PAGES.topic(
|
|
250
|
+
dbUnique.contentProseType as any,
|
|
251
|
+
navNode.shortId,
|
|
252
|
+
dbUnique.prose.id,
|
|
253
|
+
)
|
|
254
|
+
: PAGES.page(navNode.shortId, dbUnique.prose.id);
|
|
255
|
+
|
|
247
256
|
return {
|
|
248
257
|
name: uniqueName,
|
|
249
258
|
title: dbUnique.title ?? undefined,
|
|
250
|
-
link
|
|
259
|
+
link,
|
|
251
260
|
schemaName,
|
|
252
261
|
} satisfies ContentDepUnique;
|
|
253
262
|
}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { sql } from 'drizzle-orm';
|
|
3
3
|
import type { ContentProseType } from '@erudit-js/core/content/prose';
|
|
4
|
-
import {
|
|
4
|
+
import { builtValidPaths } from '../../global/build';
|
|
5
5
|
import type {
|
|
6
6
|
ContentLinks,
|
|
7
7
|
ContentLinkUsage,
|
|
@@ -148,25 +148,11 @@ function filterTargetMap(
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
function globalContentToNavNode(globalContentPath: string) {
|
|
151
|
-
// Validate the full path (including any $unique suffix) against the
|
|
152
|
-
//
|
|
153
|
-
// names
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
let cursor: any = builtLinkObject;
|
|
157
|
-
let valid = true;
|
|
158
|
-
|
|
159
|
-
for (const part of parts) {
|
|
160
|
-
if (!cursor || typeof cursor !== 'object' || !(part in cursor)) {
|
|
161
|
-
valid = false;
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
cursor = cursor[part];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (!valid) {
|
|
168
|
-
throw new Error(`Path not found in \$CONTENT: ${globalContentPath}`);
|
|
169
|
-
}
|
|
151
|
+
// Validate the full path (including any $unique suffix) against the complete
|
|
152
|
+
// set of known valid paths built from source files. This catches broken
|
|
153
|
+
// unique names and content paths before we ever touch the nav tree.
|
|
154
|
+
if (builtValidPaths && !builtValidPaths.has(globalContentPath)) {
|
|
155
|
+
throw new Error(`Path not found in \$CONTENT: ${globalContentPath}`);
|
|
170
156
|
}
|
|
171
157
|
|
|
172
158
|
const parts = globalContentPath.split('/');
|
|
@@ -175,8 +161,7 @@ function globalContentToNavNode(globalContentPath: string) {
|
|
|
175
161
|
parts.pop();
|
|
176
162
|
}
|
|
177
163
|
|
|
178
|
-
//
|
|
179
|
-
// isn't found the last segment must be a topic part — fall back to parent.
|
|
164
|
+
// If the exact node isn't found the last segment is a topic part — fall back to parent.
|
|
180
165
|
return (
|
|
181
166
|
ERUDIT.contentNav.getNode(parts.join('/')) ??
|
|
182
167
|
ERUDIT.contentNav.getNodeOrThrow(parts.slice(0, -1).join('/'))
|