@eventcatalog/core 2.8.12 → 2.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @eventcatalog/core
2
2
 
3
+ ## 2.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 70d27a7: feat(core): support nested folder structures for domains, services and messages
8
+
3
9
  ## 2.8.12
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/event-catalog/eventcatalog.git"
7
7
  },
8
8
  "type": "module",
9
- "version": "2.8.12",
9
+ "version": "2.9.0",
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },
@@ -3,277 +3,100 @@ import * as path from 'node:path';
3
3
  import fs from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import os from 'node:os';
6
- import { verifyRequiredFieldsAreInCatalogConfigFile } from './eventcatalog-config-file-utils.js';
6
+ import { verifyRequiredFieldsAreInCatalogConfigFile, addPropertyToFrontMatter } from './eventcatalog-config-file-utils.js';
7
+ import { mapCatalogToAstro } from './map-catalog-to-astro.js';
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const scriptsDir = path.dirname(__filename);
10
11
 
11
- const getTargetPath = (source, target, type, file) => {
12
- const relativePath = path.relative(source, file);
13
- const cleanedRelativePath = relativePath.split(type);
14
- const targetForEvents = path.join(type, cleanedRelativePath[1]);
15
- return path.join(target, targetForEvents);
16
- };
17
-
18
- const ensureDirSync = async (filePath) => {
19
- const dir = path.dirname(filePath);
20
-
21
- if (!fs.existsSync(dir)) {
22
- fs.mkdirSync(dir, { recursive: true });
23
- }
24
- };
25
-
26
- const copyFiles = async ({ source, target, catalogFilesDir, pathToMarkdownFiles, pathToAllFiles, type, ignore = null }) => {
27
- // Find all the event files
28
- const markdownFiles = await glob(pathToMarkdownFiles, {
29
- nodir: true,
30
- windowsPathsNoEscape: os.platform() == 'win32',
31
- ignore: ignore,
32
- });
33
- const files = await glob(pathToAllFiles, {
34
- ignore: {
35
- ignored: (p) => /\.md$/.test(p.name),
36
- },
12
+ const copyFiles = async (source, target) => {
13
+ const files = await glob(path.join(source, '**'), {
37
14
  nodir: true,
38
15
  windowsPathsNoEscape: os.platform() == 'win32',
39
16
  });
40
17
 
41
- const publicDir = path.join(target, '../../public/generated');
42
-
43
- // Copy markdown files into the astro content (collection) folder
44
- for (const file of markdownFiles) {
45
- let fileTarget = target;
46
-
47
- // If they are change logs they need to go into their own content folder
48
- if (file.includes('changelog.md')) {
49
- fileTarget = path.join(target, 'changelogs');
50
- }
51
-
52
- const targetPath = getTargetPath(source, fileTarget, type, file);
53
-
54
- //ensure the directory exists
55
- ensureDirSync(path.dirname(targetPath));
56
-
57
- fs.cpSync(file, targetPath.replace('index.md', 'index.mdx').replace('changelog.md', 'changelog.mdx'));
58
- }
59
-
60
- // Copy all other files (non markdown) files into catalog-files directory (non collection)
61
18
  for (const file of files) {
62
- // Ignore any md files
63
- if (file.endsWith('.md')) {
64
- continue;
65
- }
66
-
67
- const relativePath = path.relative(source, file);
68
- const cleanedRelativePath = relativePath.split(type);
69
- if (!cleanedRelativePath[1]) continue;
70
- const targetForEvents = path.join(type, cleanedRelativePath[1]);
71
-
72
- // Catalog-files-directory
73
- const targetPath = path.join(catalogFilesDir, targetForEvents);
74
- if (!fs.existsSync(path.dirname(targetPath))) {
75
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
76
- }
77
- await fs.cpSync(file, targetPath);
19
+ mapCatalogToAstro({
20
+ filePath: file,
21
+ astroDir: target,
22
+ projectDir: source,
23
+ })
24
+ .map((astroPath) => {
25
+ fs.cpSync(file, astroPath);
26
+ return { oldPath: file, newPath: astroPath };
27
+ })
28
+ .map(({ oldPath, newPath }) => {
29
+ if (!oldPath.endsWith('.md') && !oldPath.endsWith('.mdx')) return;
30
+ try {
31
+ // EventCatalog requires the original path to be in the frontmatter for Schemas and Changelogs
32
+ const content = fs.readFileSync(newPath, 'utf-8');
33
+ const frontmatter = addPropertyToFrontMatter(content, 'pathToFile', oldPath);
34
+ fs.writeFileSync(newPath, frontmatter);
35
+ } catch (error) {
36
+ // silent fail
37
+ }
38
+ });
39
+ }
40
+ };
78
41
 
79
- // // // Public Directory
80
- const publicTargetPath = path.join(publicDir, targetForEvents);
42
+ const ensureAstroCollectionNotEmpty = async (astroDir) => {
43
+ // TODO: maybe import collections from `src/content/config.ts`...
44
+ const COLLECTIONS = ['events', 'commands', 'services', 'users', 'teams', 'domains', 'flows', 'pages', 'changelogs'];
81
45
 
82
- if (!fs.existsSync(path.dirname(publicTargetPath))) {
83
- fs.mkdirSync(path.dirname(publicTargetPath), { recursive: true });
84
- }
46
+ // Check empty collections
47
+ const emptyCollections = [];
48
+ for (const collection of COLLECTIONS) {
49
+ const markdownFiles = await glob(path.join(astroDir, 'src/content/', collection, '**'), {
50
+ nodir: true,
51
+ windowsPathsNoEscape: os.platform() == 'win32',
52
+ });
85
53
 
86
- await fs.cpSync(file, publicTargetPath);
54
+ if (markdownFiles.length === 0) emptyCollections.push(collection);
87
55
  }
88
56
 
89
- // Check if the directory is empty. EC (astro collections) requires at least 1 item in the collection
90
- // insert empty one that is filtered out
91
- if (markdownFiles.length === 0) {
92
- const defaultCollectionFilesDir = path.join(scriptsDir, 'default-files-for-collections');
93
- const defaultFile = path.join(defaultCollectionFilesDir, `${type}.md`);
94
- const targetDir = path.join(target, type);
57
+ // Hydrate empty collections
58
+ const defaultCollectionFilesDir = path.join(scriptsDir, 'default-files-for-collections');
59
+ for (const collection of emptyCollections) {
60
+ const defaultFile = path.join(defaultCollectionFilesDir, `${collection}.md`);
61
+ const targetDir = path.join(astroDir, 'src/content/', collection);
95
62
  if (!fs.existsSync(targetDir)) {
96
63
  fs.mkdirSync(targetDir, { recursive: true });
97
64
  }
98
- await fs.cpSync(defaultFile, path.join(targetDir, `${type}.md`));
65
+ fs.cpSync(defaultFile, path.join(targetDir, `${collection}.md`));
99
66
  }
100
67
  };
101
68
 
102
- const copyComponents = async ({ source, target }) => {
103
- const componentsDir = path.join(source, 'components');
104
- const componentsTarget = path.join(target, '../custom-defined-components');
105
-
106
- // Clear the component directory before we copy files over
107
- if (fs.existsSync(componentsTarget)) {
108
- await fs.rmSync(componentsTarget, { recursive: true });
109
- }
110
-
111
- if (!fs.existsSync(componentsDir) || !fs.statSync(componentsDir).isDirectory()) {
112
- return;
113
- }
69
+ export const catalogToAstro = async (source, astroDir) => {
70
+ const astroContentDir = path.join(astroDir, 'src/content/');
114
71
 
115
- // Copy files
116
- await fs.cpSync(componentsDir, componentsTarget, { recursive: true });
117
- };
72
+ console.log(path.join(astroContentDir, 'config.ts'));
118
73
 
119
- export const catalogToAstro = async (source, astroContentDir, catalogFilesDir) => {
120
74
  // Config file
121
75
  const astroConfigFile = fs.readFileSync(path.join(astroContentDir, 'config.ts'));
122
76
 
123
77
  // Clear the astro directory before we copy files over
124
- await fs.rmSync(astroContentDir, { recursive: true });
78
+ fs.rmSync(astroContentDir, { recursive: true });
125
79
 
126
80
  // Create the folder again
127
81
  fs.mkdirSync(astroContentDir);
128
82
 
129
83
  // Write config file back
130
- // ensureDirSync(astroContentDir);
131
84
  fs.writeFileSync(path.join(astroContentDir, 'config.ts'), astroConfigFile);
132
85
 
133
- // Copy the public directory files into the astro public directory
134
- const usersPublicDirectory = path.join(source, 'public');
135
- const astroPublicDir = path.join(astroContentDir, '../../public');
136
-
137
- if (fs.existsSync(usersPublicDirectory)) {
138
- // fs.mkdirSync(astroPublicDir, { recursive: true });
139
- fs.cpSync(usersPublicDirectory, astroPublicDir, { recursive: true });
140
- }
141
-
142
- // Copy all the event files over
143
- await copyFiles({
144
- source,
145
- target: astroContentDir,
146
- catalogFilesDir,
147
- pathToMarkdownFiles: [
148
- path.join(source, 'events/**/**/index.md'),
149
- path.join(source, 'services/**/events/**/index.md'),
150
- path.join(source, 'domains/**/events/**/index.md'),
151
- path.join(source, 'events/**/**/changelog.md'),
152
- ],
153
- pathToAllFiles: [
154
- path.join(source, 'events/**'),
155
- path.join(source, 'services/**/events/**'),
156
- path.join(source, 'domains/**/events/**'),
157
- ],
158
- type: 'events',
159
- });
160
-
161
- // // copy commands
162
- await copyFiles({
163
- source,
164
- target: astroContentDir,
165
- catalogFilesDir,
166
- pathToMarkdownFiles: [
167
- path.join(source, 'commands/**/**/index.md'),
168
- path.join(source, 'services/**/commands/**/index.md'),
169
- path.join(source, 'domains/**/commands/**/index.md'),
170
- path.join(source, 'commands/**/**/changelog.md'),
171
- ],
172
- pathToAllFiles: [
173
- path.join(source, 'commands/**'),
174
- path.join(source, 'services/**/commands/**'),
175
- path.join(source, 'domains/**/commands/**'),
176
- ],
177
- type: 'commands',
178
- });
179
-
180
- // // Copy all the service files over
181
- await copyFiles({
182
- source,
183
- target: astroContentDir,
184
- catalogFilesDir,
185
- pathToMarkdownFiles: [
186
- path.join(source, 'services/**/**/index.md'),
187
- path.join(source, 'domains/**/services/**/index.md'),
188
- path.join(source, 'services/**/**/changelog.md'),
189
- ],
190
- pathToAllFiles: [path.join(source, 'services/**'), path.join(source, 'domains/**/services/**')],
191
- ignore: [
192
- path.join(source, 'services/**/events/**'),
193
- path.join(source, 'services/**/commands/**'),
194
- path.join(source, 'services/**/flows/**'),
195
- ],
196
- type: 'services',
197
- });
198
-
199
- // // Copy all the domain files over
200
- await copyFiles({
201
- source,
202
- target: astroContentDir,
203
- catalogFilesDir,
204
- pathToMarkdownFiles: [path.join(source, 'domains/**/**/index.md'), path.join(source, 'domains/**/**/changelog.md')],
205
- pathToAllFiles: [path.join(source, 'domains/**')],
206
- ignore: [
207
- path.join(source, 'domains/**/services/**'),
208
- path.join(source, 'domains/**/commands/**'),
209
- path.join(source, 'domains/**/events/**'),
210
- path.join(source, 'domains/**/flows/**'),
211
- ],
212
- type: 'domains',
213
- });
214
-
215
- // // Copy all the flow files over
216
- await copyFiles({
217
- source,
218
- target: astroContentDir,
219
- catalogFilesDir,
220
- pathToMarkdownFiles: [
221
- path.join(source, 'flows/**/**/index.md'),
222
- path.join(source, 'flows/**/**/changelog.md'),
223
- path.join(source, 'services/**/flows/**/index.md'),
224
- path.join(source, 'domains/**/flows/**/index.md'),
225
- ],
226
- pathToAllFiles: [
227
- path.join(source, 'flows/**'),
228
- path.join(source, 'services/**/flows/**'),
229
- path.join(source, 'domains/**/flows/**'),
230
- ],
231
- type: 'flows',
232
- });
233
-
234
- // // Copy all the users
235
- await copyFiles({
236
- source,
237
- target: astroContentDir,
238
- catalogFilesDir,
239
- pathToMarkdownFiles: [path.join(source, 'users/**/*.md')],
240
- pathToAllFiles: [],
241
- type: 'users',
242
- });
243
-
244
- // // Copy all the teams
245
- await copyFiles({
246
- source,
247
- target: astroContentDir,
248
- catalogFilesDir,
249
- pathToMarkdownFiles: [path.join(source, 'teams/**/*.md')],
250
- pathToAllFiles: [],
251
- type: 'teams',
252
- });
253
-
254
- // // Copy all the pages (optional)
255
- await copyFiles({
256
- source,
257
- target: astroContentDir,
258
- catalogFilesDir,
259
- pathToMarkdownFiles: [path.join(source, 'pages/**/*.md')],
260
- pathToAllFiles: [],
261
- type: 'pages',
262
- });
263
-
264
- // Copy the components if they are defined
265
- await copyComponents({ source, target: astroContentDir, catalogFilesDir });
266
-
267
86
  // Verify required fields are in the catalog config file
268
87
  await verifyRequiredFieldsAreInCatalogConfigFile(source);
88
+
89
+ await copyFiles(source, astroDir);
90
+
91
+ // Check if the directory is empty. EC (astro collections) requires at least 1 item in the collection
92
+ // insert empty one that is filtered out
93
+ await ensureAstroCollectionNotEmpty(astroDir);
269
94
  };
270
95
 
271
96
  if (process.env.NODE_ENV !== 'test') {
272
97
  // // Get the project directory of the source
273
98
  const source = process.env.PROJECT_DIR;
99
+ const astroDir = process.env.CATALOG_DIR;
274
100
 
275
- const astroContentDir = path.join(process.env.CATALOG_DIR, 'src/content');
276
- const catalogFilesDir = path.join(process.env.CATALOG_DIR, 'src/catalog-files');
277
-
278
- catalogToAstro(source, astroContentDir, catalogFilesDir);
101
+ catalogToAstro(source, astroDir);
279
102
  }
@@ -80,3 +80,21 @@ export const verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirector
80
80
  // fail silently, it's overly important
81
81
  }
82
82
  };
83
+
84
+ export function addPropertyToFrontMatter(input, newProperty, newValue) {
85
+ // Split the input into front matter and content
86
+ const [_, frontMatter, content] = input.split('---');
87
+
88
+ // Parse the front matter
89
+ const frontMatterLines = frontMatter.trim().split('\n');
90
+ const updatedFrontMatterLines = [...frontMatterLines];
91
+
92
+ // Add the new property
93
+ updatedFrontMatterLines.push(`${newProperty}: ${newValue}`);
94
+
95
+ // Reconstruct the updated input
96
+ const updatedFrontMatter = updatedFrontMatterLines.join('\n');
97
+ const updatedInput = `---\n${updatedFrontMatter}\n---\n${content.trim()}`;
98
+
99
+ return updatedInput;
100
+ }
@@ -0,0 +1,162 @@
1
+ import path from 'node:path';
2
+
3
+ const COLLECTION_KEYS = ['events', 'commands', 'services', 'users', 'teams', 'domains', 'flows', 'pages', 'changelogs'];
4
+
5
+ /**
6
+ * @typedef {Object} MapCatalogToAstroParams
7
+ * @prop {string} filePath The catalog file path
8
+ * @prop {string} astroDir The astro directory
9
+ * @prop {string} projectDir The user's project directory
10
+ */
11
+
12
+ /**
13
+ *
14
+ * @param {MapCatalogToAstroParams} params
15
+ * @returns {string[]} The astro file paths
16
+ */
17
+ export function mapCatalogToAstro({ filePath, astroDir, projectDir }) {
18
+ const relativeFilePath = removeBasePath(filePath, projectDir);
19
+
20
+ if (!isCatalogRelated(relativeFilePath)) {
21
+ return [];
22
+ }
23
+
24
+ const baseTargetPaths = getBaseTargetPaths(relativeFilePath);
25
+ const relativeTargetPath = getRelativeTargetPath(relativeFilePath);
26
+
27
+ return baseTargetPaths.map((base) =>
28
+ path.join(astroDir, base, relativeTargetPath.replace('index.md', 'index.mdx').replace('changelog.md', 'changelog.mdx'))
29
+ );
30
+ }
31
+
32
+ /**
33
+ *
34
+ * @param {string} fullPath
35
+ * @param {string} basePath
36
+ * @returns {string} The fullPath without the basePath
37
+ */
38
+ function removeBasePath(fullPath, basePath) {
39
+ const relativePath = path.relative(basePath, fullPath);
40
+ return relativePath.startsWith('..') ? fullPath : relativePath;
41
+ }
42
+
43
+ /**
44
+ * Check if the key is an ASTRO COLLECTION KEY
45
+ * @param {string} key
46
+ * @returns {boolean}
47
+ */
48
+ function isCollectionKey(key) {
49
+ return COLLECTION_KEYS.includes(key);
50
+ }
51
+
52
+ /**
53
+ * Checks whether the given path is a configuration file, styles file, public asset file or collection file.
54
+ * @param {string} filePath - The file path without the projectDir prefix.
55
+ * @returns {boolean}
56
+ */
57
+ function isCatalogRelated(filePath) {
58
+ const filePathArr = filePath.split(path.sep).filter(Boolean);
59
+
60
+ if (
61
+ [
62
+ 'eventcatalog.config.js', // config file at root
63
+ 'eventcatalog.styles.css', // custom styles file at root
64
+ 'components', // custom components
65
+ 'public', // public assets
66
+ ...COLLECTION_KEYS,
67
+ ].includes(filePathArr[0])
68
+ ) {
69
+ return true;
70
+ }
71
+
72
+ return false;
73
+ }
74
+
75
+ /**
76
+ * Generates the base target path accordingly to the file path.
77
+ * @param {string} filePath The path to the file without PROJECT_DIR prefix.
78
+ * @returns {Array.<'src/content'|'public/generated'|'src/catalog-files'|'/'>} The base target path.
79
+ */
80
+ function getBaseTargetPaths(filePath) {
81
+ const filePathArr = filePath.split(path.sep).filter(Boolean);
82
+
83
+ // Collection
84
+ if (isCollectionKey(filePathArr[0])) {
85
+ // Changelogs files
86
+ if (filePathArr[filePathArr.length - 1] == 'changelog.md') {
87
+ return [path.join('src', 'content', 'changelogs')];
88
+ }
89
+
90
+ // Markdown files
91
+ if (filePathArr[filePathArr.length - 1].match(/\.md$/)) {
92
+ return [path.join('src', 'content')];
93
+ }
94
+
95
+ // This is a workaround to differentiate between a file and a directory.
96
+ // Of course this is not the best solution. But how differentiate? `fs.stats`
97
+ // could be used, but sometimes the filePath references a deleted file/directory
98
+ // which causes an error when using `fs.stats`.
99
+ const hasExtension = (str) => /\.[a-zA-Z0-9]{2,}$/.test(str);
100
+
101
+ // Assets files
102
+ if (hasExtension(filePath)) {
103
+ return [path.join('public', 'generated'), path.join('src', 'catalog-files')];
104
+ }
105
+
106
+ /**
107
+ * @parcel/watcher throttle and coalesce events for performance reasons.
108
+ *
109
+ * Consider the following:
110
+ * The user deletes a large `services/` dir from eventcatalog.
111
+ * The @parcel/watcher could emit only one delete event of the
112
+ * `services/` directory.
113
+ *
114
+ * In this situation we need delete all files from
115
+ * - `public/generated/services/`
116
+ * - `src/catalog-files/services/`
117
+ * - `src/content/services/`
118
+ *
119
+ * TODO: What happens if services contains commands/events inside of it??? How handle this?
120
+ */
121
+ // Directories
122
+ return [path.join('public', 'generated'), path.join('src', 'catalog-files'), path.join('src', 'content')];
123
+ }
124
+
125
+ // Custom components
126
+ if (filePathArr[0] == 'components') {
127
+ return [path.join('src', 'custom-defined-components')];
128
+ }
129
+
130
+ // Public assets (public/*)
131
+ if (filePathArr[0] == 'public') {
132
+ return [path.join('public')];
133
+ }
134
+
135
+ /**
136
+ * Config files:
137
+ * - eventcatalog.config.js
138
+ * - eventcatalog.styles.css
139
+ */
140
+ return [path.join('/')];
141
+ }
142
+
143
+ /**
144
+ * Generates the path until the ASTRO_COLLECTION_KEY or the PROJECT_DIR root.
145
+ * @param {string} filePath The path to the file.
146
+ * @returns {string} The path until the COLLECTION_KEY or PROJECT_DIR root.
147
+ */
148
+ function getRelativeTargetPath(filePath) {
149
+ const filePathArr = filePath.split(path.sep).filter(Boolean);
150
+
151
+ if (filePathArr[0] == 'public' || filePathArr[0] == 'components') {
152
+ filePathArr.shift();
153
+ }
154
+
155
+ const relativePath = [];
156
+ for (let i = filePathArr.length - 1; i >= 0; i--) {
157
+ relativePath.unshift(filePathArr[i]);
158
+ if (isCollectionKey(filePathArr[i])) break;
159
+ }
160
+
161
+ return path.join(...relativePath);
162
+ }
@@ -1,69 +1,134 @@
1
- #!/usr/bin/env node
2
1
  import watcher from '@parcel/watcher';
3
- import path from 'path';
4
- import fs from 'fs';
5
- import os from 'os';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { mapCatalogToAstro } from './map-catalog-to-astro.js';
5
+ import { rimrafSync } from 'rimraf';
6
+ import { addPropertyToFrontMatter } from './eventcatalog-config-file-utils.js';
6
7
 
7
- // Where the users project is located
8
- const projectDirectory = process.env.PROJECT_DIR || process.cwd();
9
- // Where the catalog code is located.
10
- const catalogDirectory = process.env.CATALOG_DIR;
8
+ /**
9
+ * @typedef {Object} Event
10
+ * @property {string} path
11
+ * @property {"create"|"update"|"delete"} type
12
+ *
13
+ * @typedef {(err: Error | null, events: Event[]) => unknown} SubscribeCallback
14
+ */
11
15
 
12
- const contentPath = path.join(catalogDirectory, 'src', 'content');
16
+ /**
17
+ *
18
+ * @param {string} projectDirectory
19
+ * @param {string} catalogDirectory
20
+ * @param {SubscribeCallback|undefined} callback
21
+ */
22
+ export async function watch(projectDirectory, catalogDirectory, callback = undefined) {
23
+ const subscription = await watcher.subscribe(
24
+ projectDirectory,
25
+ compose(
26
+ /**
27
+ * @param {Error|null} err
28
+ * @param {Event[]} events
29
+ * @returns {unknown}
30
+ */
31
+ (err, events) => {
32
+ if (err) {
33
+ return;
34
+ }
13
35
 
14
- const watchList = ['domains', 'commands', 'events', 'services', 'teams', 'users', 'pages', 'components', 'flows'];
15
- // const absoluteWatchList = watchList.map((item) => path.join(projectDirectory, item));
36
+ for (let event of events) {
37
+ const { path: filePath, type } = event;
16
38
 
17
- // confirm folders exist before watching them
18
- const verifiedWatchList = watchList.filter((item) => fs.existsSync(path.join(projectDirectory, item)));
39
+ const astroPaths = mapCatalogToAstro({
40
+ filePath,
41
+ astroDir: catalogDirectory,
42
+ projectDir: projectDirectory,
43
+ });
19
44
 
20
- const extensionReplacer = (collection, file) => {
21
- if (collection === 'teams' || collection == 'users') return file;
22
- return file.replace('.md', '.mdx');
23
- };
45
+ for (const astroPath of astroPaths) {
46
+ switch (type) {
47
+ case 'create':
48
+ case 'update':
49
+ try {
50
+ // EventCatalog requires the original path to be in the frontmatter for Schemas and Changelogs
51
+ if (astroPath.endsWith('.mdx')) {
52
+ const content = fs.readFileSync(astroPath, 'utf-8');
53
+ const frontmatter = addPropertyToFrontMatter(content, 'pathToFile', filePath);
54
+ fs.writeFileSync(astroPath, frontmatter);
55
+ }
56
+ } catch (error) {
57
+ // silent fail
58
+ }
24
59
 
25
- for (let item of [...verifiedWatchList]) {
26
- // Listen to the users directory for any changes.
27
- watcher.subscribe(path.join(projectDirectory, item), (err, events) => {
28
- if (err) {
29
- return;
30
- }
31
- for (let event of events) {
32
- const { path: eventPath, type } = event;
33
- const file = eventPath.split(item)[1];
34
- let newPath = path.join(contentPath, item, extensionReplacer(item, file));
60
+ if (fs.statSync(filePath).isDirectory()) fs.mkdirSync(astroPath, { recursive: true });
61
+ else retryEPERM(fs.cpSync)(filePath, astroPath);
35
62
 
36
- // Check if changlogs, they need to go into their own content folder
37
- if (file.includes('changelog.md')) {
38
- newPath = newPath.replace('src/content', 'src/content/changelogs');
39
- if (os.platform() == 'win32') {
40
- newPath = newPath.replace('src\\content', 'src\\content\\changelogs');
63
+ break;
64
+ case 'delete':
65
+ retryEPERM(rimrafSync)(astroPath);
66
+ break;
67
+ }
68
+ }
41
69
  }
42
- }
70
+ },
71
+ callback
72
+ ),
73
+ {
74
+ ignore: [`**/${catalogDirectory}/!(${projectDirectory})**`],
75
+ }
76
+ );
43
77
 
44
- // Check if its a component, need to move to the correct location
45
- if (newPath.includes('components')) {
46
- newPath = newPath.replace('src/content/components', 'src/custom-defined-components');
47
- if (os.platform() == 'win32') {
48
- newPath = newPath.replace('src\\content\\components', 'src\\custom-defined-components');
49
- }
50
- }
78
+ return () => subscription.unsubscribe();
79
+ }
51
80
 
52
- // If config files have changes
53
- if (eventPath.includes('eventcatalog.config.js') || eventPath.includes('eventcatalog.styles.css')) {
54
- fs.cpSync(eventPath, path.join(catalogDirectory, file));
55
- return;
81
+ /**
82
+ *
83
+ * @param {...Function} fns
84
+ * @returns {SubscribeCallback}
85
+ */
86
+ function compose(...fns) {
87
+ return function (err, events) {
88
+ fns.filter(Boolean).forEach((fn, i) => {
89
+ try {
90
+ fn(err, events);
91
+ } catch (error) {
92
+ console.error({ error });
93
+ throw error;
56
94
  }
95
+ });
96
+ };
97
+ }
57
98
 
58
- // If markdown files or astro files copy file over to the required location
59
- if ((eventPath.endsWith('.md') || eventPath.endsWith('.astro')) && type === 'update') {
60
- fs.cpSync(eventPath, newPath);
61
- }
99
+ const MAX_RETRIES = 5;
100
+ const DELAY_MS = 100;
62
101
 
63
- // IF directory remove it
64
- if (type === 'delete') {
65
- fs.rmSync(newPath);
102
+ // In win32 some tests failed when attempting copy the file at the same time
103
+ // that another process is editing it.
104
+ function retryEPERM(fn) {
105
+ return (...args) => {
106
+ let retries = 0;
107
+
108
+ while (retries < MAX_RETRIES) {
109
+ try {
110
+ return fn(...args);
111
+ } catch (err) {
112
+ if (err.code !== 'EPERM') throw err;
113
+ setTimeout(() => {}, DELAY_MS);
114
+ retries += 1;
66
115
  }
67
116
  }
68
- });
117
+ };
118
+ }
119
+
120
+ /**
121
+ * TODO: call `watch` from the dev command.
122
+ * Calling `watch` there will avoid these if statement.
123
+ * The same could be done to `catalog-to-astro-content-directory`
124
+ */
125
+ if (process.env.NODE_ENV !== 'test') {
126
+ // Where the users project is located
127
+ const projectDirectory = process.env.PROJECT_DIR || process.cwd();
128
+ // Where the catalog code is located.
129
+ const catalogDirectory = process.env.CATALOG_DIR || path.join(projectDirectory, '.eventcatalog-core');
130
+
131
+ const unsub = await watch(projectDirectory, catalogDirectory);
132
+
133
+ process.on('exit', () => unsub());
69
134
  }
@@ -61,10 +61,12 @@ const baseSchema = z.object({
61
61
  // Used by eventcatalog
62
62
  versions: z.array(z.string()).optional(),
63
63
  latestVersion: z.string().optional(),
64
+ pathToFile: z.string().optional(),
64
65
  catalog: z
65
66
  .object({
66
67
  path: z.string(),
67
68
  filePath: z.string(),
69
+ astroContentFilePath: z.string(),
68
70
  publicPath: z.string(),
69
71
  type: z.string(),
70
72
  })
@@ -50,7 +50,7 @@ export async function getFilesForDiffInCollection(
50
50
  collection: CollectionEntry<CollectionTypes>
51
51
  ): Promise<Array<{ file: string; dir: string }>> {
52
52
  // @ts-ignore
53
- const pathToFolder = collection.catalog?.absoluteFilePath;
53
+ const pathToFolder = collection.data.pathToFile;
54
54
  if (!pathToFolder) return [];
55
55
 
56
56
  const dir = dirname(pathToFolder);