erudit 4.3.0-dev.1 → 4.3.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/modules/erudit/setup/elements/appTemplate.ts +5 -3
- package/modules/erudit/setup/elements/globalTemplate.ts +7 -6
- package/modules/erudit/setup/problemChecks/template.ts +7 -3
- package/modules/erudit/setup/toJsSlug.ts +19 -0
- package/package.json +9 -9
- package/server/api/problemScript/[...problemScriptPath].ts +18 -0
- package/server/erudit/importer.ts +72 -14
|
@@ -2,15 +2,17 @@ import type { Nuxt } from 'nuxt/schema';
|
|
|
2
2
|
import { addTemplate } from 'nuxt/kit';
|
|
3
3
|
|
|
4
4
|
import type { ElementData } from './shared';
|
|
5
|
+
import { toJsSlug } from '../toJsSlug';
|
|
5
6
|
|
|
6
7
|
export function createAppTemplate(nuxt: Nuxt, elementsData: ElementData[]) {
|
|
7
|
-
const
|
|
8
|
+
const importName = (i: number, name: string) => `app_${i}_${toJsSlug(name)}`;
|
|
8
9
|
|
|
9
10
|
const apps: Record<string, string> = {};
|
|
10
11
|
|
|
11
|
-
for (
|
|
12
|
+
for (let i = 0; i < elementsData.length; i++) {
|
|
13
|
+
const elementData = elementsData[i]!;
|
|
12
14
|
if (elementData.absAppPath) {
|
|
13
|
-
apps[
|
|
15
|
+
apps[importName(i, elementData.name)] = elementData.absAppPath;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -3,20 +3,21 @@ import type { Nuxt } from 'nuxt/schema';
|
|
|
3
3
|
import { addTemplate } from 'nuxt/kit';
|
|
4
4
|
|
|
5
5
|
import type { ElementData } from './shared';
|
|
6
|
+
import { toJsSlug } from '../toJsSlug';
|
|
6
7
|
|
|
7
8
|
export function createGlobalTemplate(nuxt: Nuxt, elementsData: ElementData[]) {
|
|
8
|
-
const
|
|
9
|
-
`${type}_${
|
|
9
|
+
const importName = (type: 'core' | 'global', i: number, name: string) =>
|
|
10
|
+
`${type}_${i}_${toJsSlug(name)}`;
|
|
10
11
|
|
|
11
12
|
const cores: Record<string, string> = {};
|
|
12
13
|
const globals: Record<string, string> = {};
|
|
13
14
|
|
|
14
|
-
for (
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
for (let i = 0; i < elementsData.length; i++) {
|
|
16
|
+
const elementData = elementsData[i]!;
|
|
17
|
+
cores[importName('core', i, elementData.name)] = elementData.absCorePath;
|
|
17
18
|
|
|
18
19
|
if (existsSync(elementData.absDirectory + '/_global.ts')) {
|
|
19
|
-
globals[
|
|
20
|
+
globals[importName('global', i, elementData.name)] =
|
|
20
21
|
elementData.absDirectory + '/_global.ts';
|
|
21
22
|
}
|
|
22
23
|
}
|
|
@@ -2,23 +2,27 @@ import type { Nuxt } from 'nuxt/schema';
|
|
|
2
2
|
import { addTemplate } from 'nuxt/kit';
|
|
3
3
|
|
|
4
4
|
import type { ResolvedProblemCheck } from './shared';
|
|
5
|
+
import { toJsSlug } from '../toJsSlug';
|
|
5
6
|
|
|
6
7
|
export function createTemplate(
|
|
7
8
|
nuxt: Nuxt,
|
|
8
9
|
problemChecks: ResolvedProblemCheck[],
|
|
9
10
|
) {
|
|
11
|
+
const importName = (i: number, name: string) =>
|
|
12
|
+
`check_${i}_${toJsSlug(name)}`;
|
|
13
|
+
|
|
10
14
|
const template = `
|
|
11
15
|
import type { ProblemCheckers } from '@erudit-js/core/problemCheck';
|
|
12
16
|
|
|
13
17
|
${problemChecks
|
|
14
18
|
.map(
|
|
15
|
-
(check) =>
|
|
16
|
-
`import ${check.name} from '${check.absPath.replace(/\.(ts|js)$/, '')}';`,
|
|
19
|
+
(check, i) =>
|
|
20
|
+
`import ${importName(i, check.name)} from '${check.absPath.replace(/\.(ts|js)$/, '')}';`,
|
|
17
21
|
)
|
|
18
22
|
.join('\n')}
|
|
19
23
|
|
|
20
24
|
export const problemCheckers: ProblemCheckers = {
|
|
21
|
-
${problemChecks.map((check) => `${check.name},`).join('\n ')}
|
|
25
|
+
${problemChecks.map((check, i) => `${JSON.stringify(check.name)}: ${importName(i, check.name)},`).join('\n ')}
|
|
22
26
|
}
|
|
23
27
|
`.trim();
|
|
24
28
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a name string into a valid JavaScript identifier segment.
|
|
3
|
+
* Replaces non-alphanumeric/underscore/$ characters with `_`.
|
|
4
|
+
* Prefixes with `_` if the result starts with a digit.
|
|
5
|
+
* Returns `_` if the result is empty.
|
|
6
|
+
*/
|
|
7
|
+
export function toJsSlug(name: string): string {
|
|
8
|
+
let slug = name.replace(/[^a-zA-Z0-9_$]/g, '_');
|
|
9
|
+
|
|
10
|
+
if (/^[0-9]/.test(slug)) {
|
|
11
|
+
slug = '_' + slug;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!slug) {
|
|
15
|
+
slug = '_';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return slug;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "erudit",
|
|
3
|
-
"version": "4.3.0
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "🤓 CMS for perfect educational sites.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@erudit-js/cli": "4.3.0
|
|
28
|
-
"@erudit-js/core": "4.3.0
|
|
29
|
-
"@erudit-js/prose": "4.3.0
|
|
27
|
+
"@erudit-js/cli": "4.3.0",
|
|
28
|
+
"@erudit-js/core": "4.3.0",
|
|
29
|
+
"@erudit-js/prose": "4.3.0",
|
|
30
30
|
"unslash": "^2.0.0",
|
|
31
|
-
"@floating-ui/vue": "^1.1.
|
|
31
|
+
"@floating-ui/vue": "^1.1.11",
|
|
32
32
|
"tsprose": "^1.0.1",
|
|
33
|
-
"@tailwindcss/vite": "^4.2.
|
|
33
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
34
34
|
"better-sqlite3": "^12.6.2",
|
|
35
35
|
"chokidar": "^5.0.0",
|
|
36
36
|
"consola": "^3.4.2",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"nuxt": "4.3.1",
|
|
45
45
|
"nuxt-my-icons": "1.2.2",
|
|
46
46
|
"perfect-debounce": "^2.1.0",
|
|
47
|
-
"tailwindcss": "^4.2.
|
|
48
|
-
"vue": "
|
|
49
|
-
"vue-router": "
|
|
47
|
+
"tailwindcss": "^4.2.1",
|
|
48
|
+
"vue": "latest",
|
|
49
|
+
"vue-router": "latest",
|
|
50
50
|
"ts-xor": "^1.3.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
@@ -312,10 +312,28 @@ function normalizeEruditGlobals(code: string): string {
|
|
|
312
312
|
code = code.replace(/_jsx\d*\b/g, 'jsx');
|
|
313
313
|
code = code.replace(/_Fragment\d*\b/g, 'Fragment');
|
|
314
314
|
|
|
315
|
+
// Collect names already declared via real imports that esbuild kept
|
|
316
|
+
// (i.e. non-global imports). These must NOT appear in the preamble to avoid
|
|
317
|
+
// duplicate-identifier errors when a file explicitly imports a global name.
|
|
318
|
+
const declaredByImports = new Set<string>();
|
|
319
|
+
const importPattern = /^import\s+\{([^}]+)\}\s+from\s+/gm;
|
|
320
|
+
let im;
|
|
321
|
+
while ((im = importPattern.exec(code)) !== null) {
|
|
322
|
+
for (const part of im[1]!.split(',')) {
|
|
323
|
+
const trimmed = part.trim();
|
|
324
|
+
if (!trimmed) continue;
|
|
325
|
+
// Handle "X as Y" — the local name Y is the declared identifier
|
|
326
|
+
const asMatch = trimmed.match(/\w+\s+as\s+(\w+)/);
|
|
327
|
+
const name = asMatch ? asMatch[1]! : trimmed.match(/^(\w+)$/)?.[1];
|
|
328
|
+
if (name) declaredByImports.add(name);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
315
332
|
// Detect which ERUDIT_GLOBAL names are actually used in the code
|
|
316
333
|
const allNames = getGlobalNames();
|
|
317
334
|
const usedNames = [...allNames]
|
|
318
335
|
.filter((n) => /^[a-zA-Z_$]\w*$/.test(n) && !n.startsWith('_'))
|
|
336
|
+
.filter((n) => !declaredByImports.has(n))
|
|
319
337
|
.filter((n) => new RegExp('\\b' + n + '\\b').test(code));
|
|
320
338
|
|
|
321
339
|
if (usedNames.length > 0) {
|
|
@@ -16,27 +16,85 @@ export type EruditServerImporter = Jiti['import'];
|
|
|
16
16
|
|
|
17
17
|
export let jiti: Jiti;
|
|
18
18
|
|
|
19
|
-
/** Cached
|
|
20
|
-
let
|
|
19
|
+
/** Cached list of valid identifier keys from ERUDIT_GLOBAL. */
|
|
20
|
+
let cachedGlobalKeys: string[] | undefined;
|
|
21
21
|
|
|
22
|
-
function
|
|
23
|
-
if (
|
|
22
|
+
function getGlobalKeys(): string[] {
|
|
23
|
+
if (cachedGlobalKeys !== undefined) return cachedGlobalKeys;
|
|
24
24
|
|
|
25
25
|
const eg = (globalThis as any).ERUDIT_GLOBAL;
|
|
26
26
|
if (!eg || typeof eg !== 'object') {
|
|
27
|
-
|
|
28
|
-
return
|
|
27
|
+
cachedGlobalKeys = [];
|
|
28
|
+
return cachedGlobalKeys;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
cachedGlobalKeys = Object.keys(eg).filter((n) => /^[a-zA-Z_$]\w*$/.test(n));
|
|
32
|
+
return cachedGlobalKeys;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Collect names already declared in the transpiled code via imports.
|
|
37
|
+
* Jiti transpiles ESM imports to CJS-style interop, so we match patterns like:
|
|
38
|
+
* const/var/let { X, Y } = require(...) — destructured CJS
|
|
39
|
+
* const/var/let X = require(...) — default CJS
|
|
40
|
+
* const/var/let X = ... — interop helpers
|
|
41
|
+
* import { X } from '...' — preserved ESM (if any)
|
|
42
|
+
*/
|
|
43
|
+
function collectDeclaredNames(code: string): Set<string> {
|
|
44
|
+
const declared = new Set<string>();
|
|
45
|
+
|
|
46
|
+
// Destructured require/import: const/var/let { X, Y as Z } = require(...)
|
|
47
|
+
// or: import { X, Y as Z } from '...'
|
|
48
|
+
const destructuredPattern =
|
|
49
|
+
/\b(?:const|let|var)\s+\{([^}]+)\}\s*=\s*require\s*\(|\bimport\s+\{([^}]+)\}\s+from\s+/g;
|
|
50
|
+
let m;
|
|
51
|
+
while ((m = destructuredPattern.exec(code)) !== null) {
|
|
52
|
+
const bindings = m[1] ?? m[2];
|
|
53
|
+
if (!bindings) continue;
|
|
54
|
+
for (const part of bindings.split(',')) {
|
|
55
|
+
const trimmed = part.trim();
|
|
56
|
+
if (!trimmed) continue;
|
|
57
|
+
// Handle "X as Y" (import) or "X: Y" (destructured require)
|
|
58
|
+
const asMatch = trimmed.match(/\w+\s+as\s+(\w+)/);
|
|
59
|
+
if (asMatch) {
|
|
60
|
+
declared.add(asMatch[1]!);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const colonMatch = trimmed.match(/\w+\s*:\s*(\w+)/);
|
|
64
|
+
if (colonMatch) {
|
|
65
|
+
declared.add(colonMatch[1]!);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const nameOnly = trimmed.match(/^(\w+)$/);
|
|
69
|
+
if (nameOnly) declared.add(nameOnly[1]!);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Simple declarations: const/var/let X = require(...) or interop helpers
|
|
74
|
+
const simplePattern = /\b(?:const|let|var)\s+(\w+)\s*=/g;
|
|
75
|
+
let sm;
|
|
76
|
+
while ((sm = simplePattern.exec(code)) !== null) {
|
|
77
|
+
declared.add(sm[1]!);
|
|
35
78
|
}
|
|
36
79
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
80
|
+
return declared;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build a per-file preamble that destructures ERUDIT_GLOBAL keys, skipping
|
|
85
|
+
* any names the file already declares via explicit imports.
|
|
86
|
+
*/
|
|
87
|
+
function buildFilteredPreamble(code: string): string {
|
|
88
|
+
const allKeys = getGlobalKeys();
|
|
89
|
+
if (allKeys.length === 0) return '';
|
|
90
|
+
|
|
91
|
+
const declared = collectDeclaredNames(code);
|
|
92
|
+
const filtered =
|
|
93
|
+
declared.size > 0 ? allKeys.filter((n) => !declared.has(n)) : allKeys;
|
|
94
|
+
|
|
95
|
+
if (filtered.length === 0) return '';
|
|
96
|
+
|
|
97
|
+
return 'var { ' + filtered.join(', ') + ' } = globalThis.ERUDIT_GLOBAL;\n';
|
|
40
98
|
}
|
|
41
99
|
|
|
42
100
|
export async function setupServerImporter() {
|
|
@@ -67,7 +125,7 @@ export async function setupServerImporter() {
|
|
|
67
125
|
// into local variables so bare identifiers resolve correctly.
|
|
68
126
|
//
|
|
69
127
|
if (filename.startsWith(ERUDIT.paths.project() + '/')) {
|
|
70
|
-
const preamble =
|
|
128
|
+
const preamble = buildFilteredPreamble(code);
|
|
71
129
|
if (preamble) {
|
|
72
130
|
code = preamble + code;
|
|
73
131
|
}
|