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.
Files changed (61) hide show
  1. package/app/components/Prose.vue +2 -0
  2. package/app/components/aside/major/contentNav/items/ContentNavTopic.vue +12 -1
  3. package/app/components/aside/major/search/SearchResult.vue +16 -2
  4. package/app/components/aside/minor/contributor/AsideMinorContributor.vue +7 -1
  5. package/app/components/main/MainStickyHeader.vue +5 -2
  6. package/app/components/main/MainStickyHeaderPreamble.vue +5 -2
  7. package/app/components/main/MainTopicPartPage.vue +3 -2
  8. package/app/components/main/MainTopicPartSwitch.vue +18 -7
  9. package/app/components/main/connections/Deps.vue +1 -4
  10. package/app/components/main/connections/MainConnections.vue +9 -3
  11. package/app/components/main/contentStats/ItemLastChanged.vue +3 -32
  12. package/app/components/main/contentStats/MainContentStats.vue +3 -4
  13. package/app/components/preview/Preview.vue +8 -6
  14. package/app/components/preview/PreviewScreen.vue +9 -7
  15. package/app/components/preview/screen/Unique.vue +3 -2
  16. package/app/composables/ads.ts +1 -1
  17. package/app/composables/analytics.ts +1 -1
  18. package/app/composables/lastChanged.ts +38 -5
  19. package/app/composables/og.ts +5 -5
  20. package/app/composables/scrollUp.ts +3 -1
  21. package/app/pages/book/[...bookId].vue +3 -2
  22. package/app/pages/group/[...groupId].vue +3 -2
  23. package/app/pages/page/[...pageId].vue +4 -2
  24. package/app/plugins/appSetup/config.ts +1 -0
  25. package/app/plugins/appSetup/global.ts +3 -0
  26. package/app/plugins/appSetup/index.ts +4 -1
  27. package/app/plugins/devReload.client.ts +13 -0
  28. package/app/router.options.ts +17 -3
  29. package/app/styles/main.css +2 -2
  30. package/modules/erudit/dependencies.ts +16 -0
  31. package/modules/erudit/index.ts +8 -1
  32. package/modules/erudit/setup/autoImports.ts +143 -0
  33. package/modules/erudit/setup/elements/appTemplate.ts +5 -3
  34. package/modules/erudit/setup/elements/globalTemplate.ts +17 -8
  35. package/modules/erudit/setup/elements/setup.ts +8 -14
  36. package/modules/erudit/setup/elements/tagsTable.ts +2 -18
  37. package/modules/erudit/setup/fullRestart.ts +5 -3
  38. package/modules/erudit/setup/namesTable.ts +33 -0
  39. package/modules/erudit/setup/problemChecks/setup.ts +60 -0
  40. package/modules/erudit/setup/problemChecks/shared.ts +4 -0
  41. package/modules/erudit/setup/problemChecks/template.ts +37 -0
  42. package/modules/erudit/setup/runtimeConfig.ts +12 -7
  43. package/modules/erudit/setup/toJsSlug.ts +19 -0
  44. package/nuxt.config.ts +14 -6
  45. package/package.json +10 -11
  46. package/server/api/problemScript/[...problemScriptPath].ts +263 -60
  47. package/server/erudit/build.ts +10 -4
  48. package/server/erudit/content/nav/build.ts +5 -5
  49. package/server/erudit/content/nav/front.ts +1 -0
  50. package/server/erudit/content/repository/deps.ts +33 -3
  51. package/server/erudit/content/resolve/index.ts +3 -3
  52. package/server/erudit/content/resolve/utils/contentError.ts +2 -2
  53. package/server/erudit/content/resolve/utils/insertContentResolved.ts +22 -5
  54. package/server/erudit/global.ts +5 -1
  55. package/server/erudit/importer.ts +127 -0
  56. package/server/erudit/index.ts +2 -2
  57. package/server/erudit/logger.ts +18 -10
  58. package/server/erudit/reloadSignal.ts +14 -0
  59. package/server/routes/_reload.ts +27 -0
  60. package/shared/types/contentConnections.ts +1 -0
  61. package/shared/types/frontContentNav.ts +2 -0
@@ -1,4 +1,4 @@
1
- import chalk from 'chalk';
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
- .onConflictDoNothing();
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 += ` ${chalk.gray('➔')} <${type}>${label}</${type}>\n`;
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 ${chalk.red(storageKey.replace('<link:unknown>/', ''))}`,
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 ${chalk.red(storageKey.replace('<link:global>/', ''))}`,
156
+ `Failed to resolve content link ${styleText('red', storageKey.replace('<link:global>/', ''))}`,
140
157
  metas,
141
158
  ),
142
159
  );
@@ -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
- Object.assign(globalThis, {
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
  });
@@ -1,4 +1,4 @@
1
- import chalk from 'chalk';
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(chalk.green('Setup Complete!'));
75
+ ERUDIT.log.success(styleText('green', 'Setup Complete!'));
76
76
 
77
77
  await tryServerWatchProject();
78
78
  await buildServerErudit();
@@ -1,4 +1,4 @@
1
- import chalk from 'chalk';
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(brandColorTitle + ' Server');
19
- const serverDebugLogger = createLogger(brandColorTitle + ' Server Debug');
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 = chalk.gray(`[${tag}]`);
49
+ const formattedTag = `${styleText('gray', '[')}${tag}${styleText('gray', ']')}`;
46
50
 
47
51
  return {
48
52
  info(message: any) {
49
- console.log(`${formattedTag} ${chalk.blueBright('ℹ')} ${message}`);
53
+ console.log(`${formattedTag} ${styleText('blueBright', 'ℹ')} ${message}`);
50
54
  },
51
55
  start(message: any) {
52
- console.log(`${formattedTag} ${chalk.magentaBright('◐')} ${message}`);
56
+ console.log(
57
+ `${formattedTag} ${styleText('magentaBright', '◐')} ${message}`,
58
+ );
53
59
  },
54
60
  success(message: any) {
55
- console.log(`${formattedTag} ${chalk.greenBright('✔')} ${message}`);
61
+ console.log(
62
+ `${formattedTag} ${styleText('greenBright', '✔')} ${message}`,
63
+ );
56
64
  },
57
65
  warn(message: any) {
58
66
  console.log(
59
- `${formattedTag} ${chalk.bgYellowBright.black(' WARN ')} ${message}`,
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} ${chalk.bgRed.whiteBright(' ERROR ')} ${message}`,
73
+ `${formattedTag} ${styleText(['bgRed', 'whiteBright'], ' ERROR ')} ${message}`,
66
74
  );
67
75
  console.log();
68
76
  },
69
77
  stress(message: any) {
70
- return chalk.cyanBright(message);
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
+ });
@@ -24,6 +24,7 @@ export interface ContentAutoDep extends BaseContentDep {
24
24
  export interface ContentHardDep extends BaseContentDep {
25
25
  type: 'hard';
26
26
  reason: string;
27
+ uniques?: ContentDepUnique[];
27
28
  }
28
29
 
29
30
  export type ContentDep = ContentAutoDep | ContentHardDep;
@@ -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 {