erudit 3.0.0-dev.10 → 3.0.0-dev.11

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.
@@ -1,8 +1,6 @@
1
1
  <template>
2
2
  <main :class="$style.main" erudit-main>
3
- <ClientOnly>
4
- <Preview />
5
- </ClientOnly>
3
+ <Preview />
6
4
  <div :class="$style.mainInner">
7
5
  <slot></slot>
8
6
  </div>
@@ -27,7 +27,7 @@ function linkAttrs(link: string) {
27
27
  <AsideListItem
28
28
  icon="users"
29
29
  v-bind="linkAttrs('/members')"
30
- :main="phrase.members + ':'"
30
+ :main="phrase.members"
31
31
  :secondary="memberCount!.toString()"
32
32
  />
33
33
  </PaneContentScroll>
@@ -23,7 +23,7 @@ if (strDate) {
23
23
  :link="`https://github.com/${eruditConfig.repository?.name}`"
24
24
  target="_blank"
25
25
  icon="draw"
26
- :main="phrase.content + ':'"
26
+ :main="phrase.content"
27
27
  :secondary
28
28
  />
29
29
  </template>
@@ -7,7 +7,7 @@ import { version } from '@erudit/package.json';
7
7
  link="https://github.com/erudit-js/erudit"
8
8
  target="_blank"
9
9
  icon="chip"
10
- main="Erudit:"
10
+ main="Erudit"
11
11
  :secondary="version"
12
12
  />
13
13
  </template>
@@ -47,7 +47,7 @@ onMounted(() => {
47
47
  <AsideListItem
48
48
  @click="_cycle"
49
49
  :icon="themeItem![0]"
50
- :main="phrase.theme + ':'"
50
+ :main="phrase.theme"
51
51
  :secondary="themeItem![1]"
52
52
  />
53
53
  </ClientOnly>
@@ -56,7 +56,7 @@ const isServer = import.meta.server;
56
56
  <style lang="scss" module>
57
57
  @use '$/def/bp';
58
58
 
59
- .eruditBitranContainer {
59
+ :global(.bitran-component).eruditBitranContainer {
60
60
  padding: var(--_pMainY) var(--_pMainX);
61
61
  padding-left: calc(var(--_pMainX) - var(--_bitran_asideWidth));
62
62
 
@@ -21,7 +21,7 @@ const root = await bitranTranspiler.parser.parse(data.bitran.content.biCode);
21
21
  <BitranContent
22
22
  :content="{
23
23
  root,
24
- preRenderData: data.bitran.content.preRenderData,
24
+ renderDataStorage: data.bitran.content.renderDataStorage,
25
25
  }"
26
26
  :context="data.bitran.context"
27
27
  />
@@ -88,7 +88,7 @@ export async function useBitranRenderers() {
88
88
 
89
89
  export async function useBitranElementRenderer(productName: string) {
90
90
  const renderer =
91
- (await useBitranRenderers())[productName] ||
91
+ (await getRenderers())[productName] ||
92
92
  getDefaultBitranRenderers()[productName];
93
93
 
94
94
  if (!renderer)
@@ -7,6 +7,7 @@ import {
7
7
  stringifyBitranLocation,
8
8
  type BitranLocation,
9
9
  } from '@erudit-js/cog/schema';
10
+
10
11
  import type { StringBitranContent } from '@erudit/shared/bitran/stringContent';
11
12
 
12
13
  import eruditConfig from '#erudit/config';
@@ -33,9 +34,32 @@ export async function useBitranContent(
33
34
 
34
35
  // @ts-ignore
35
36
  contentPromise = (async () => {
36
- const stringContent = (await $fetch(apiRoute, {
37
- responseType: 'json',
38
- })) as StringBitranContent;
37
+ const locationString = encodeBitranLocation(
38
+ stringifyBitranLocation(location.value!),
39
+ );
40
+
41
+ const payloadKey = `content`;
42
+ const payload =
43
+ (nuxtApp.static.data[payloadKey] ||=
44
+ nuxtApp.payload.data[payloadKey] ||=
45
+ {});
46
+
47
+ let stringContent: StringBitranContent;
48
+
49
+ if (payload.location === locationString) {
50
+ stringContent = payload;
51
+ } else {
52
+ stringContent = await $fetch(apiRoute, {
53
+ responseType: 'json',
54
+ });
55
+
56
+ // Clear the payload and set new data
57
+ Object.keys(payload).forEach((key) => delete payload[key]);
58
+ Object.assign(payload, {
59
+ ...stringContent,
60
+ location: locationString,
61
+ });
62
+ }
39
63
 
40
64
  const bitranTranspiler = await useBitranTranspiler();
41
65
 
@@ -58,7 +82,7 @@ export async function useBitranContent(
58
82
 
59
83
  content.value = {
60
84
  root,
61
- preRenderData: stringContent.preRenderData,
85
+ renderDataStorage: stringContent.renderDataStorage,
62
86
  };
63
87
 
64
88
  return content;
@@ -1,13 +1,15 @@
1
1
  import { PreviewDataType, type PreviewData } from './data';
2
2
  import { PreviewRequestType, type PreviewRequest } from './request';
3
-
4
- import { buildGenericLink } from './data/genericLink';
5
- import { buildPageLink } from './data/pageLink';
6
- import { buildUnique } from './data/unique';
7
- import { createPreviewError } from './data/alert';
8
3
  import { PreviewThemeName } from './state';
9
4
 
10
- const builders = [buildGenericLink, buildPageLink, buildUnique];
5
+ const builders = [
6
+ async (request: PreviewRequest) =>
7
+ (await import('./data/genericLink')).buildGenericLink(request),
8
+ async (request: PreviewRequest) =>
9
+ (await import('./data/pageLink')).buildPageLink(request),
10
+ async (request: PreviewRequest) =>
11
+ (await import('./data/unique')).buildUnique(request),
12
+ ];
11
13
 
12
14
  export async function buildPreviewData(
13
15
  request: PreviewRequest,
@@ -24,6 +26,8 @@ export async function buildPreviewData(
24
26
  'expected_page_hash',
25
27
  );
26
28
 
29
+ const { createPreviewError } = await import('./data/alert');
30
+
27
31
  return createPreviewError({
28
32
  title: phrase.preview_missing_title,
29
33
  message: phrase.preview_missing_explain_mismatch,
@@ -36,6 +40,8 @@ export async function buildPreviewData(
36
40
  'element_id',
37
41
  );
38
42
 
43
+ const { createPreviewError } = await import('./data/alert');
44
+
39
45
  return createPreviewError({
40
46
  title: phrase.preview_missing_title,
41
47
  message: phrase.preview_missing_explain,
@@ -66,6 +72,7 @@ export async function buildPreviewData(
66
72
  if (result) return result;
67
73
  }
68
74
 
75
+ const { createPreviewError } = await import('./data/alert');
69
76
  throw createPreviewError({
70
77
  message: `Unable to build preview data for request!`,
71
78
  pre: JSON.stringify(request, null, 4),
package/module/imports.ts CHANGED
@@ -33,6 +33,11 @@ export function setupGlobalImports() {
33
33
  from: eruditPath(`globals/content`),
34
34
  }));
35
35
  })(),
36
+ // Problems Generation
37
+ {
38
+ name: 'defineProblemGenerator',
39
+ from: '@erudit-js/bitran-elements/problem/generator',
40
+ },
36
41
  // Helper Asset Path Functions
37
42
  ...(() => {
38
43
  const imports = [
package/module/index.ts CHANGED
@@ -7,6 +7,7 @@ import { logger } from '@erudit/module/logger';
7
7
  import { setupGlobalImports } from '@erudit/module/imports';
8
8
  import { setupBitranTemplates } from './bitran';
9
9
  import { setupGlobalPaths } from './paths';
10
+ import { setupProblemGenerators } from './problemGenerators';
10
11
 
11
12
  export default defineNuxtModule({
12
13
  meta: { name: 'Erudit', configKey: 'erudit' },
@@ -18,6 +19,7 @@ export default defineNuxtModule({
18
19
 
19
20
  const eruditConfig = await setupEruditConfig(_nuxt);
20
21
  await setupBitranTemplates(_nuxt);
22
+ await setupProblemGenerators(_nuxt);
21
23
  setupGlobalPaths(_nuxt);
22
24
 
23
25
  if (eruditConfig.site?.baseUrl) {
@@ -0,0 +1,46 @@
1
+ import { addTemplate } from 'nuxt/kit';
2
+ import type { Nuxt } from 'nuxt/schema';
3
+ import { globSync } from 'glob';
4
+
5
+ import { eruditEndNuxtPath, projectPath } from '@erudit/globalPath';
6
+ import { getContentPath } from '@erudit/utils/contentPath';
7
+
8
+ export async function setupProblemGenerators(_nuxt: Nuxt) {
9
+ const templateName = '#erudit/problemGenerators.ts';
10
+
11
+ addTemplate({
12
+ filename: templateName,
13
+ write: true,
14
+ async getContents() {
15
+ const generatorFiles = findGenerators();
16
+
17
+ return `
18
+ export const loaders = {
19
+ ${generatorFiles
20
+ .map((file) => {
21
+ const filePath = projectPath('content/' + file);
22
+ const contentPath = getContentPath(
23
+ file,
24
+ projectPath('content'),
25
+ ).replace(/\.gen\.(js|ts)$/, '');
26
+ return ` '${contentPath}': async () => (await import('${filePath}')).default,`;
27
+ })
28
+ .join('\n')}
29
+ }
30
+ `.trim();
31
+ },
32
+ });
33
+
34
+ const alias = (_nuxt.options.alias ||= {});
35
+ alias['#erudit/problemGenerators'] = eruditEndNuxtPath(
36
+ `.nuxt/${templateName}`,
37
+ );
38
+ }
39
+
40
+ function findGenerators() {
41
+ const generatorFiles = globSync('**/*.gen.{js,ts}', {
42
+ cwd: projectPath('content'),
43
+ posix: true,
44
+ });
45
+ return generatorFiles;
46
+ }
package/nuxt.config.ts CHANGED
@@ -18,11 +18,17 @@ export default defineNuxtConfig({
18
18
  '@shared': eruditPath('shared'),
19
19
  '@app': eruditPath('app'),
20
20
  $: eruditPath('app/styles'),
21
+ //
22
+ '#project': projectPath(),
23
+ '#content': projectPath('content'),
21
24
  },
22
25
  modules: [eruditPath('module'), 'nuxt-my-icons'],
23
26
  myicons: {
24
27
  iconsDir: eruditPath('app/assets/icons'),
25
28
  },
29
+ features: {
30
+ inlineStyles: false,
31
+ },
26
32
  typescript: {
27
33
  tsConfig: {
28
34
  include: [eruditPath('**/*'), projectPath('**/*')],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erudit",
3
- "version": "3.0.0-dev.10",
3
+ "version": "3.0.0-dev.11",
4
4
  "type": "module",
5
5
  "description": "🤓 CMS for perfect educational sites.",
6
6
  "license": "MIT",
@@ -15,14 +15,14 @@
15
15
  "erudit": "bin/erudit.mjs"
16
16
  },
17
17
  "peerDependencies": {
18
- "@erudit-js/cog": "3.0.0-dev.10",
19
- "@erudit-js/cli": "3.0.0-dev.10",
20
- "@erudit-js/bitran-elements": "3.0.0-dev.10"
18
+ "@erudit-js/cog": "3.0.0-dev.11",
19
+ "@erudit-js/cli": "3.0.0-dev.11",
20
+ "@erudit-js/bitran-elements": "3.0.0-dev.11"
21
21
  },
22
22
  "dependencies": {
23
- "@bitran-js/core": "1.0.0-dev.11",
24
- "@bitran-js/renderer-vue": "1.0.0-dev.11",
25
- "@bitran-js/transpiler": "1.0.0-dev.11",
23
+ "@bitran-js/core": "1.0.0-dev.12",
24
+ "@bitran-js/renderer-vue": "1.0.0-dev.12",
25
+ "@bitran-js/transpiler": "1.0.0-dev.12",
26
26
  "@floating-ui/vue": "^1.1.6",
27
27
  "chalk": "^5.4.1",
28
28
  "chokidar": "^4.0.3",
@@ -1,11 +1,13 @@
1
- import { createPreRenderData, type PreRenderData } from '@bitran-js/transpiler';
2
1
  import {
3
2
  BlockErrorNode,
4
3
  BlockNode,
5
4
  BlocksNode,
5
+ createRenderData,
6
+ ElementNode,
6
7
  InlinerErrorNode,
8
+ walkDown,
9
+ type RenderDataStorage,
7
10
  } from '@bitran-js/core';
8
-
9
11
  import {
10
12
  isTopicPart,
11
13
  mergeAliases,
@@ -26,11 +28,10 @@ import { DbTopic } from '@server/db/entities/Topic';
26
28
  import { DbGroup } from '@server/db/entities/Group';
27
29
  import { traverseInclude } from '@server/bitran/elements/include';
28
30
  import { createBitranTranspiler } from '@server/bitran/transpiler';
31
+ import { logger } from '@server/logger';
29
32
 
30
33
  import type { StringBitranContent } from '@shared/bitran/stringContent';
31
34
 
32
- import { logger } from '@server/logger';
33
-
34
35
  interface RawBitranContent {
35
36
  context: BitranContext;
36
37
  biCode: string;
@@ -122,63 +123,56 @@ async function createBitranContent(
122
123
  setEruditBitranRuntime(item, runtime),
123
124
  );
124
125
 
126
+ const renderDataStorage: RenderDataStorage = {};
127
+
128
+ async function makePreRender(node: ElementNode) {
129
+ if (generatePrerenderData === false) {
130
+ return;
131
+ }
132
+
133
+ const transpiler = bitranTranspiler.transpilers[node.name];
134
+ await createRenderData({
135
+ storage: renderDataStorage,
136
+ node,
137
+ extra: runtime,
138
+ generator: transpiler?.renderDataGenerator,
139
+ });
140
+ }
141
+
125
142
  const root = await bitranTranspiler.parser.parse(biCode, {
126
143
  async step(node) {
127
- if (node instanceof AliasesNode) {
128
- mergeAliases(context.aliases, node.parseData);
129
- return;
144
+ switch (true) {
145
+ case node instanceof AliasesNode:
146
+ mergeAliases(context.aliases, node.parseData);
147
+ break;
148
+ case node instanceof IncludeNode:
149
+ await resolveInclude(node, context);
150
+ break;
151
+ case node instanceof BlockErrorNode:
152
+ case node instanceof InlinerErrorNode:
153
+ logParseError(node.name, node.error);
154
+ return;
130
155
  }
131
156
 
132
- if (node instanceof IncludeNode) {
133
- await resolveInclude(node, context);
134
- return;
135
- }
157
+ await makePreRender(node);
136
158
 
137
- if (
138
- node instanceof BlockErrorNode ||
139
- node instanceof InlinerErrorNode
140
- ) {
141
- logParseError(node.name, node.error);
142
- return;
159
+ if (node instanceof IncludeNode) {
160
+ for (const block of node.parseData.blocks?.children || []) {
161
+ await walkDown(block, async (child) => {
162
+ if (child instanceof ElementNode) {
163
+ await makePreRender(child);
164
+ }
165
+ });
166
+ }
143
167
  }
144
168
  },
145
169
  });
146
170
 
147
171
  const finalContent = await bitranTranspiler.stringifier.stringify(root);
148
172
 
149
- // Building render data
150
- const preRenderDataMap: Record<string, PreRenderData> = {};
151
-
152
- if (generatePrerenderData) {
153
- await bitranTranspiler.parser.parse(finalContent, {
154
- step: async (node) => {
155
- const id = node.autoId;
156
- const elementTranspiler =
157
- bitranTranspiler.transpilers[node.name]!;
158
-
159
- if (id) {
160
- const preRenderData = await createPreRenderData(
161
- node,
162
- elementTranspiler,
163
- runtime,
164
- );
165
- if (preRenderData) preRenderDataMap[id] = preRenderData;
166
- }
167
-
168
- if (
169
- node instanceof BlockErrorNode ||
170
- node instanceof InlinerErrorNode
171
- ) {
172
- logParseError(node.name, node.error);
173
- return;
174
- }
175
- },
176
- });
177
- }
178
-
179
173
  return {
180
174
  biCode: finalContent,
181
- preRenderData: preRenderDataMap,
175
+ renderDataStorage,
182
176
  };
183
177
  }
184
178
 
@@ -1,6 +1,5 @@
1
1
  import { BlockNode, type ElementNode } from '@bitran-js/core';
2
2
  import type { BitranTranspiler } from '@bitran-js/transpiler';
3
-
4
3
  import {
5
4
  parseBitranLocation,
6
5
  parsePartialBitranLocation,
@@ -34,11 +33,6 @@ export type TraverseStepFn = (payload: {
34
33
 
35
34
  export type TraverseLeaveFn = (payload: { _location: string }) => Promise<any>;
36
35
 
37
- /**
38
- * This operation is heavy as fuck.
39
- * It consumes a lot of memory and is slow.
40
- * Use as rarely as possible!
41
- */
42
36
  export async function traverseInclude(
43
37
  includeNode: IncludeNode,
44
38
  context: BitranContext,
@@ -52,27 +46,32 @@ export async function traverseInclude(
52
46
  parsePartialBitranLocation(includeNode.id, context.location),
53
47
  );
54
48
 
49
+ // Always use absolute locations as keys for travelMap to avoid infinite loop bugs
50
+ const absEntryLocation = toAbsoluteLocation(
51
+ entryLocation,
52
+ context.location.path!,
53
+ getNavBookIds(),
54
+ );
55
+
55
56
  const travelMap: Record<string, string | null> = {
56
- // Not displayed when error, but needed for checking infinite loops
57
- [entryLocation]: null,
57
+ [absEntryLocation]: null,
58
58
  };
59
59
 
60
60
  try {
61
61
  await _traverseStep(
62
62
  includeNode,
63
- entryLocation,
63
+ absEntryLocation,
64
64
  context.aliases,
65
65
  listeners,
66
66
  travelMap,
67
67
  );
68
68
  } catch (message) {
69
- let finalMessage = `Include Traversal Error!\n\n${message}\n\n`;
69
+ let finalMessage = `Include Traversal Error!\n\n${message instanceof Error ? message.message : message}\n\n`;
70
70
 
71
- for (const [location, includeTarget] of Object.entries(
72
- travelMap,
73
- ).reverse()) {
71
+ // Print the traversal path in the order of traversal
72
+ for (const location of Object.keys(travelMap)) {
73
+ const includeTarget = travelMap[location];
74
74
  if (includeTarget === null) continue;
75
-
76
75
  finalMessage += `at "${printIncludeTarget(includeTarget)}" in "${location}"\n`;
77
76
  }
78
77
 
@@ -110,11 +109,16 @@ async function _traverseStep(
110
109
  );
111
110
  } catch (error) {
112
111
  travelMap[location] = includeNode.parseData.location;
113
- throw error;
112
+ throw new Error(
113
+ `Failed to resolve include target location "${includeNode.parseData.location}" from "${location}": ${error instanceof Error ? error.message : error}`,
114
+ );
114
115
  }
115
116
 
117
+ // Use only absolute locations as keys for infinite loop detection
116
118
  if (includeTargetLocation in travelMap)
117
- throw `Include "${printIncludeTarget(includeNode.parseData.location)}" targets "${includeTargetLocation}" which creates infinite loop!`;
119
+ throw new Error(
120
+ `Include "${printIncludeTarget(includeNode.parseData.location)}" targets "${includeTargetLocation}" which creates infinite loop!`,
121
+ );
118
122
 
119
123
  travelMap[location] = includeNode.parseData.location;
120
124
 
@@ -127,7 +131,9 @@ async function _traverseStep(
127
131
  });
128
132
 
129
133
  if (!dbUnique)
130
- throw `Include "${printIncludeTarget(includeNode.parseData.location)}" is targeting to non-existing unique "${includeTargetLocation}"!`;
134
+ throw new Error(
135
+ `Include "${printIncludeTarget(includeNode.parseData.location)}" is targeting non-existing unique "${includeTargetLocation}"!`,
136
+ );
131
137
 
132
138
  //
133
139
  // Creating Bitran core within loaded unique's location context.
@@ -148,18 +154,19 @@ async function _traverseStep(
148
154
  }),
149
155
  );
150
156
 
151
- listeners.enter &&
152
- (await listeners.enter({
157
+ if (listeners.enter) {
158
+ await listeners.enter({
153
159
  _biCode: dbUnique.content,
154
160
  _location: includeTargetLocation,
155
161
  _bitranTranspiler: bitranTranspiler,
156
- }));
162
+ });
163
+ }
157
164
 
158
165
  //
159
166
  // Parsing unique content and sub-traversing all includes if any.
160
167
  //
161
168
 
162
- let stepErrorMessage: string | undefined;
169
+ let stepErrorMessage: string | Error | undefined;
163
170
 
164
171
  await bitranTranspiler.parser.parse(dbUnique.content, {
165
172
  step: async (node) => {
@@ -175,13 +182,13 @@ async function _traverseStep(
175
182
  .join('__');
176
183
 
177
184
  if (!(node instanceof IncludeNode)) {
178
- listeners.step &&
179
- (await listeners.step({
185
+ if (listeners.step) {
186
+ await listeners.step({
180
187
  _location: includeTargetLocation,
181
188
  _node: node,
182
189
  _bitranTranspiler: bitranTranspiler,
183
- }));
184
-
190
+ });
191
+ }
185
192
  return;
186
193
  }
187
194
 
@@ -193,8 +200,9 @@ async function _traverseStep(
193
200
  listeners,
194
201
  travelMap,
195
202
  );
196
- } catch (message: any) {
197
- stepErrorMessage = message;
203
+ } catch (err: any) {
204
+ stepErrorMessage =
205
+ err instanceof Error ? err : new Error(String(err));
198
206
  }
199
207
  },
200
208
  });
@@ -205,10 +213,11 @@ async function _traverseStep(
205
213
  // Leaving from current Include
206
214
  //
207
215
 
208
- listeners.leave &&
209
- (await listeners.leave({
216
+ if (listeners.leave) {
217
+ await listeners.leave({
210
218
  _location: includeTargetLocation,
211
- }));
219
+ });
220
+ }
212
221
  }
213
222
 
214
223
  function printIncludeTarget(target: string) {
@@ -4,6 +4,10 @@ import {
4
4
  headingName,
5
5
  HeadingNode,
6
6
  } from '@erudit-js/bitran-elements/heading/shared';
7
+ import {
8
+ ProblemNode,
9
+ ProblemsNode,
10
+ } from '@erudit-js/bitran-elements/problem/shared';
7
11
 
8
12
  import type { Toc } from '@erudit/shared/bitran/toc';
9
13
  import { ERUDIT_SERVER } from '@server/global';
@@ -58,6 +62,16 @@ export async function getBitranToc(location: BitranLocation) {
58
62
  return 0;
59
63
  };
60
64
 
65
+ // Problems by default are in TOC
66
+ if (node instanceof ProblemsNode || node instanceof ProblemNode) {
67
+ toc.push({
68
+ ...tocItemBase,
69
+ level: notHeadingLevel(),
70
+ title: node.meta?.title || node.parseData.info.title,
71
+ });
72
+ return;
73
+ }
74
+
61
75
  if (
62
76
  ERUDIT_SERVER.CONFIG.bitran?.toc?.includes(node.name) ||
63
77
  node.meta?.toc
@@ -4,7 +4,9 @@ import { ERUDIT_SERVER } from '@server/global';
4
4
  export async function close() {
5
5
  debug.start('Shutting down server...');
6
6
 
7
- await ERUDIT_SERVER.DB?.destroy();
7
+ if (ERUDIT_SERVER.DB && ERUDIT_SERVER.DB.isInitialized) {
8
+ await ERUDIT_SERVER.DB.destroy();
9
+ }
8
10
 
9
11
  logger.success('Server shut down gracefully!');
10
12
  }
@@ -11,6 +11,7 @@ import {
11
11
  import { resolvePaths } from '@erudit-js/cog/kit';
12
12
 
13
13
  import { PROJECT_DIR } from '#erudit/globalPaths';
14
+ import { scanContentPaths } from '@erudit/utils/contentPath';
14
15
  import { stress } from '@erudit/utils/stress';
15
16
 
16
17
  import { debug, logger } from '@server/logger';
@@ -31,7 +32,6 @@ import { contentItemPath } from './path';
31
32
  import { buildBook } from './type/book';
32
33
  import { buildGroup } from './type/group';
33
34
  import { buildTopic } from './type/topic';
34
- import { scanFiles } from './files';
35
35
 
36
36
  const typeBuilders: Record<ContentType, Function> = {
37
37
  book: buildBook,
@@ -69,7 +69,7 @@ export async function buildContent() {
69
69
  }
70
70
 
71
71
  async function scanContentFiles() {
72
- const contentFiles = scanFiles(PROJECT_DIR + '/content');
72
+ const contentFiles = scanContentPaths(PROJECT_DIR + '/content');
73
73
  for (const [path, fullPath] of Object.entries(contentFiles)) {
74
74
  const dbFile = new DbFile();
75
75
  dbFile.path = path;
@@ -5,6 +5,7 @@ import { setupLanguage } from './jobs/language';
5
5
  import { buildContributors } from './jobs/contributors';
6
6
  import { buildNav } from './jobs/nav';
7
7
  import { buildContent } from './jobs/content/generic';
8
+ import { resetDatabase } from '../db/reset';
8
9
 
9
10
  let initial = true;
10
11
 
@@ -16,6 +17,7 @@ export async function build() {
16
17
  initial = false;
17
18
  }
18
19
 
20
+ await resetDatabase();
19
21
  await setupLanguage();
20
22
  await buildContributors();
21
23
  await buildNav();
@@ -26,6 +26,7 @@ const rebuildDelay = 300;
26
26
  let watcher: FSWatcher;
27
27
  let rebuildTimeout: any;
28
28
  let rebuildRequested = false;
29
+ let pendingRebuild = false;
29
30
 
30
31
  export async function setupRebuildWatcher() {
31
32
  if (watcher) return;
@@ -41,7 +42,11 @@ export async function setupRebuildWatcher() {
41
42
  }
42
43
 
43
44
  export async function requestServerRebuild(reason?: string) {
44
- if (rebuildRequested) return;
45
+ // If rebuild is in progress, set flag for another rebuild after completion
46
+ if (rebuildRequested) {
47
+ pendingRebuild = true;
48
+ return;
49
+ }
45
50
 
46
51
  clearTimeout(rebuildTimeout);
47
52
  rebuildTimeout = setTimeout(async () => {
@@ -51,5 +56,11 @@ export async function requestServerRebuild(reason?: string) {
51
56
  logger.info(`Rebuilding server data!${reason ? ` ${reason}` : ''}\n\n`);
52
57
  await build();
53
58
  rebuildRequested = false;
59
+
60
+ // If changes were detected during rebuild, start another rebuild
61
+ if (pendingRebuild) {
62
+ pendingRebuild = false;
63
+ requestServerRebuild('Changes detected during previous rebuild');
64
+ }
54
65
  }, rebuildDelay);
55
66
  }
@@ -0,0 +1,12 @@
1
+ import { ERUDIT_SERVER } from '@server/global';
2
+
3
+ export async function resetDatabase() {
4
+ const db = ERUDIT_SERVER.DB;
5
+
6
+ if (!db) {
7
+ return;
8
+ }
9
+
10
+ await db.dropDatabase();
11
+ await db.synchronize(true);
12
+ }
@@ -1,3 +1,4 @@
1
+ import { rmSync } from 'node:fs';
1
2
  import { DataSource } from 'typeorm';
2
3
 
3
4
  import { PROJECT_DIR } from '#erudit/globalPaths';
@@ -13,12 +14,17 @@ import { DbHash } from './entities/Hash';
13
14
  import { DbTopic } from './entities/Topic';
14
15
  import { DbUnique } from './entities/Unique';
15
16
  import { DbFile } from './entities/File';
17
+ import { logger } from '../logger';
16
18
 
17
19
  export async function setupDatabase() {
20
+ rmSync(PROJECT_DIR + '/.erudit/data.sqlite', { force: true });
21
+
18
22
  ERUDIT_SERVER.DB = new DataSource({
19
23
  type: 'sqlite',
24
+ enableWAL: true,
20
25
  database: PROJECT_DIR + '/.erudit/data.sqlite',
21
26
  synchronize: true,
27
+ dropSchema: true,
22
28
  entities: [
23
29
  DbBook,
24
30
  DbContent,
@@ -32,5 +38,12 @@ export async function setupDatabase() {
32
38
  ],
33
39
  });
34
40
 
35
- await ERUDIT_SERVER.DB.initialize();
41
+ try {
42
+ // Wait before creating connection in case fast server restarts
43
+ // Otherwise, it may crash the whole Node process
44
+ await new Promise((resolve) => setTimeout(resolve, 500));
45
+ await ERUDIT_SERVER.DB.initialize();
46
+ } catch (error) {
47
+ logger.error('Error creating database connection:', error);
48
+ }
36
49
  }
@@ -1,6 +1,6 @@
1
- import type { PreRenderData } from '@bitran-js/transpiler';
1
+ import type { RenderDataStorage } from '@bitran-js/core';
2
2
 
3
3
  export interface StringBitranContent {
4
4
  biCode: string;
5
- preRenderData?: Record<string, PreRenderData>;
5
+ renderDataStorage: RenderDataStorage;
6
6
  }
@@ -0,0 +1,67 @@
1
+ import { readdirSync, statSync } from 'fs';
2
+
3
+ // A directory is considered a "content directory" if it contains book, group, or topic with a file having a .js or .ts extension.
4
+ // When a directory qualifies as a content directory, its name is simplified by removing any leading digits and a '+' or '-' (e.g., "29+foo" becomes "foo").
5
+
6
+ const contentFileRegex = /^(book|group|topic)\.(js|ts)$/i;
7
+
8
+ function normalizePath(path: string): string {
9
+ return path.replace(/\\/g, '/');
10
+ }
11
+
12
+ export function isContentDirectory(directory: string): boolean {
13
+ if (!statSync(directory).isDirectory()) {
14
+ return false;
15
+ }
16
+
17
+ return readdirSync(directory).some((file) => {
18
+ return contentFileRegex.test(file);
19
+ });
20
+ }
21
+
22
+ export function getContentPath(path: string, cwd?: string): string {
23
+ path = normalizePath(path);
24
+ cwd = cwd ? normalizePath(cwd) : undefined;
25
+
26
+ const unresolvedParts = path.split('/').filter(Boolean);
27
+ const resolvedParts: string[] = [];
28
+
29
+ let currentPath = cwd || '';
30
+ while (unresolvedParts.length) {
31
+ const currentPart = unresolvedParts.shift()!;
32
+ currentPath += '/' + currentPart;
33
+ if (isContentDirectory(currentPath)) {
34
+ resolvedParts.push(currentPart.replace(/^\d+(?:\+|-)/, ''));
35
+ } else {
36
+ resolvedParts.push(currentPart);
37
+ }
38
+ }
39
+
40
+ return resolvedParts.join('/');
41
+ }
42
+
43
+ export function scanContentPaths(directory: string): Record<string, string> {
44
+ directory = normalizePath(directory);
45
+
46
+ const map: Record<string, string> = {};
47
+
48
+ function traverse(currentPath: string) {
49
+ const files = readdirSync(currentPath);
50
+
51
+ for (const file of files) {
52
+ const fullPath = currentPath + '/' + file;
53
+ const relativePath = fullPath.slice(directory.length + 1);
54
+ const stat = statSync(fullPath);
55
+
56
+ if (stat.isDirectory()) {
57
+ traverse(fullPath);
58
+ } else {
59
+ map[getContentPath(relativePath, directory)] = relativePath;
60
+ }
61
+ }
62
+ }
63
+
64
+ traverse(directory);
65
+
66
+ return map;
67
+ }
@@ -1,79 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
-
4
- interface FileMap {
5
- [simplifiedPath: string]: string;
6
- }
7
-
8
- // Regex to check if a directory is a "content directory".
9
- const contentFileRegex = /^(book|group|topic)\.(js|ts)$/i;
10
-
11
- // Regex to simplify directory names (remove leading digits + '+' or '-').
12
- const simplifyDirRegex = /^\d+(?:\+|-)/;
13
-
14
- /**
15
- * Recursively scans files starting from `dirPath` and builds a mapping where:
16
- * - The key is the "simplified" path using forward slashes.
17
- * - The value is the relative filesystem path, also using forward slashes.
18
- *
19
- * A directory is considered a "content directory" if it contains any file that matches:
20
- * book, group, or topic with a .js or .ts extension.
21
- * When a directory qualifies as a content directory, its name is simplified
22
- * by removing any leading digits and a '+' or '-' (e.g., "29+foo" becomes "foo").
23
- *
24
- * @param dirPath The root directory to scan.
25
- * @returns An object mapping simplified file paths to relative file paths.
26
- */
27
- export function scanFiles(dirPath: string): FileMap {
28
- const result: FileMap = {};
29
- const absoluteRoot = path.resolve(dirPath); // Ensure absolute root path
30
-
31
- function isContentDirectory(directory: string): boolean {
32
- try {
33
- return fs
34
- .readdirSync(directory)
35
- .some((entry) => contentFileRegex.test(entry));
36
- } catch {
37
- return false;
38
- }
39
- }
40
-
41
- function traverse(currentPath: string, simplifiedParts: string[]) {
42
- let currentSimplifiedParts = simplifiedParts;
43
- if (currentPath !== absoluteRoot) {
44
- let dirName = path.basename(currentPath);
45
- if (isContentDirectory(currentPath)) {
46
- dirName = dirName.replace(simplifyDirRegex, '');
47
- }
48
- currentSimplifiedParts = [...simplifiedParts, dirName];
49
- }
50
-
51
- for (const entry of fs.readdirSync(currentPath)) {
52
- const fullPath = path.join(currentPath, entry);
53
- const relativePath = path
54
- .relative(absoluteRoot, fullPath)
55
- .replace(/\\/g, '/'); // Always forward slashes
56
- const stat = fs.statSync(fullPath);
57
-
58
- if (stat.isDirectory()) {
59
- traverse(fullPath, currentSimplifiedParts);
60
- } else if (stat.isFile()) {
61
- const simplifiedFilePath = path.posix.join(
62
- ...currentSimplifiedParts,
63
- entry,
64
- );
65
-
66
- // Remove first repeating part in the key
67
- const keyParts = simplifiedFilePath.split('/');
68
- if (keyParts.length > 1 && keyParts[0] === keyParts[1]) {
69
- keyParts.shift(); // Remove first repeating part
70
- }
71
-
72
- result[keyParts.join('/')] = relativePath;
73
- }
74
- }
75
- }
76
-
77
- traverse(absoluteRoot, []);
78
- return result;
79
- }