From 772de618cf64b4269aaefb0ef34efb7dbce7c537 Mon Sep 17 00:00:00 2001 From: carlosallexandre Date: Mon, 9 Sep 2024 20:16:40 -0300 Subject: [PATCH] test: add tests for custom watcher --- .../example-catalog/components/footer.astro | 7 + .../__tests__/example-catalog/pages/index.md | 28 + .../__tests__/example-catalog/public/logo.png | Bin 0 -> 54304 bytes .../__tests__/example-catalog/public/logo.svg | 16 + scripts/__tests__/watcher.spec.ts | 791 ++++++++++++++++++ scripts/watcher.js | 165 ++-- 6 files changed, 949 insertions(+), 58 deletions(-) create mode 100644 scripts/__tests__/example-catalog/components/footer.astro create mode 100644 scripts/__tests__/example-catalog/pages/index.md create mode 100644 scripts/__tests__/example-catalog/public/logo.png create mode 100644 scripts/__tests__/example-catalog/public/logo.svg create mode 100644 scripts/__tests__/watcher.spec.ts diff --git a/scripts/__tests__/example-catalog/components/footer.astro b/scripts/__tests__/example-catalog/components/footer.astro new file mode 100644 index 00000000..e0299163 --- /dev/null +++ b/scripts/__tests__/example-catalog/components/footer.astro @@ -0,0 +1,7 @@ +--- +import config from '@config'; +--- + +
+ Event-driven architecture documentation: {config.organizationName} +
diff --git a/scripts/__tests__/example-catalog/pages/index.md b/scripts/__tests__/example-catalog/pages/index.md new file mode 100644 index 00000000..298fefc6 --- /dev/null +++ b/scripts/__tests__/example-catalog/pages/index.md @@ -0,0 +1,28 @@ +--- +id: index +--- + +# **EventCatalog** + +Welcome to [EventCatalog](https://www.eventcatalog.dev/). + +This open-source project is designed to help you and your teams bring discoverability and clarity to your event-driven architectures (EDA). + +To get started you can read the following guides: + +* [Getting started with EventCatalog](https://eventcatalog.dev/docs/development/getting-started/introduction) +* [Creating domains](https://eventcatalog.dev/docs/development/guides/domains/adding-domains) +* [Creating services](https://eventcatalog.dev/docs/development/guides/services/adding-services) +* [Creating commands](https://eventcatalog.dev/docs/development/guides/messages/commands/introduction) +* [Creating events](https://eventcatalog.dev/docs/development/guides/messages/events/introduction) +* [Assigning owners to resources](https://eventcatalog.dev/docs/owners) +* [Using components in your pages (Schemas, OpenAPI, etc)](https://eventcatalog.dev/docs/development/components/using-components) +* [Deploying and hosting your EventCatalog](https://eventcatalog.dev/docs/development/deployment) + +### **Join the community** + +Got questions about EventCatalog? Feature requests or need support? [Join our community on Discord.](https://discord.gg/3rjaZMmrAm) + +### **Enterprise support** + +Using EventCatalog and needs enterprise support? Work with us, find out what we offer on our [enterprise page](https://eventcatalog.dev/enterprise). diff --git a/scripts/__tests__/example-catalog/public/logo.png b/scripts/__tests__/example-catalog/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9df4b958f86ae134163674c0edb1778efb5c015c GIT binary patch literal 54304 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 literal 0 HcmV?d00001 diff --git a/scripts/__tests__/example-catalog/public/logo.svg b/scripts/__tests__/example-catalog/public/logo.svg new file mode 100644 index 00000000..e7c2c840 --- /dev/null +++ b/scripts/__tests__/example-catalog/public/logo.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/scripts/__tests__/watcher.spec.ts b/scripts/__tests__/watcher.spec.ts new file mode 100644 index 00000000..8ce88132 --- /dev/null +++ b/scripts/__tests__/watcher.spec.ts @@ -0,0 +1,791 @@ +import { describe, beforeAll, afterAll, it, expect, test } from 'vitest'; +import path from 'node:path'; +import fs from 'node:fs/promises'; +import { randomUUID } from 'node:crypto'; +import { watch } from '../watcher'; + +type Events = Array<{ path: string; type: 'create' | 'update' | 'delete' }>; + +const PROJECT_DIR = path.join(__dirname, 'tmp-watcher', randomUUID()); +const EC_CORE_DIR = path.join(PROJECT_DIR, '.eventcatalog-core'); + +describe('Watcher', () => { + let watcherSubscription: () => Promise; + + let callbacks: Array<{ resolve: (val: Events) => void; reject: (reason?: any) => void }> = []; + + const waitWatcher = () => { + return new Promise((resolve, reject) => { + callbacks.push({ resolve, reject }); + }); + }; + + const callbackFn = (err: Error | null, events: Events) => { + setImmediate(() => { + for (let { resolve, reject } of callbacks) { + if (err) reject(err); + else resolve(events); + } + + callbacks = []; + }); + }; + + beforeAll(async () => { + await prepareProjectDir(PROJECT_DIR, EC_CORE_DIR); + watcherSubscription = await watch(PROJECT_DIR, EC_CORE_DIR, callbackFn); + }); + + afterAll(async () => { + await watcherSubscription?.(); + await fs.rm(PROJECT_DIR, { recursive: true }); + }); + + describe('Commands', () => { + describe('/commands directory', () => { + // TODO: handle create event in watcher + test.skip('when a command is created, it adds it to the correct location in astro', async () => { + const filePath = path.join('commands/FakeCommand/index.md'); + + fs.writeFile(path.join(PROJECT_DIR, filePath), 'FAKE COMMAND TESTING'); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf8') + ).resolves.toEqual('FAKE COMMAND TESTING'); + }); + + test('when a command is updated, it updates the corresponding command in astro', async () => { + const filePath = path.join('commands/AddInventory/index.md'); + const fileProjectDir = path.join(PROJECT_DIR, filePath); + + fs.appendFile(fileProjectDir, 'UPDATE TEST'); + await waitWatcher(); + + const contentProjectDir = await fs.readFile(fileProjectDir, 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).resolves.toEqual(contentProjectDir); + }); + + // TODO: Verify what happens if /domains is deleted. + test('when a command is deleted, it deletes the corresponding command from astro', async () => { + const filePath = path.join('commands/AddInventory/index.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE_TEST'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).rejects.toThrow(/ENOENT: no such file or directory/); + }); + + test.skip('when a versioned command is created, it adds it to the correct location in astro', async () => { + const filePath = path.join('commands/FakeCommand/versioned/0.0.1/index.md'); + + fs.writeFile(path.join(PROJECT_DIR, filePath), 'FAKE COMMAND TESTING'); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf8') + ).resolves.toEqual('FAKE COMMAND TESTING'); + }); + + test('when a versioned command is updated, it updates the corresponding command in astro', async () => { + const filePath = path.join('commands/AddInventory/versioned/0.0.1/index.md'); + const fileProjectDir = path.join(PROJECT_DIR, filePath); + + fs.appendFile(fileProjectDir, 'UPDATE TEST'); + await waitWatcher(); + + const contentProjectDir = await fs.readFile(fileProjectDir, 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).resolves.toEqual(contentProjectDir); + }); + + test('when a versioned command is deleted, it deletes the corresponding command from astro', async () => { + const filePath = path.join('commands/AddInventory/versioned/0.0.1/index.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE_TEST'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).rejects.toThrow(/ENOENT: no such file or directory/); + }); + }); + }); + + describe('Domains', () => { + describe('/domains directory', () => { + // TODO: handle create event in watcher + test.skip('when a domain is created, it adds to the correct location in astro', async () => { + const filePath = path.join('domains/FakeDomain/index.md'); + + fs.writeFile(path.join(PROJECT_DIR, filePath), 'FAKE DOMAIN TESTING'); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf8') + ).resolves.toEqual('FAKE DOMAIN TESTING'); + }); + + test('when a domain is updated, it updates the corresponding domain in astro', async () => { + const filePath = path.join('domains/Payment/index.md'); + const fileProjectDir = path.join(PROJECT_DIR, filePath); + + fs.appendFile(fileProjectDir, 'UPDATE TEST'); + await waitWatcher(); + + const contentProjectDir = await fs.readFile(fileProjectDir, 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).resolves.toEqual(contentProjectDir); + }); + + // TODO: Verify what happens if /domains is deleted. + test('when a domain is deleted, it deletes the corresponding domain from astro', async () => { + const filePath = path.join('domains/Payment/index.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE_TEST'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).rejects.toThrow(/ENOENT: no such file or directory/); + }); + + test.skip('when a versioned domain is created, it adds to the correct location in astro', async () => { + const filePath = path.join('domains/FakeDomain/versioned/0.0.1/index.md'); + + fs.writeFile(path.join(PROJECT_DIR, filePath), 'FAKE DOMAIN TESTING'); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf8') + ).resolves.toEqual('FAKE DOMAIN TESTING'); + }); + + test('when a versioned domain is updated, it updates the corresponding domain in astro', async () => { + const filePath = path.join('domains/Payment/versioned/0.0.1/index.md'); + const fileProjectDir = path.join(PROJECT_DIR, filePath); + + fs.appendFile(fileProjectDir, 'UPDATE TEST'); + await waitWatcher(); + + const contentProjectDir = await fs.readFile(fileProjectDir, 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).resolves.toEqual(contentProjectDir); + }); + + test('when a versioned domain is deleted, it deletes the corresponding domain in astro', async () => { + const filePath = path.join('domains/Payment/versioned/0.0.1/index.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE_TEST'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).rejects.toThrow(/ENOENT: no such file or directory/); + }); + }); + }); + + describe('Events', () => { + describe('/events directory', () => { + // TODO: handle create event in watcher + test.skip('when an event is created, it adds it to the correct location in astro', async () => { + const filePath = path.join('events/FakeEvent/index.md'); + + fs.writeFile(path.join(PROJECT_DIR, filePath), 'FAKE EVENT TESTING'); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf8') + ).resolves.toEqual('FAKE EVENT TESTING'); + }); + + test('when an event is updated, it updates the corresponding event in astro', async () => { + const filePath = path.join('events/Order/OrderCancelled/index.md'); + const fileProjectDir = path.join(PROJECT_DIR, filePath); + + fs.appendFile(fileProjectDir, 'UPDATE TEST'); + await waitWatcher(); + + const contentProjectDir = await fs.readFile(fileProjectDir, 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).resolves.toEqual(contentProjectDir); + }); + + // TODO: Verify what happens if /events is deleted. + test('when an event is deleted, it deletes the corresponding event from astro', async () => { + const filePath = path.join('events/Order/OrderCancelled/index.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE_TEST'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).rejects.toThrow(/ENOENT: no such file or directory/); + }); + + // TODO: handle create event in watcher + test.skip('when a versioned event is created, it adds it to the correct location in astro', async () => { + const filePath = path.join('events/FakeEvent/versioned/0.0.1/index.md'); + + fs.writeFile(path.join(PROJECT_DIR, filePath), 'FAKE EVENT TESTING'); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf8') + ).resolves.toEqual('FAKE EVENT TESTING'); + }); + + test('when a versioned event is updated, it updates the corresponding event in astro', async () => { + const filePath = path.join('events/Inventory/InventoryAdjusted/versioned/0.0.1/index.md'); + const fileProjectDir = path.join(PROJECT_DIR, filePath); + + fs.appendFile(fileProjectDir, 'UPDATE TEST'); + await waitWatcher(); + + const contentProjectDir = await fs.readFile(fileProjectDir, 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).resolves.toEqual(contentProjectDir); + }); + + test('when a versioned event is deleted, it deletes the corresponding event from astro', async () => { + const filePath = path.join('events/Inventory/InventoryAdjusted/versioned/0.0.1/index.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE_TEST'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).rejects.toThrow(/ENOENT: no such file or directory/); + }); + }); + }); + + describe('Services', () => { + describe('/services directory', () => { + // TODO: handle create event in watcher + test.skip('when a service is created, it adds it to the correct location in astro', async () => { + const filePath = path.join('services/FakeService/index.md'); + + fs.writeFile(path.join(PROJECT_DIR), 'FAKE SERVICE TESTING'); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf8') + ).resolves.toEqual('FAKE SERVICE TESTING'); + }); + + test('when a service is updated, it updates the corresponding service in astro', async () => { + const filePath = path.join('services/PaymentService/index.md'); + const fileProjectDir = path.join(PROJECT_DIR, filePath); + + fs.appendFile(fileProjectDir, 'UPDATE TEST'); + await waitWatcher(); + + const contentProjectDir = await fs.readFile(fileProjectDir, 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).resolves.toEqual(contentProjectDir); + }); + + // TODO: Verify what happens if /services is deleted. + test('when a service is deleted, it deletes the corresponding service from astro', async () => { + const filePath = path.join('services/PaymentService/index.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE_TEST'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).rejects.toThrow(/ENOENT: no such file or directory/); + }); + + test.skip('when a versioned service is created, it adds to the correct location in astro', async () => { + const filePath = path.join('services/FakeService/versioned/0.0.1/index.md'); + + fs.writeFile(path.join(PROJECT_DIR), 'FAKE SERVICE TESTING'); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf8') + ).resolves.toEqual('FAKE SERVICE TESTING'); + }); + + test('when a versioned service is updated, it updates the corresponding service in astro', async () => { + const filePath = path.join('services/PaymentService/versioned/0.0.1/index.md'); + const fileProjectDir = path.join(PROJECT_DIR, filePath); + + fs.appendFile(fileProjectDir, 'UPDATE TEST'); + await waitWatcher(); + + const contentProjectDir = await fs.readFile(fileProjectDir, 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).resolves.toEqual(contentProjectDir); + }); + + test('when a versioned service is deleted, it deletes the corresponding service from astro', async () => { + const filePath = path.join('services/PaymentService/versioned/0.0.1/index.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE_TEST'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).rejects.toThrow(/ENOENT: no such file or directory/); + }); + }); + }); + + describe('Teams', () => { + // TODO: handle create event in watcher + test.skip('when a team is created, it adds it to the correct location in astro', async () => { + const filePath = path.join('teams/ec.md'); + + fs.writeFile(path.join(PROJECT_DIR, filePath), 'testing'); + await waitWatcher(); + + await expect(fs.readFile(path.join(EC_CORE_DIR, 'src/content/', filePath), 'utf8')).resolves.toEqual('testing'); + }); + + test('when a team is updated, it updates the corresponding team in astro', async () => { + const filePath = path.join('teams/full-stack.md'); + const teamProjectDir = path.join(PROJECT_DIR, filePath); + + fs.appendFile(teamProjectDir, 'test'); + await waitWatcher(); + + const teamContentProjectDir = await fs.readFile(teamProjectDir, 'utf-8'); + await expect(fs.readFile(path.join(EC_CORE_DIR, 'src/content/', filePath), 'utf-8')).resolves.toEqual( + teamContentProjectDir + ); + }); + + // TODO: Verify what happens if /teams is deleted. + test('when a team is deleted, it deletes the corresponding team from astro', async () => { + const filePath = path.join('teams/full-stack.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE_TEST'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect(fs.readFile(path.join(EC_CORE_DIR, 'src/content/', filePath), 'utf-8')).rejects.toThrow( + /ENOENT: no such file or directory/ + ); + }); + }); + + describe('Users', () => { + // TODO: handle create event in watcher + test.skip('when an user is created, it adds it to the correct location in astro', async () => { + const filePath = path.join('users/test.md'); + + fs.writeFile(path.join(PROJECT_DIR, filePath), 'testing'); + await waitWatcher(); + + await expect(fs.readFile(path.join(EC_CORE_DIR, 'src/content/', filePath), 'utf8')).resolves.toEqual('testing'); + }); + + test('when an user is updated, it updates the corresponding user in astro', async () => { + const filePath = path.join('users/dboyne.md'); + const userFileInProjectDir = path.join(PROJECT_DIR, filePath); + + fs.appendFile(userFileInProjectDir, 'test'); + await waitWatcher(); + + const userFileContentInProjectDir = await fs.readFile(userFileInProjectDir, { encoding: 'utf-8' }); + await expect(fs.readFile(path.join(EC_CORE_DIR, 'src/content/', filePath), 'utf-8')).resolves.toEqual( + userFileContentInProjectDir + ); + }); + + // TODO: Verify what happens if /users is deleted. + test('when an user is deleted, it deletes the corresponding user from astro', async () => { + const filePath = path.join('users/dboyne.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE TEST'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect(fs.readFile(path.join(EC_CORE_DIR, 'src/content/', filePath))).rejects.toThrow( + /ENOENT: no such file or directory/ + ); + }); + }); + + describe('Changelogs', () => { + test.todo('when a changelog is created, it adds it to the correct location in astro'); + + test('when a changelog is updated, it updates the corresponding changelog in astro', async () => { + const filePath = path.join('events/Inventory/InventoryAdjusted/changelog.md'); + + fs.appendFile(path.join(PROJECT_DIR, filePath), 'UPDATE TEST'); + await waitWatcher(); + + const changelogContent = await fs.readFile(path.join(PROJECT_DIR, filePath), 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/changelogs', path.dirname(filePath), 'changelog.mdx'), 'utf-8') + ).resolves.toEqual(changelogContent); + }); + + test('when a changelog is deleted, it deletes the corresponding changelog from astro', async () => { + const filePath = path.join('events/Inventory/InventoryAdjusted/changelog.md'); + + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE_TEST'); + await waitWatcher(); + + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/changelogs', path.dirname(filePath), 'changelog.mdx'), 'utf-8') + ).rejects.toThrowError(/ENOENT: no such file or directory/); + }); + + test.todo('when a versioned changelog is created, it adds to the correct location in astro'); + + test('when a versiond changelog is updated, it updates the corresponding changelog in astro', async () => { + const filePath = path.join('events/Inventory/InventoryAdjusted/versioned/0.0.1/changelog.md'); + + // Act + fs.appendFile(path.join(PROJECT_DIR, filePath), 'UPDATE TEST'); + await waitWatcher(); + + // Assert + const changelogContent = await fs.readFile(path.join(PROJECT_DIR, filePath), 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/changelogs', path.dirname(filePath), 'changelog.mdx'), 'utf-8') + ).resolves.toEqual(changelogContent); + }); + + test('when a versiond changelog is deleted, it deletes the corresponding changelog from astro', async () => { + const filePath = path.join('events/Inventory/InventoryAdjusted/versioned/0.0.1/changelog.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'DELETE_TEST'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content/changelogs', path.dirname(filePath), 'changelog.mdx'), 'utf-8') + ).rejects.toThrowError(/ENOENT: no such file or directory/); + }); + }); + + describe('Flows', () => { + // TODO: handle the create event in the watcher + test.todo('when a flow is created, it adds it to the correct location in astro'); + + test('when a flow is updated, it updates the corresponding flow in astro', async () => { + const filePath = path.join('flows/Payment/PaymentProcessed/index.md'); + + fs.appendFile(path.join(PROJECT_DIR, filePath), 'update_test'); + await waitWatcher(); + + const fileContent = await fs.readFile(path.join(PROJECT_DIR, filePath), 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).resolves.toEqual(fileContent); + }); + + test('when a flow is deleted, it deletes the corresponding flow from astro', async () => { + const filePath = path.join('flows/Payment/PaymentProcessed/index.md'); + + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'delete_test'); + await waitWatcher(); + + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).rejects.toThrowError(/ENOENT: no such file or directory/); + }); + + test.todo('when a versioned flow is created, it adds it to the correct location in astro'); + + test('when a versioned flow is updated, it updates the corresponding flow in astro', async () => { + const filePath = path.join('flows/Payment/PaymentProcessed/versioned/0.0.1/index.md'); + + // Act + fs.appendFile(path.join(PROJECT_DIR, filePath), 'update_test'); + await waitWatcher(); + + // Assert + const fileContent = await fs.readFile(path.join(PROJECT_DIR, filePath), 'utf-8'); + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).resolves.toEqual(fileContent); + }); + + test('when a versioned flow is deleted, it deletes the corresponding flow from astro', async () => { + const filePath = path.join('flows/Payment/PaymentProcessed/versioned/0.0.1/index.md'); + + // Arrange + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'delete_test'); + await waitWatcher(); + + // Act + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + // Assert + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).rejects.toThrowError(/ENOENT: no such file or directory/); + }); + }); + + describe('Pages', () => { + // TODO: handle create events in the watcher + test.todo('when a page is created, it adds it to the correct location in astro'); + + test('when a page is udpated, it updates the corresponding page in astro', async () => { + const filePath = path.join('pages/index.md'); + + fs.appendFile(path.join(PROJECT_DIR, filePath), 'update_test'); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).resolves.toMatch(/update_test$/); + }); + + test('when a page is deleted, it deletes the corresponding page from astro', async () => { + const filePath = path.join('pages/index.md'); + + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), 'delete_test'); + await waitWatcher(); + + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/content', path.dirname(filePath), 'index.mdx'), 'utf-8') + ).rejects.toThrowError(/ENOENT: no such file or directory/); + }); + }); + + describe('Custom components', () => { + // TODO: handle create events in the watcher + test.todo('when a custom component is created, it adds it to the correct location in astro'); + + test('when a custom component is updated, it updates the corresponding custom component in astro', async () => { + const filePath = path.join('components/footer.astro'); + + fs.appendFile(path.join(PROJECT_DIR, filePath), '

