erudit 4.2.0 → 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/app/components/Prose.vue +2 -0
- package/app/components/aside/major/contentNav/items/ContentNavTopic.vue +12 -1
- package/app/components/aside/major/search/SearchResult.vue +16 -2
- package/app/components/aside/minor/contributor/AsideMinorContributor.vue +7 -1
- package/app/components/main/MainStickyHeader.vue +5 -2
- package/app/components/main/MainStickyHeaderPreamble.vue +5 -2
- package/app/components/main/MainTopicPartPage.vue +3 -2
- package/app/components/main/MainTopicPartSwitch.vue +18 -7
- package/app/components/main/connections/Deps.vue +1 -4
- package/app/components/main/connections/MainConnections.vue +9 -3
- package/app/components/main/contentStats/ItemLastChanged.vue +3 -32
- package/app/components/main/contentStats/MainContentStats.vue +3 -4
- package/app/components/preview/Preview.vue +8 -6
- package/app/components/preview/PreviewScreen.vue +9 -7
- package/app/components/preview/screen/Unique.vue +3 -2
- package/app/composables/ads.ts +1 -1
- package/app/composables/analytics.ts +1 -1
- package/app/composables/lastChanged.ts +38 -5
- package/app/composables/og.ts +5 -5
- package/app/composables/scrollUp.ts +3 -1
- package/app/pages/book/[...bookId].vue +3 -2
- package/app/pages/group/[...groupId].vue +3 -2
- package/app/pages/page/[...pageId].vue +4 -2
- package/app/plugins/appSetup/config.ts +1 -0
- package/app/plugins/appSetup/global.ts +3 -0
- package/app/plugins/appSetup/index.ts +4 -1
- package/app/plugins/devReload.client.ts +13 -0
- package/app/router.options.ts +17 -3
- package/app/styles/main.css +2 -2
- package/modules/erudit/dependencies.ts +16 -0
- package/modules/erudit/index.ts +8 -1
- package/modules/erudit/setup/autoImports.ts +143 -0
- package/modules/erudit/setup/elements/appTemplate.ts +5 -3
- package/modules/erudit/setup/elements/globalTemplate.ts +17 -8
- package/modules/erudit/setup/elements/setup.ts +8 -14
- package/modules/erudit/setup/elements/tagsTable.ts +2 -18
- package/modules/erudit/setup/fullRestart.ts +5 -3
- package/modules/erudit/setup/namesTable.ts +33 -0
- package/modules/erudit/setup/problemChecks/setup.ts +60 -0
- package/modules/erudit/setup/problemChecks/shared.ts +4 -0
- package/modules/erudit/setup/problemChecks/template.ts +37 -0
- package/modules/erudit/setup/runtimeConfig.ts +12 -7
- package/modules/erudit/setup/toJsSlug.ts +19 -0
- package/nuxt.config.ts +14 -6
- package/package.json +10 -11
- package/server/api/problemScript/[...problemScriptPath].ts +263 -60
- package/server/erudit/build.ts +10 -4
- package/server/erudit/content/nav/build.ts +5 -5
- package/server/erudit/content/nav/front.ts +1 -0
- package/server/erudit/content/repository/deps.ts +33 -3
- package/server/erudit/content/resolve/index.ts +3 -3
- package/server/erudit/content/resolve/utils/contentError.ts +2 -2
- package/server/erudit/content/resolve/utils/insertContentResolved.ts +22 -5
- package/server/erudit/global.ts +5 -1
- package/server/erudit/importer.ts +127 -0
- package/server/erudit/index.ts +2 -2
- package/server/erudit/logger.ts +18 -10
- package/server/erudit/reloadSignal.ts +14 -0
- package/server/routes/_reload.ts +27 -0
- package/shared/types/contentConnections.ts +1 -0
- package/shared/types/frontContentNav.ts +2 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
2
|
import { sql } from 'drizzle-orm';
|
|
3
3
|
import type { ContentProseType } from '@erudit-js/core/content/prose';
|
|
4
4
|
import { builtValidPaths } from '../../global/build';
|
|
@@ -80,7 +80,24 @@ async function insertContentDeps(
|
|
|
80
80
|
await ERUDIT.db
|
|
81
81
|
.insert(ERUDIT.db.schema.contentDeps)
|
|
82
82
|
.values(contentDeps)
|
|
83
|
-
.
|
|
83
|
+
.onConflictDoUpdate({
|
|
84
|
+
target: [
|
|
85
|
+
ERUDIT.db.schema.contentDeps.fromFullId,
|
|
86
|
+
ERUDIT.db.schema.contentDeps.toFullId,
|
|
87
|
+
],
|
|
88
|
+
set: {
|
|
89
|
+
// Merge unique names from the auto dep into the existing row
|
|
90
|
+
// (which may already be a hard dep that has no uniqueNames yet).
|
|
91
|
+
// Only uniqueNames is updated — hard/reason are left untouched.
|
|
92
|
+
uniqueNames: sql`CASE
|
|
93
|
+
WHEN ${ERUDIT.db.schema.contentDeps.uniqueNames} IS NULL
|
|
94
|
+
THEN excluded.uniqueNames
|
|
95
|
+
WHEN excluded.uniqueNames IS NULL
|
|
96
|
+
THEN ${ERUDIT.db.schema.contentDeps.uniqueNames}
|
|
97
|
+
ELSE ${ERUDIT.db.schema.contentDeps.uniqueNames} || ',' || excluded.uniqueNames
|
|
98
|
+
END`,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
84
101
|
}
|
|
85
102
|
}
|
|
86
103
|
|
|
@@ -93,7 +110,7 @@ function filterTargetMap(
|
|
|
93
110
|
const brokenLinkMessage = (message: string, metas: ContentLinkUsage[]) => {
|
|
94
111
|
let output = `${message} in ${ERUDIT.log.stress(contentFullId)}:\n`;
|
|
95
112
|
for (const { type, label } of metas) {
|
|
96
|
-
output += ` ${
|
|
113
|
+
output += ` ${styleText('gray', '➔')} <${type}>${label}</${type}>\n`;
|
|
97
114
|
}
|
|
98
115
|
return output;
|
|
99
116
|
};
|
|
@@ -104,7 +121,7 @@ function filterTargetMap(
|
|
|
104
121
|
if (storageKey.startsWith('<link:unknown>/')) {
|
|
105
122
|
ERUDIT.log.warn(
|
|
106
123
|
brokenLinkMessage(
|
|
107
|
-
`Unknown link ${
|
|
124
|
+
`Unknown link ${styleText('red', storageKey.replace('<link:unknown>/', ''))}`,
|
|
108
125
|
metas,
|
|
109
126
|
),
|
|
110
127
|
);
|
|
@@ -136,7 +153,7 @@ function filterTargetMap(
|
|
|
136
153
|
} catch {
|
|
137
154
|
ERUDIT.log.warn(
|
|
138
155
|
brokenLinkMessage(
|
|
139
|
-
`Failed to resolve content link ${
|
|
156
|
+
`Failed to resolve content link ${styleText('red', storageKey.replace('<link:global>/', ''))}`,
|
|
140
157
|
metas,
|
|
141
158
|
),
|
|
142
159
|
);
|
package/server/erudit/global.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { EruditServerPaths } from './path';
|
|
|
11
11
|
import type { EruditServerRepository } from './repository';
|
|
12
12
|
|
|
13
13
|
import { registerProseGlobals } from '#erudit/prose/global';
|
|
14
|
+
import { registerAutoImportGlobals } from '#erudit/autoImports';
|
|
14
15
|
|
|
15
16
|
export const ERUDIT: {
|
|
16
17
|
buildError: EruditServerBuildError;
|
|
@@ -27,7 +28,9 @@ export const ERUDIT: {
|
|
|
27
28
|
import: EruditServerImporter;
|
|
28
29
|
} = {} as any;
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
(globalThis as any).ERUDIT_GLOBAL = (globalThis as any).ERUDIT_GLOBAL || {};
|
|
32
|
+
|
|
33
|
+
Object.assign((globalThis as any).ERUDIT_GLOBAL, {
|
|
31
34
|
defineContributor,
|
|
32
35
|
defineSponsor,
|
|
33
36
|
defineCameo,
|
|
@@ -40,3 +43,4 @@ Object.assign(globalThis, {
|
|
|
40
43
|
});
|
|
41
44
|
|
|
42
45
|
registerProseGlobals();
|
|
46
|
+
registerAutoImportGlobals();
|
|
@@ -16,6 +16,87 @@ export type EruditServerImporter = Jiti['import'];
|
|
|
16
16
|
|
|
17
17
|
export let jiti: Jiti;
|
|
18
18
|
|
|
19
|
+
/** Cached list of valid identifier keys from ERUDIT_GLOBAL. */
|
|
20
|
+
let cachedGlobalKeys: string[] | undefined;
|
|
21
|
+
|
|
22
|
+
function getGlobalKeys(): string[] {
|
|
23
|
+
if (cachedGlobalKeys !== undefined) return cachedGlobalKeys;
|
|
24
|
+
|
|
25
|
+
const eg = (globalThis as any).ERUDIT_GLOBAL;
|
|
26
|
+
if (!eg || typeof eg !== 'object') {
|
|
27
|
+
cachedGlobalKeys = [];
|
|
28
|
+
return cachedGlobalKeys;
|
|
29
|
+
}
|
|
30
|
+
|
|
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]!);
|
|
78
|
+
}
|
|
79
|
+
|
|
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';
|
|
98
|
+
}
|
|
99
|
+
|
|
19
100
|
export async function setupServerImporter() {
|
|
20
101
|
const jitiId = ERUDIT.paths.project();
|
|
21
102
|
const defaultJiti = createJiti(jitiId, createBaseJitiOptions());
|
|
@@ -38,6 +119,18 @@ export async function setupServerImporter() {
|
|
|
38
119
|
|
|
39
120
|
let code = getDefaultCode(opts);
|
|
40
121
|
|
|
122
|
+
//
|
|
123
|
+
// Inject ERUDIT_GLOBAL preamble for project files
|
|
124
|
+
// Destructures all erudit globals (tags, defineX, jsx runtime, etc.)
|
|
125
|
+
// into local variables so bare identifiers resolve correctly.
|
|
126
|
+
//
|
|
127
|
+
if (filename.startsWith(ERUDIT.paths.project() + '/')) {
|
|
128
|
+
const preamble = buildFilteredPreamble(code);
|
|
129
|
+
if (preamble) {
|
|
130
|
+
code = preamble + code;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
41
134
|
//
|
|
42
135
|
// Insert IDs in `defineDocument(...)` calls
|
|
43
136
|
//
|
|
@@ -58,6 +151,40 @@ export async function setupServerImporter() {
|
|
|
58
151
|
|
|
59
152
|
code = insertProblemScriptId(toRelPath(filename), code);
|
|
60
153
|
|
|
154
|
+
//
|
|
155
|
+
// Rebind problem script creator src to this file's path.
|
|
156
|
+
//
|
|
157
|
+
// When defineProblemScript is called inside a shared utility file (e.g.
|
|
158
|
+
// shared.tsx) and then re-exported through an entry file (e.g.
|
|
159
|
+
// my-script.tsx), jiti injects the *shared* file's path as the scriptSrc.
|
|
160
|
+
// That makes the client fetch `/api/problemScript/.../shared.js`, which has
|
|
161
|
+
// no default export and fails at runtime.
|
|
162
|
+
//
|
|
163
|
+
// After the module's own code runs we inspect its default export: if it is
|
|
164
|
+
// a ProblemScriptInstanceCreator (a function whose return value has a
|
|
165
|
+
// `.generate` method), we wrap it so every created instance gets its
|
|
166
|
+
// scriptSrc replaced with **this** file's relative path – the actual entry
|
|
167
|
+
// file the API route will serve.
|
|
168
|
+
//
|
|
169
|
+
if (!filename.startsWith(ERUDIT.paths.project() + '/')) {
|
|
170
|
+
return { code };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const relFilePath = toRelPath(filename).replace(/\.[jt]sx?$/, '');
|
|
174
|
+
code += `
|
|
175
|
+
;(function() {
|
|
176
|
+
var _eruditFileSrc = ${JSON.stringify(relFilePath)};
|
|
177
|
+
var _def = exports.default;
|
|
178
|
+
if (typeof _def !== 'function') return;
|
|
179
|
+
exports.default = Object.assign(function() {
|
|
180
|
+
var instance = _def.apply(this, arguments);
|
|
181
|
+
if (instance && typeof instance === 'object' && typeof instance.generate === 'function') {
|
|
182
|
+
return Object.assign({}, instance, { scriptSrc: _eruditFileSrc });
|
|
183
|
+
}
|
|
184
|
+
return instance;
|
|
185
|
+
}, _def);
|
|
186
|
+
})();`;
|
|
187
|
+
|
|
61
188
|
return { code };
|
|
62
189
|
},
|
|
63
190
|
});
|
package/server/erudit/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
2
|
import type { EruditMode } from '@erudit-js/core/mode';
|
|
3
3
|
|
|
4
4
|
import { setupServerLogger } from './logger';
|
|
@@ -72,7 +72,7 @@ async function setupServer() {
|
|
|
72
72
|
await setupServerDatabase();
|
|
73
73
|
await setupServerRepository();
|
|
74
74
|
await setupServerContentNav();
|
|
75
|
-
ERUDIT.log.success(
|
|
75
|
+
ERUDIT.log.success(styleText('green', 'Setup Complete!'));
|
|
76
76
|
|
|
77
77
|
await tryServerWatchProject();
|
|
78
78
|
await buildServerErudit();
|
package/server/erudit/logger.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
2
|
import { brandColorTitle } from '@erudit-js/core/brandTerminal';
|
|
3
3
|
|
|
4
4
|
interface Logger {
|
|
@@ -15,8 +15,12 @@ export type EruditServerLogger = Logger & {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export async function setupServerLogger() {
|
|
18
|
-
const serverLogger = createLogger(
|
|
19
|
-
|
|
18
|
+
const serverLogger = createLogger(
|
|
19
|
+
`${brandColorTitle}${styleText('gray', ' Server')}`,
|
|
20
|
+
);
|
|
21
|
+
const serverDebugLogger = createLogger(
|
|
22
|
+
`${brandColorTitle}${styleText('gray', ' Server Debug')}`,
|
|
23
|
+
);
|
|
20
24
|
const debugLogEnabled = !!ERUDIT.config.public.debug.log;
|
|
21
25
|
|
|
22
26
|
ERUDIT.log = new Proxy(serverLogger, {
|
|
@@ -42,32 +46,36 @@ export async function setupServerLogger() {
|
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
function createLogger(tag: string): Logger {
|
|
45
|
-
const formattedTag =
|
|
49
|
+
const formattedTag = `${styleText('gray', '[')}${tag}${styleText('gray', ']')}`;
|
|
46
50
|
|
|
47
51
|
return {
|
|
48
52
|
info(message: any) {
|
|
49
|
-
console.log(`${formattedTag} ${
|
|
53
|
+
console.log(`${formattedTag} ${styleText('blueBright', 'ℹ')} ${message}`);
|
|
50
54
|
},
|
|
51
55
|
start(message: any) {
|
|
52
|
-
console.log(
|
|
56
|
+
console.log(
|
|
57
|
+
`${formattedTag} ${styleText('magentaBright', '◐')} ${message}`,
|
|
58
|
+
);
|
|
53
59
|
},
|
|
54
60
|
success(message: any) {
|
|
55
|
-
console.log(
|
|
61
|
+
console.log(
|
|
62
|
+
`${formattedTag} ${styleText('greenBright', '✔')} ${message}`,
|
|
63
|
+
);
|
|
56
64
|
},
|
|
57
65
|
warn(message: any) {
|
|
58
66
|
console.log(
|
|
59
|
-
`${formattedTag} ${
|
|
67
|
+
`${formattedTag} ${styleText(['bgYellowBright', 'black'], ' WARN ')} ${message}`,
|
|
60
68
|
);
|
|
61
69
|
},
|
|
62
70
|
error(message: any) {
|
|
63
71
|
console.log();
|
|
64
72
|
console.log(
|
|
65
|
-
`${formattedTag} ${
|
|
73
|
+
`${formattedTag} ${styleText(['bgRed', 'whiteBright'], ' ERROR ')} ${message}`,
|
|
66
74
|
);
|
|
67
75
|
console.log();
|
|
68
76
|
},
|
|
69
77
|
stress(message: any) {
|
|
70
|
-
return
|
|
78
|
+
return styleText('cyanBright', String(message));
|
|
71
79
|
},
|
|
72
80
|
};
|
|
73
81
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type ReloadCallback = () => void;
|
|
2
|
+
|
|
3
|
+
const subscribers = new Set<ReloadCallback>();
|
|
4
|
+
|
|
5
|
+
export function subscribeReload(callback: ReloadCallback): () => void {
|
|
6
|
+
subscribers.add(callback);
|
|
7
|
+
return () => subscribers.delete(callback);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function triggerReload(): void {
|
|
11
|
+
for (const callback of subscribers) {
|
|
12
|
+
callback();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isDevLikeMode } from '@erudit-js/core/mode';
|
|
2
|
+
import { subscribeReload } from '../erudit/reloadSignal';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler((event) => {
|
|
5
|
+
if (!isDevLikeMode(ERUDIT.mode)) {
|
|
6
|
+
throw createError({ statusCode: 404 });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { res, req } = event.node;
|
|
10
|
+
|
|
11
|
+
setResponseHeaders(event, {
|
|
12
|
+
'Content-Type': 'text/event-stream',
|
|
13
|
+
'Cache-Control': 'no-cache',
|
|
14
|
+
Connection: 'keep-alive',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
res.write(': connected\n\n');
|
|
18
|
+
|
|
19
|
+
const unsub = subscribeReload(() => {
|
|
20
|
+
res.write('data: reload\n\n');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
req.on('close', unsub);
|
|
24
|
+
|
|
25
|
+
// Keep connection open
|
|
26
|
+
return new Promise<void>(() => {});
|
|
27
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContentFlags } from '@erudit-js/core/content/flags';
|
|
2
|
+
import type { TopicPart } from '@erudit-js/core/content/topic';
|
|
2
3
|
|
|
3
4
|
export interface FrontContentNavItemBase {
|
|
4
5
|
shortId: string;
|
|
@@ -9,6 +10,7 @@ export interface FrontContentNavItemBase {
|
|
|
9
10
|
|
|
10
11
|
export interface FrontContentNavTopic extends FrontContentNavItemBase {
|
|
11
12
|
type: 'topic';
|
|
13
|
+
parts: TopicPart[];
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export interface FrontContentNavPage extends FrontContentNavItemBase {
|