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.
- package/app/composables/formatText.ts +9 -100
- package/app/composables/og.ts +58 -16
- package/app/pages/contributor/[contributorId].vue +1 -0
- package/app/pages/contributors.vue +1 -0
- package/app/pages/sponsors.vue +1 -0
- package/app/plugins/prerender.server.ts +1 -0
- 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/nuxt.config.ts +1 -1
- package/package.json +14 -12
- package/server/api/prerender/ogImages.ts +46 -0
- package/server/api/problemScript/[...problemScriptPath].ts +18 -0
- package/server/erudit/importer.ts +72 -14
- package/server/erudit/ogImage/fonts/NotoSans-Bold.ttf +0 -0
- package/server/erudit/ogImage/fonts/NotoSans-Regular.ttf +0 -0
- package/server/erudit/ogImage/formatText.ts +12 -0
- package/server/erudit/ogImage/icons.ts +22 -0
- package/server/erudit/ogImage/logotype.ts +51 -0
- package/server/erudit/ogImage/render.ts +90 -0
- package/server/erudit/ogImage/shared.ts +320 -0
- package/server/erudit/ogImage/templates/content.ts +200 -0
- package/server/erudit/ogImage/templates/index.ts +138 -0
- package/server/erudit/ogImage/templates/sitePage.ts +110 -0
- package/server/erudit/staticFile.ts +3 -0
- package/server/routes/og/content/[...contentTypePath].ts +126 -0
- package/server/routes/og/site/contributor/[contributorId].ts +60 -0
- package/server/routes/og/site/contributors.png.ts +38 -0
- package/server/routes/og/site/index.png.ts +51 -0
- package/server/routes/og/site/sponsors.png.ts +38 -0
- package/shared/utils/formatText.ts +73 -0
- package/app/formatters/ru.ts +0 -14
- package/public/og.png +0 -0
|
@@ -1,100 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
}
|
package/app/composables/og.ts
CHANGED
|
@@ -15,22 +15,18 @@ export function initOgSiteName() {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function initOgImage() {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
//
|
package/app/pages/sponsors.vue
CHANGED
|
@@ -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/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.
|
|
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.
|
|
28
|
-
"@erudit-js/core": "4.3.
|
|
29
|
-
"@erudit-js/prose": "4.3.
|
|
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.
|
|
31
|
+
"@floating-ui/vue": "^1.1.11",
|
|
32
32
|
"tsprose": "^1.0.1",
|
|
33
|
-
"@tailwindcss/vite": "^4.2.
|
|
34
|
-
"better-sqlite3": "^12.
|
|
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.
|
|
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.
|
|
46
|
+
"nuxt": "4.4.2",
|
|
45
47
|
"nuxt-my-icons": "1.2.2",
|
|
46
48
|
"perfect-debounce": "^2.1.0",
|
|
47
|
-
"tailwindcss": "^4.2.
|
|
48
|
-
"vue": "
|
|
49
|
-
"vue-router": "
|
|
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
|
|
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
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|