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.
@@ -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 defaultImportName = (elementName: string) => `app_${elementName}`;
8
+ const importName = (i: number, name: string) => `app_${i}_${toJsSlug(name)}`;
8
9
 
9
10
  const apps: Record<string, string> = {};
10
11
 
11
- for (const elementData of elementsData) {
12
+ for (let i = 0; i < elementsData.length; i++) {
13
+ const elementData = elementsData[i]!;
12
14
  if (elementData.absAppPath) {
13
- apps[defaultImportName(elementData.name)] = elementData.absAppPath;
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 defaultImportName = (type: 'core' | 'global', elementName: string) =>
9
- `${type}_${elementName}`;
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 (const elementData of elementsData) {
15
- cores[defaultImportName('core', elementData.name)] =
16
- elementData.absCorePath;
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[defaultImportName('global', elementData.name)] =
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-dev.1",
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-dev.1",
28
- "@erudit-js/core": "4.3.0-dev.1",
29
- "@erudit-js/prose": "4.3.0-dev.1",
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.10",
31
+ "@floating-ui/vue": "^1.1.11",
32
32
  "tsprose": "^1.0.1",
33
- "@tailwindcss/vite": "^4.2.0",
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.0",
48
- "vue": "^3.5.28",
49
- "vue-router": "^5.0.3",
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 preamble that destructures all ERUDIT_GLOBAL keys into local vars. */
20
- let eruditGlobalPreamble: string | undefined;
19
+ /** Cached list of valid identifier keys from ERUDIT_GLOBAL. */
20
+ let cachedGlobalKeys: string[] | undefined;
21
21
 
22
- function getEruditGlobalPreamble(): string {
23
- if (eruditGlobalPreamble !== undefined) return eruditGlobalPreamble;
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
- eruditGlobalPreamble = '';
28
- return eruditGlobalPreamble;
27
+ cachedGlobalKeys = [];
28
+ return cachedGlobalKeys;
29
29
  }
30
30
 
31
- const names = Object.keys(eg).filter((n) => /^[a-zA-Z_$]\w*$/.test(n));
32
- if (names.length === 0) {
33
- eruditGlobalPreamble = '';
34
- return eruditGlobalPreamble;
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
- eruditGlobalPreamble =
38
- 'var { ' + names.join(', ') + ' } = globalThis.ERUDIT_GLOBAL;\n';
39
- return eruditGlobalPreamble;
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 = getEruditGlobalPreamble();
128
+ const preamble = buildFilteredPreamble(code);
71
129
  if (preamble) {
72
130
  code = preamble + code;
73
131
  }