Hello from EC Testing

'); + await waitWatcher(); + + await expect(fs.readFile(path.join(EC_CORE_DIR, 'src/custom-defined-components/footer.astro'), 'utf-8')).resolves.toMatch( + /

Hello from EC Testing<\/p>$/ + ); + }); + + test('when a custom component is deleted, it deletes the corresponding custom component from astro', async () => { + const filePath = path.join('components/footer.astro'); + + // The Watcher needs the file to be in the astro directory to not throws an error. + // Could the Watcher warn instead of throwing an error? + fs.appendFile(path.join(PROJECT_DIR, filePath), '

Hello from EC Testing

'); + await waitWatcher(); + + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + await expect( + fs.readFile(path.join(EC_CORE_DIR, 'src/custom-defined-components/footer.astro'), 'utf-8') + ).rejects.toThrowError(/ENOENT: no such file or directory/); + }); + }); + + // TODO: handle public assets in the Watcher + describe.skip('Public assets', () => { + it('should reflect the creation of public assets to astroDir', async () => { + const logoSvg = ` + + + + + + + + `; + + fs.writeFile(path.join(PROJECT_DIR, 'public/logo.svg'), logoSvg); + await waitWatcher(); + + await expect(fs.readFile(path.join(EC_CORE_DIR, 'public/logo.svg'), 'utf-8')).resolves.toEqual(logoSvg); + }); + + it('should reflect the update of public assets to astroDir', async () => { + const filePath = path.join('public/logo.svg'); + + const content = await fs.readFile(path.join(PROJECT_DIR, filePath), 'utf-8'); + + fs.writeFile(path.join(PROJECT_DIR, filePath), content); + await waitWatcher(); + + await expect(fs.readFile(path.join(EC_CORE_DIR, filePath), 'utf-8')).resolves.toEqual(content); + }); + + it('should reflect the deletion of public assets to astroDir', async () => { + const filePath = path.join('public/logo.svg'); + + fs.rm(path.join(PROJECT_DIR, filePath)); + await waitWatcher(); + + await expect(fs.readFile(path.join(EC_CORE_DIR, filePath), 'utf-8')).rejects.toThrowError( + /ENOENT: no such file or directory/ + ); + }); + }); + + // TODO: handle config files in the Watcher. Now, Watcher only observes files + // in one of the following directories: 'domains', 'commands', 'events', + // 'services', 'teams', 'users', 'pages', 'components' and 'flows' + describe.skip('Config file', () => { + it('should reflect the update of the config file to astroDir', async () => { + const filePath = path.join('eventcatalog.config.js'); + + fs.writeFile(path.join(PROJECT_DIR, filePath), 'export default {}'); + await waitWatcher(); + + await expect(fs.readFile(path.join(EC_CORE_DIR, filePath), 'utf-8')).resolves.toBe('export default {}'); + }); + + // Could the config file be deleted? + it.todo('should reflect the deletion of the config file to astroDir'); + }); + + // TODO: handle config files in the Watcher. Now, Watcher only observes files + // in one of the following directories: 'domains', 'commands', 'events', + // 'services', 'teams', 'users', 'pages', 'components' and 'flows' + describe.todo('Custom css'); +}); + +async function prepareProjectDir(projectDir: string, ecCoreDir: string) { + await fs.mkdir(projectDir, { recursive: true }); + + const sampleCatalog = path.join(__dirname, 'example-catalog'); + await fs.cp(sampleCatalog, projectDir, { recursive: true }); + + await fs.mkdir(ecCoreDir, { recursive: true }); +} diff --git a/scripts/watcher.js b/scripts/watcher.js index 2cbf8dd6..03014181 100644 --- a/scripts/watcher.js +++ b/scripts/watcher.js @@ -1,69 +1,118 @@ -#!/usr/bin/env node import watcher from '@parcel/watcher'; import path from 'path'; import fs from 'fs'; import os from 'os'; -// Where the users project is located -const projectDirectory = process.env.PROJECT_DIR || process.cwd(); -// Where the catalog code is located. -const catalogDirectory = process.env.CATALOG_DIR; - -const contentPath = path.join(catalogDirectory, 'src', 'content'); - -const watchList = ['domains', 'commands', 'events', 'services', 'teams', 'users', 'pages', 'components', 'flows']; -// const absoluteWatchList = watchList.map((item) => path.join(projectDirectory, item)); - -// confirm folders exist before watching them -const verifiedWatchList = watchList.filter((item) => fs.existsSync(path.join(projectDirectory, item))); - -const extensionReplacer = (collection, file) => { - if (collection === 'teams' || collection == 'users') return file; - return file.replace('.md', '.mdx'); -}; - -for (let item of [...verifiedWatchList]) { - // Listen to the users directory for any changes. - watcher.subscribe(path.join(projectDirectory, item), (err, events) => { - if (err) { - return; - } - for (let event of events) { - const { path: eventPath, type } = event; - const file = eventPath.split(item)[1]; - let newPath = path.join(contentPath, item, extensionReplacer(item, file)); - - // Check if changlogs, they need to go into their own content folder - if (file.includes('changelog.md')) { - newPath = newPath.replace('src/content', 'src/content/changelogs'); - if (os.platform() == 'win32') { - newPath = newPath.replace('src\\content', 'src\\content\\changelogs'); - } - } +/** + * @typedef {Object} Event + * @property {string} path + * @property {"create"|"update"|"delete"} type + */ - // Check if its a component, need to move to the correct location - if (newPath.includes('components')) { - newPath = newPath.replace('src/content/components', 'src/custom-defined-components'); - if (os.platform() == 'win32') { - newPath = newPath.replace('src\\content\\components', 'src\\custom-defined-components'); - } - } +/** + * + * @param {string} projectDirectory + * @param {string} catalogDirectory + * @param {(err: Error | null, events: Event[]) => void | undefined} callback + */ +export async function watch(projectDirectory, catalogDirectory, callback) { + const contentPath = path.join(catalogDirectory, 'src', 'content'); - // If config files have changes - if (eventPath.includes('eventcatalog.config.js') || eventPath.includes('eventcatalog.styles.css')) { - fs.cpSync(eventPath, path.join(catalogDirectory, file)); - return; - } + const watchList = ['domains', 'commands', 'events', 'services', 'teams', 'users', 'pages', 'components', 'flows']; + // const absoluteWatchList = watchList.map((item) => path.join(projectDirectory, item)); - // If markdown files or astro files copy file over to the required location - if ((eventPath.endsWith('.md') || eventPath.endsWith('.astro')) && type === 'update') { - fs.cpSync(eventPath, newPath); - } + // confirm folders exist before watching them + const verifiedWatchList = watchList.filter((item) => fs.existsSync(path.join(projectDirectory, item))); + + const extensionReplacer = (collection, file) => { + if (collection === 'teams' || collection == 'users') return file; + return file.replace('.md', '.mdx'); + }; + + const subscriptions = await Promise.all( + verifiedWatchList.map((item) => + watcher.subscribe( + path.join(projectDirectory, item), + compose((err, events) => { + if (err) { + return; + } + + for (let event of events) { + const { path: eventPath, type } = event; + const file = eventPath.split(item)[1]; + let newPath = path.join(contentPath, item, extensionReplacer(item, file)); + + // Check if changlogs, they need to go into their own content folder + if (file.includes('changelog.md')) { + newPath = newPath.replace('src/content', 'src/content/changelogs'); + if (os.platform() == 'win32') { + newPath = newPath.replace('src\\content', 'src\\content\\changelogs'); + } + } + + // Check if its a component, need to move to the correct location + if (newPath.includes('components')) { + newPath = newPath.replace('src/content/components', 'src/custom-defined-components'); + if (os.platform() == 'win32') { + newPath = newPath.replace('src\\content\\components', 'src\\custom-defined-components'); + } + } - // IF directory remove it - if (type === 'delete') { - fs.rmSync(newPath); + // If config files have changes + if (eventPath.includes('eventcatalog.config.js') || eventPath.includes('eventcatalog.styles.css')) { + fs.cpSync(eventPath, path.join(catalogDirectory, file)); + return; + } + + // If markdown files or astro files copy file over to the required location + if ((eventPath.endsWith('.md') || eventPath.endsWith('.astro')) && type === 'update') { + fs.cpSync(eventPath, newPath); + } + + // IF directory remove it + if (type === 'delete') { + fs.rmSync(newPath); + } + } + }, callback) + ) + ) + ); + + return async () => { + await Promise.allSettled(subscriptions.map((sub) => sub.unsubscribe())); + }; +} + +/** + * + * @param {...Function} fns + * @returns {Function} + */ +function compose(...fns) { + return function (_err, events) { + let error = _err; + fns.filter(Boolean).forEach((fn) => { + try { + fn(error, events); + } catch (e) { + error = e; } - } - }); + }); + }; +} + +/** + * TODO: call `watch` from the dev command. + * Calling `watch` there will avoid these if statement. + * The same could be done to `catalog-to-astro-content-directory` + */ +if (process.env.NODE_ENV !== 'test') { + // Where the users project is located + const projectDirectory = process.env.PROJECT_DIR || process.cwd(); + // Where the catalog code is located. + const catalogDirectory = process.env.CATALOG_DIR; + + watch(projectDirectory, catalogDirectory); }