erudit 4.2.0-dev.1 → 4.3.0-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/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/aside/minor/news/AsideMinorNews.vue +1 -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/phrases.ts +2 -0
- 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/globalTemplate.ts +10 -2
- 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 +33 -0
- package/modules/erudit/setup/runtimeConfig.ts +12 -7
- package/nuxt.config.ts +14 -6
- package/package.json +5 -6
- package/server/api/problemScript/[...problemScriptPath].ts +245 -52
- package/server/erudit/build.ts +10 -4
- package/server/erudit/content/global/build.ts +43 -3
- 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 +45 -6
- 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 +29 -27
- package/server/erudit/global.ts +5 -1
- package/server/erudit/importer.ts +69 -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,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
2
|
import type { ContentNavNode } from '../../nav/types';
|
|
3
3
|
|
|
4
4
|
export function logContentError(contentNode: ContentNavNode) {
|
|
5
5
|
ERUDIT.log.error(
|
|
6
6
|
`Error parsing ${contentNode.type} ${ERUDIT.log.stress(
|
|
7
7
|
contentNode.fullId,
|
|
8
|
-
)}!\nLocation: ${
|
|
8
|
+
)}!\nLocation: ${styleText('red', ERUDIT.paths.project(`content/${contentNode.contentRelPath}`))}`,
|
|
9
9
|
);
|
|
10
10
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
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
|
-
import {
|
|
4
|
+
import { builtValidPaths } from '../../global/build';
|
|
5
5
|
import type {
|
|
6
6
|
ContentLinks,
|
|
7
7
|
ContentLinkUsage,
|
|
@@ -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
|
);
|
|
@@ -148,25 +165,11 @@ function filterTargetMap(
|
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
function globalContentToNavNode(globalContentPath: string) {
|
|
151
|
-
// Validate the full path (including any $unique suffix) against the
|
|
152
|
-
//
|
|
153
|
-
// names
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
let cursor: any = builtLinkObject;
|
|
157
|
-
let valid = true;
|
|
158
|
-
|
|
159
|
-
for (const part of parts) {
|
|
160
|
-
if (!cursor || typeof cursor !== 'object' || !(part in cursor)) {
|
|
161
|
-
valid = false;
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
cursor = cursor[part];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (!valid) {
|
|
168
|
-
throw new Error(`Path not found in \$CONTENT: ${globalContentPath}`);
|
|
169
|
-
}
|
|
168
|
+
// Validate the full path (including any $unique suffix) against the complete
|
|
169
|
+
// set of known valid paths built from source files. This catches broken
|
|
170
|
+
// unique names and content paths before we ever touch the nav tree.
|
|
171
|
+
if (builtValidPaths && !builtValidPaths.has(globalContentPath)) {
|
|
172
|
+
throw new Error(`Path not found in \$CONTENT: ${globalContentPath}`);
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
const parts = globalContentPath.split('/');
|
|
@@ -175,8 +178,7 @@ function globalContentToNavNode(globalContentPath: string) {
|
|
|
175
178
|
parts.pop();
|
|
176
179
|
}
|
|
177
180
|
|
|
178
|
-
//
|
|
179
|
-
// isn't found the last segment must be a topic part — fall back to parent.
|
|
181
|
+
// If the exact node isn't found the last segment is a topic part — fall back to parent.
|
|
180
182
|
return (
|
|
181
183
|
ERUDIT.contentNav.getNode(parts.join('/')) ??
|
|
182
184
|
ERUDIT.contentNav.getNodeOrThrow(parts.slice(0, -1).join('/'))
|
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,29 @@ 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;
|
|
21
|
+
|
|
22
|
+
function getEruditGlobalPreamble(): string {
|
|
23
|
+
if (eruditGlobalPreamble !== undefined) return eruditGlobalPreamble;
|
|
24
|
+
|
|
25
|
+
const eg = (globalThis as any).ERUDIT_GLOBAL;
|
|
26
|
+
if (!eg || typeof eg !== 'object') {
|
|
27
|
+
eruditGlobalPreamble = '';
|
|
28
|
+
return eruditGlobalPreamble;
|
|
29
|
+
}
|
|
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;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
eruditGlobalPreamble =
|
|
38
|
+
'var { ' + names.join(', ') + ' } = globalThis.ERUDIT_GLOBAL;\n';
|
|
39
|
+
return eruditGlobalPreamble;
|
|
40
|
+
}
|
|
41
|
+
|
|
19
42
|
export async function setupServerImporter() {
|
|
20
43
|
const jitiId = ERUDIT.paths.project();
|
|
21
44
|
const defaultJiti = createJiti(jitiId, createBaseJitiOptions());
|
|
@@ -38,6 +61,18 @@ export async function setupServerImporter() {
|
|
|
38
61
|
|
|
39
62
|
let code = getDefaultCode(opts);
|
|
40
63
|
|
|
64
|
+
//
|
|
65
|
+
// Inject ERUDIT_GLOBAL preamble for project files
|
|
66
|
+
// Destructures all erudit globals (tags, defineX, jsx runtime, etc.)
|
|
67
|
+
// into local variables so bare identifiers resolve correctly.
|
|
68
|
+
//
|
|
69
|
+
if (filename.startsWith(ERUDIT.paths.project() + '/')) {
|
|
70
|
+
const preamble = getEruditGlobalPreamble();
|
|
71
|
+
if (preamble) {
|
|
72
|
+
code = preamble + code;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
41
76
|
//
|
|
42
77
|
// Insert IDs in `defineDocument(...)` calls
|
|
43
78
|
//
|
|
@@ -58,6 +93,40 @@ export async function setupServerImporter() {
|
|
|
58
93
|
|
|
59
94
|
code = insertProblemScriptId(toRelPath(filename), code);
|
|
60
95
|
|
|
96
|
+
//
|
|
97
|
+
// Rebind problem script creator src to this file's path.
|
|
98
|
+
//
|
|
99
|
+
// When defineProblemScript is called inside a shared utility file (e.g.
|
|
100
|
+
// shared.tsx) and then re-exported through an entry file (e.g.
|
|
101
|
+
// my-script.tsx), jiti injects the *shared* file's path as the scriptSrc.
|
|
102
|
+
// That makes the client fetch `/api/problemScript/.../shared.js`, which has
|
|
103
|
+
// no default export and fails at runtime.
|
|
104
|
+
//
|
|
105
|
+
// After the module's own code runs we inspect its default export: if it is
|
|
106
|
+
// a ProblemScriptInstanceCreator (a function whose return value has a
|
|
107
|
+
// `.generate` method), we wrap it so every created instance gets its
|
|
108
|
+
// scriptSrc replaced with **this** file's relative path – the actual entry
|
|
109
|
+
// file the API route will serve.
|
|
110
|
+
//
|
|
111
|
+
if (!filename.startsWith(ERUDIT.paths.project() + '/')) {
|
|
112
|
+
return { code };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const relFilePath = toRelPath(filename).replace(/\.[jt]sx?$/, '');
|
|
116
|
+
code += `
|
|
117
|
+
;(function() {
|
|
118
|
+
var _eruditFileSrc = ${JSON.stringify(relFilePath)};
|
|
119
|
+
var _def = exports.default;
|
|
120
|
+
if (typeof _def !== 'function') return;
|
|
121
|
+
exports.default = Object.assign(function() {
|
|
122
|
+
var instance = _def.apply(this, arguments);
|
|
123
|
+
if (instance && typeof instance === 'object' && typeof instance.generate === 'function') {
|
|
124
|
+
return Object.assign({}, instance, { scriptSrc: _eruditFileSrc });
|
|
125
|
+
}
|
|
126
|
+
return instance;
|
|
127
|
+
}, _def);
|
|
128
|
+
})();`;
|
|
129
|
+
|
|
61
130
|
return { code };
|
|
62
131
|
},
|
|
63
132
|
});
|
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 {
|