erudit 4.3.0-dev.1 → 4.3.1-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.
Files changed (34) hide show
  1. package/app/composables/formatText.ts +9 -100
  2. package/app/composables/og.ts +58 -16
  3. package/app/pages/contributor/[contributorId].vue +1 -0
  4. package/app/pages/contributors.vue +1 -0
  5. package/app/pages/sponsors.vue +1 -0
  6. package/app/plugins/prerender.server.ts +1 -0
  7. package/modules/erudit/setup/elements/appTemplate.ts +5 -3
  8. package/modules/erudit/setup/elements/globalTemplate.ts +7 -6
  9. package/modules/erudit/setup/problemChecks/template.ts +7 -3
  10. package/modules/erudit/setup/toJsSlug.ts +19 -0
  11. package/nuxt.config.ts +1 -1
  12. package/package.json +14 -12
  13. package/server/api/prerender/ogImages.ts +46 -0
  14. package/server/api/problemScript/[...problemScriptPath].ts +18 -0
  15. package/server/erudit/importer.ts +72 -14
  16. package/server/erudit/ogImage/fonts/NotoSans-Bold.ttf +0 -0
  17. package/server/erudit/ogImage/fonts/NotoSans-Regular.ttf +0 -0
  18. package/server/erudit/ogImage/formatText.ts +12 -0
  19. package/server/erudit/ogImage/icons.ts +22 -0
  20. package/server/erudit/ogImage/logotype.ts +51 -0
  21. package/server/erudit/ogImage/render.ts +90 -0
  22. package/server/erudit/ogImage/shared.ts +320 -0
  23. package/server/erudit/ogImage/templates/content.ts +200 -0
  24. package/server/erudit/ogImage/templates/index.ts +138 -0
  25. package/server/erudit/ogImage/templates/sitePage.ts +110 -0
  26. package/server/erudit/staticFile.ts +3 -0
  27. package/server/routes/og/content/[...contentTypePath].ts +126 -0
  28. package/server/routes/og/site/contributor/[contributorId].ts +60 -0
  29. package/server/routes/og/site/contributors.png.ts +38 -0
  30. package/server/routes/og/site/index.png.ts +51 -0
  31. package/server/routes/og/site/sponsors.png.ts +38 -0
  32. package/shared/utils/formatText.ts +73 -0
  33. package/app/formatters/ru.ts +0 -14
  34. package/public/og.png +0 -0
