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.
Files changed (62) 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/aside/minor/news/AsideMinorNews.vue +1 -1
  6. package/app/components/main/MainStickyHeader.vue +5 -2
  7. package/app/components/main/MainStickyHeaderPreamble.vue +5 -2
  8. package/app/components/main/MainTopicPartPage.vue +3 -2
  9. package/app/components/main/MainTopicPartSwitch.vue +18 -7
  10. package/app/components/main/connections/Deps.vue +1 -4
  11. package/app/components/main/connections/MainConnections.vue +9 -3
  12. package/app/components/main/contentStats/ItemLastChanged.vue +3 -32
  13. package/app/components/main/contentStats/MainContentStats.vue +3 -4
  14. package/app/components/preview/Preview.vue +8 -6
  15. package/app/components/preview/PreviewScreen.vue +9 -7
  16. package/app/components/preview/screen/Unique.vue +3 -2
  17. package/app/composables/ads.ts +1 -1
  18. package/app/composables/analytics.ts +1 -1
  19. package/app/composables/lastChanged.ts +38 -5
  20. package/app/composables/og.ts +5 -5
  21. package/app/composables/phrases.ts +2 -0
  22. package/app/composables/scrollUp.ts +3 -1
  23. package/app/pages/book/[...bookId].vue +3 -2
  24. package/app/pages/group/[...groupId].vue +3 -2
  25. package/app/pages/page/[...pageId].vue +4 -2
  26. package/app/plugins/appSetup/config.ts +1 -0
  27. package/app/plugins/appSetup/global.ts +3 -0
  28. package/app/plugins/appSetup/index.ts +4 -1
  29. package/app/plugins/devReload.client.ts +13 -0
  30. package/app/router.options.ts +17 -3
  31. package/app/styles/main.css +2 -2
  32. package/modules/erudit/dependencies.ts +16 -0
  33. package/modules/erudit/index.ts +8 -1
  34. package/modules/erudit/setup/autoImports.ts +143 -0
  35. package/modules/erudit/setup/elements/globalTemplate.ts +10 -2
  36. package/modules/erudit/setup/elements/setup.ts +8 -14
  37. package/modules/erudit/setup/elements/tagsTable.ts +2 -18
  38. package/modules/erudit/setup/fullRestart.ts +5 -3
  39. package/modules/erudit/setup/namesTable.ts +33 -0
  40. package/modules/erudit/setup/problemChecks/setup.ts +60 -0
  41. package/modules/erudit/setup/problemChecks/shared.ts +4 -0
  42. package/modules/erudit/setup/problemChecks/template.ts +33 -0
  43. package/modules/erudit/setup/runtimeConfig.ts +12 -7
  44. package/nuxt.config.ts +14 -6
  45. package/package.json +5 -6
  46. package/server/api/problemScript/[...problemScriptPath].ts +245 -52
  47. package/server/erudit/build.ts +10 -4
  48. package/server/erudit/content/global/build.ts +43 -3
  49. package/server/erudit/content/nav/build.ts +5 -5
  50. package/server/erudit/content/nav/front.ts +1 -0
  51. package/server/erudit/content/repository/deps.ts +45 -6
  52. package/server/erudit/content/resolve/index.ts +3 -3
  53. package/server/erudit/content/resolve/utils/contentError.ts +2 -2
  54. package/server/erudit/content/resolve/utils/insertContentResolved.ts +29 -27
  55. package/server/erudit/global.ts +5 -1
  56. package/server/erudit/importer.ts +69 -0
  57. package/server/erudit/index.ts +2 -2
  58. package/server/erudit/logger.ts +18 -10
  59. package/server/erudit/reloadSignal.ts +14 -0
  60. package/server/routes/_reload.ts +27 -0
  61. package/shared/types/contentConnections.ts +1 -0
  62. package/shared/types/frontContentNav.ts +2 -0
@@ -1,10 +1,10 @@
1
- import chalk from 'chalk';
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: ${chalk.red(ERUDIT.paths.project(`content/${contentNode.contentRelPath}`))}`,
8
+ )}!\nLocation: ${styleText('red', ERUDIT.paths.project(`content/${contentNode.contentRelPath}`))}`,
9
9
  );
10
10
  }
@@ -1,7 +1,7 @@
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
- import { builtLinkObject } from '../../global/build';
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
- .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
  );
@@ -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 link
152
- // object that was built from the source files. This catches broken unique
153
- // names as well as broken content paths before we ever touch the nav tree.
154
- if (builtLinkObject) {
155
- const parts = globalContentPath.split('/');
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
- // Path already validated against builtLinkObject, so if the exact node
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('/'))
@@ -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,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
  });
@@ -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 {