@@ -1,100 +1,9 @@
1
- import type { FormatTextState, FormatText } from '@erudit-js/core/formatText';
2
-
3
- type LanguageFormatText = (text: string) => string;
4
-
5
- const formatTextLoaders: Partial<
6
- Record<LanguageCode, () => Promise<{ default: LanguageFormatText }>>
7
- > = {
8
- ru: () => import('../formatters/ru'),
9
- };
10
-
11
- export let formatText: FormatText;
12
-
13
- export async function initFormatText() {
14
- const languageCode = ERUDIT.config.language.current;
15
-
16
- const formatTextLoader =
17
- languageCode in formatTextLoaders
18
- ? formatTextLoaders[languageCode]
19
- : undefined;
20
-
21
- let languageFormatText: LanguageFormatText = (text) => text;
22
- if (formatTextLoader) {
23
- languageFormatText = (await formatTextLoader()).default;
24
- }
25
-
26
- function _formatText(text: string, state?: FormatTextState): string;
27
- function _formatText(text: undefined, state?: FormatTextState): undefined;
28
- function _formatText(
29
- text?: string,
30
- state?: FormatTextState,
31
- ): string | undefined;
32
- function _formatText(
33
- text?: string,
34
- state?: FormatTextState,
35
- ): string | undefined {
36
- if (text === undefined) {
37
- return text;
38
- }
39
-
40
- //
41
- // Normalize spacing (new lines, spaces)
42
- //
43
-
44
- {
45
- text = text
46
- .trim()
47
- .replace(/\r\n/gm, '\n')
48
- .replace(/\n{3,}/gm, '\n\n')
49
- .replace(/[ \t]+/gm, ' ');
50
- }
51
-
52
- //
53
- // Normalize dashes
54
- //
55
-
56
- {
57
- text = text.replace(/(^| )--($| )/gm, '$1—$2');
58
- }
59
-
60
- //
61
- // Normalize quotes
62
- //
63
-
64
- {
65
- const quoteSymbols: [string, string] = (() => {
66
- switch (languageCode) {
67
- case 'ru':
68
- return ['«', '»'];
69
- default:
70
- return ['“', '”'];
71
- }
72
- })();
73
-
74
- let quoteOpen = state?.quote === 'opened';
75
- text = text.replaceAll(/"/gm, () => {
76
- quoteOpen = !quoteOpen;
77
- if (state) {
78
- state.quote = quoteOpen ? 'opened' : 'closed';
79
- }
80
- return quoteOpen ? quoteSymbols[0] : quoteSymbols[1];
81
- });
82
- }
83
-
84
- //
85
- // Normalize ellipsis
86
- //
87
-
88
- {
89
- text = text.replace(/\.{3}/gm, '…');
90
- }
91
-
92
- //
93
- // Language-specific formatting
94
- //
95
-
96
- return languageFormatText(text);
97
- }
98
-
99
- formatText = _formatText;
100
- }
1
+ import type { FormatText } from '@erudit-js/core/formatText';
2
+ import { createFormatTextFn } from '../../shared/utils/formatText';
3
+
4
+ export let formatText: FormatText;
5
+
6
+ export async function initFormatText() {
7
+ const languageCode = ERUDIT.config.language.current;
8
+ formatText = createFormatTextFn(languageCode);
9
+ }
@@ -15,22 +15,18 @@ export function initOgSiteName() {
15
15
  }
16
16
 
17
17
  export function initOgImage() {
18
- const withSiteUrl = useSiteUrl();
19
-
20
- const fallbackOgImage = {
21
- src: eruditPublic('og.png'),
22
- width: 500,
23
- height: 500,
24
- };
25
-
26
- const ogImage = ERUDIT.config.seo?.image || fallbackOgImage;
27
- useSeoMeta({
28
- ogImage: {
29
- url: withSiteUrl(ogImage.src),
30
- width: ogImage.width,
31
- height: ogImage.height,
32
- },
33
- });
18
+ const ogImageConfig = ERUDIT.config.seo?.ogImage;
19
+
20
+ if (ogImageConfig?.type === 'manual') {
21
+ const withSiteUrl = useSiteUrl();
22
+ useSeoMeta({
23
+ ogImage: {
24
+ url: withSiteUrl(ogImageConfig.src),
25
+ width: ogImageConfig.width,
26
+ height: ogImageConfig.height,
27
+ },
28
+ });
29
+ }
34
30
  }
35
31
 
36
32
  export function useIndexSeo(indexPage: IndexPage) {
@@ -39,12 +35,25 @@ export function useIndexSeo(indexPage: IndexPage) {
39
35
  description: indexPage.seo?.description || indexPage.description,
40
36
  urlPath: '/',
41
37
  });
38
+
39
+ const ogImageConfig = ERUDIT.config.seo?.ogImage;
40
+ if (ogImageConfig?.type === 'auto') {
41
+ const withSiteUrl = useSiteUrl();
42
+ useSeoMeta({
43
+ ogImage: {
44
+ url: withSiteUrl('/og/site/index.png'),
45
+ width: 1200,
46
+ height: 630,
47
+ },
48
+ });
49
+ }
42
50
  }
43
51
 
44
52
  export function useStandartSeo(args: {
45
53
  title: string;
46
54
  description?: string;
47
55
  urlPath: string;
56
+ ogImagePath?: string;
48
57
  }) {
49
58
  const seoSiteTitle =
50
59
  ERUDIT.config.seo?.siteTitle ||
@@ -58,6 +67,20 @@ export function useStandartSeo(args: {
58
67
  description: args.description,
59
68
  urlPath: args.urlPath,
60
69
  });
70
+
71
+ if (args.ogImagePath) {
72
+ const ogImageConfig = ERUDIT.config.seo?.ogImage;
73
+ if (ogImageConfig?.type === 'auto') {
74
+ const withSiteUrl = useSiteUrl();
75
+ useSeoMeta({
76
+ ogImage: {
77
+ url: withSiteUrl(args.ogImagePath),
78
+ width: 1200,
79
+ height: 630,
80
+ },
81
+ });
82
+ }
83
+ }
61
84
  }
62
85
 
63
86
  export async function useContentSeo(args: {
@@ -100,6 +123,25 @@ export async function useContentSeo(args: {
100
123
 
101
124
  setupSeo(baseSeo);
102
125
 
126
+ // Auto-generated OG image for content
127
+ // Manual OG image is handled globally by initOgImage() in app.vue
128
+ const ogImageConfig = ERUDIT.config.seo?.ogImage;
129
+ if (ogImageConfig?.type === 'auto') {
130
+ const withSiteUrl = useSiteUrl();
131
+ const ogTypePart =
132
+ args.contentTypePath.type === 'topic'
133
+ ? args.contentTypePath.topicPart
134
+ : args.contentTypePath.type;
135
+ const ogPath = `/og/content/${ogTypePart}/${args.contentTypePath.contentId}.png`;
136
+ useSeoMeta({
137
+ ogImage: {
138
+ url: withSiteUrl(ogPath),
139
+ width: 1200,
140
+ height: 630,
141
+ },
142
+ });
143
+ }
144
+
103
145
  //
104
146
  // SEO snippets
105
147
  //
@@ -45,6 +45,7 @@ useStandartSeo({
45
45
  pageContributor.displayName || pageContributor.id,
46
46
  ),
47
47
  urlPath: PAGES.contributor(contributorId.value),
48
+ ogImagePath: `/og/site/contributor/${contributorId.value}.png`,
48
49
  });
49
50
  </script>
50
51
 
@@ -33,6 +33,7 @@ useStandartSeo({
33
33
  title: phrase.contributors,
34
34
  description: phrase.contributors_description,
35
35
  urlPath: PAGES.contributors,
36
+ ogImagePath: '/og/site/contributors.png',
36
37
  });
37
38
  </script>
38
39
 
@@ -24,6 +24,7 @@ useStandartSeo({
24
24
  title: phrase.sponsors,
25
25
  description: phrase.sponsors_description,
26
26
  urlPath: PAGES.sponsors,
27
+ ogImagePath: '/og/site/sponsors.png',
27
28
  });
28
29
  </script>
29
30
 
@@ -25,6 +25,7 @@ export default defineNuxtPlugin({
25
25
  '/api/prerender/content',
26
26
  '/api/prerender/quotes',
27
27
  '/api/prerender/news',
28
+ '/api/prerender/ogImages',
28
29
  ];
29
30
 
30
31
  for (const provider of routeProviders) {
@@ -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/nuxt.config.ts CHANGED
@@ -53,7 +53,7 @@ export default defineNuxtConfig({
53
53
  rollupConfig: {
54
54
  // Prevent inlining some packages
55
55
  external(source) {
56
- const ignore = ['jiti', 'tsprose'];
56
+ const ignore = ['jiti', 'tsprose', '@resvg/resvg-js'];
57
57
 
58
58
  for (const ignoreItem of ignore) {
59
59
  if (source.includes(ignoreItem)) {
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.1-dev.1",
4
4
  "type": "module",
5
5
  "description": "🤓 CMS for perfect educational sites.",
6
6
  "license": "MIT",
@@ -24,29 +24,31 @@
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.1-dev.1",
28
+ "@erudit-js/core": "4.3.1-dev.1",
29
+ "@erudit-js/prose": "4.3.1-dev.1",
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",
34
- "better-sqlite3": "^12.6.2",
33
+ "@tailwindcss/vite": "^4.2.1",
34
+ "better-sqlite3": "^12.8.0",
35
35
  "chokidar": "^5.0.0",
36
36
  "consola": "^3.4.2",
37
37
  "drizzle-kit": "^0.31.9",
38
38
  "drizzle-orm": "^0.45.1",
39
- "esbuild": "^0.27.3",
39
+ "esbuild": "^0.27.4",
40
40
  "flexsearch": "^0.8.212",
41
41
  "glob": "^13.0.6",
42
42
  "image-size": "^2.0.2",
43
+ "@resvg/resvg-js": "^2.6.2",
44
+ "satori": "^0.25.0",
43
45
  "jiti": "^2.6.1",
44
- "nuxt": "4.3.1",
46
+ "nuxt": "4.4.2",
45
47
  "nuxt-my-icons": "1.2.2",
46
48
  "perfect-debounce": "^2.1.0",
47
- "tailwindcss": "^4.2.0",
48
- "vue": "^3.5.28",
49
- "vue-router": "^5.0.3",
49
+ "tailwindcss": "^4.2.1",
50
+ "vue": "latest",
51
+ "vue-router": "latest",
50
52
  "ts-xor": "^1.3.0"
51
53
  },
52
54
  "devDependencies": {
@@ -0,0 +1,46 @@
1
+ export default defineEventHandler(async () => {
2
+ const ogImageConfig = ERUDIT.config.public.seo?.ogImage;
3
+ if (ogImageConfig?.type !== 'auto') {
4
+ return [];
5
+ }
6
+
7
+ const routes: string[] = [];
8
+
9
+ // Index page
10
+ routes.push('/og/site/index.png');
11
+
12
+ // Contributors page
13
+ if (ERUDIT.config.public.contributors?.enabled) {
14
+ routes.push('/og/site/contributors.png');
15
+
16
+ const dbContributors = await ERUDIT.db.query.contributors.findMany({
17
+ columns: { contributorId: true },
18
+ });
19
+
20
+ for (const dbContributor of dbContributors) {
21
+ routes.push(`/og/site/contributor/${dbContributor.contributorId}.png`);
22
+ }
23
+ }
24
+
25
+ // Sponsors page
26
+ if (ERUDIT.config.public.sponsors?.enabled) {
27
+ routes.push('/og/site/sponsors.png');
28
+ }
29
+
30
+ // Content pages
31
+ for (const navNode of ERUDIT.contentNav.id2Node.values()) {
32
+ if (navNode.type === 'topic') {
33
+ const topicParts = await ERUDIT.repository.content.topicParts(
34
+ navNode.fullId,
35
+ );
36
+
37
+ for (const part of topicParts) {
38
+ routes.push(`/og/content/${part}/${navNode.shortId}.png`);
39
+ }
40
+ } else {
41
+ routes.push(`/og/content/${navNode.type}/${navNode.shortId}.png`);
42
+ }
43
+ }
44
+
45
+ return routes;
46
+ });
@@ -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
  }
@@ -0,0 +1,12 @@
1
+ import type { FormatText } from '@erudit-js/core/formatText';
2
+ import { createFormatTextFn } from '../../../shared/utils/formatText';
3
+
4
+ let _ogFormatText: FormatText | undefined;
5
+
6
+ export function ogFormatText(text: string): string {
7
+ if (!_ogFormatText) {
8
+ const languageCode = ERUDIT.config.public.language.current;
9
+ _ogFormatText = createFormatTextFn(languageCode);
10
+ }
11
+ return _ogFormatText(text);
12
+ }
@@ -0,0 +1,22 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { ICONS } from '../../../shared/utils/icons';
3
+
4
+ const ICON_MAP: Record<string, string> = {
5
+ ...ICONS,
6
+ contributors: 'users',
7
+ contributor: 'user',
8
+ sponsors: 'diamond',
9
+ };
10
+
11
+ const iconCache = new Map<string, string>();
12
+
13
+ export function getIconSvg(contentType: string): string {
14
+ const cached = iconCache.get(contentType);
15
+ if (cached) return cached;
16
+
17
+ const iconName = ICON_MAP[contentType] || 'lines';
18
+ const iconPath = ERUDIT.paths.erudit('app/assets/icons', iconName + '.svg');
19
+ const svg = readFileSync(iconPath, 'utf-8');
20
+ iconCache.set(contentType, svg);
21
+ return svg;
22
+ }