From d3c0a7cda56ab5beacf52d50256f529918ad5791 Mon Sep 17 00:00:00 2001 From: Clay Tercek <30105080+claytercek@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:05:42 -0400 Subject: [PATCH 1/4] use error classes for all throws in content --- .../__tests__/content-plugin-driver.test.js | 22 ++--- .../lib/__tests__/launchpad-content.test.js | 38 +++++---- packages/content/lib/content-plugin-driver.js | 7 +- packages/content/lib/launchpad-content.js | 20 ++--- .../lib/plugins/__tests__/md-to-html.test.js | 2 +- .../plugins/__tests__/sanity-to-html.test.js | 2 +- .../__tests__/sanity-to-markdown.test.js | 2 +- .../plugins/__tests__/sanity-to-plain.test.js | 6 +- .../sources/__tests__/airtable-source.test.js | 11 ++- .../__tests__/contentful-source.test.js | 9 ++- .../lib/sources/__tests__/json-source.test.js | 13 ++- .../sources/__tests__/sanity-source.test.js | 5 +- .../sources/__tests__/strapi-source.test.js | 9 ++- .../content/lib/sources/airtable-source.js | 27 +++---- .../content/lib/sources/contentful-source.js | 13 ++- packages/content/lib/sources/json-source.js | 11 ++- packages/content/lib/sources/sanity-source.js | 13 ++- packages/content/lib/sources/source-errors.js | 27 ------- packages/content/lib/sources/source.js | 54 ++++++++++++- packages/content/lib/sources/strapi-source.js | 11 ++- .../__tests__/content-transform-utils.test.js | 2 +- .../lib/utils/__tests__/data-store.test.js | 17 ++-- .../utils/__tests__/fetch-paginated.test.js | 12 +-- .../lib/utils/__tests__/file-utils.test.js | 4 +- .../__tests__/markdown-it-italic-bold.test.js | 80 +++++++++---------- .../lib/utils/__tests__/safe-ky.test.js | 6 +- .../lib/utils/content-transform-utils.js | 6 +- packages/content/lib/utils/data-store.js | 54 +++++++------ packages/content/lib/utils/fetch-paginated.js | 10 +-- packages/content/lib/utils/file-utils.js | 60 +++++++------- packages/content/lib/utils/safe-ky.js | 53 ++++++------ 31 files changed, 321 insertions(+), 285 deletions(-) delete mode 100644 packages/content/lib/sources/source-errors.js diff --git a/packages/content/lib/__tests__/content-plugin-driver.test.js b/packages/content/lib/__tests__/content-plugin-driver.test.js index 647432c5..f6ff5073 100644 --- a/packages/content/lib/__tests__/content-plugin-driver.test.js +++ b/packages/content/lib/__tests__/content-plugin-driver.test.js @@ -113,16 +113,13 @@ describe('ContentPluginDriver', () => { }) }); + const contentErr = new ContentError('Plugin setup failed', error); contentDriver.add(plugin); - await contentDriver.runHookSequential('onSetupError', new ContentError('Plugin setup failed', error)); + await contentDriver.runHookSequential('onSetupError', contentErr); expect(onSetupError).toHaveBeenCalledWith( - expect.any(Object), - expect.objectContaining({ - name: 'ContentError', - message: 'Plugin setup failed', - cause: error - }) + expect.anything(), + contentErr ); }); @@ -146,16 +143,13 @@ describe('ContentPluginDriver', () => { }) }); + const contentErr = new ContentError('Content fetch failed', error); contentDriver.add(plugin); - await contentDriver.runHookSequential('onContentFetchError', new ContentError('Content fetch failed', error)); + await contentDriver.runHookSequential('onContentFetchError', contentErr); expect(onContentFetchError).toHaveBeenCalledWith( - expect.any(Object), - expect.objectContaining({ - name: 'ContentError', - message: 'Content fetch failed', - cause: error - }) + expect.anything(), + expect.any(ContentError) ); }); }); diff --git a/packages/content/lib/__tests__/launchpad-content.test.js b/packages/content/lib/__tests__/launchpad-content.test.js index 77c04129..cfa9189c 100644 --- a/packages/content/lib/__tests__/launchpad-content.test.js +++ b/packages/content/lib/__tests__/launchpad-content.test.js @@ -22,25 +22,23 @@ describe('LaunchpadContent', () => { */ const createBasicConfig = (plugins = []) => { return { - content: { - downloadPath: '/downloads', - tempPath: '/temp', - backupPath: '/backups', - sources: [ - defineSource({ - id: 'test', - fetch: () => { - return ok([{ + downloadPath: '/downloads', + tempPath: '/temp', + backupPath: '/backups', + sources: [ + defineSource({ + id: 'test', + fetch: () => { + return ok([{ + id: 'doc1', + dataPromise: okAsync([{ id: 'doc1', - dataPromise: okAsync([{ - id: 'doc1', - data: 'doc1' - }]) - }]); - } - }) - ] - }, + data: 'doc1' + }]) + }]); + } + }) + ], plugins }; }; @@ -56,7 +54,7 @@ describe('LaunchpadContent', () => { const config = createBasicConfig(); const content = new LaunchpadContent(config, createMockLogger()); expect(content).toBeInstanceOf(LaunchpadContent); - expect(content._config).toEqual(resolveContentOptions(config.content)); + expect(content._config).toEqual(resolveContentOptions(config)); }); }); @@ -79,7 +77,7 @@ describe('LaunchpadContent', () => { vol.writeFileSync('/downloads/test/old.json', '{}'); const config = { - ...createBasicConfig().content, + ...createBasicConfig(), keep: ['.keep'] }; diff --git a/packages/content/lib/content-plugin-driver.js b/packages/content/lib/content-plugin-driver.js index 5dcaca11..b216ee5d 100644 --- a/packages/content/lib/content-plugin-driver.js +++ b/packages/content/lib/content-plugin-driver.js @@ -2,11 +2,10 @@ import { HookContextProvider } from '@bluecadet/launchpad-utils/lib/plugin-drive export class ContentError extends Error { /** - * @param {string} [message] - * @param {Error} [cause] + * @param {ConstructorParameters} args */ - constructor(message, cause) { - super(message, { cause }); + constructor(...args) { + super(...args); this.name = 'ContentError'; } } diff --git a/packages/content/lib/launchpad-content.js b/packages/content/lib/launchpad-content.js index ff4040ed..19b38656 100644 --- a/packages/content/lib/launchpad-content.js +++ b/packages/content/lib/launchpad-content.js @@ -93,7 +93,7 @@ export class LaunchpadContent { this._logger.error('Error in content fetch process:', e); this._logger.info('Restoring from backup...'); return this.restore(sources).andThen(() => { - return err(new ContentError('Failed to download content. Restored from backup.')); + return err(new ContentError('Failed to download content. Restored from backup.', { cause: e })); }); }) .andThen(() => this.clear(sources, { @@ -143,7 +143,7 @@ export class LaunchpadContent { } return ResultAsync.combine(tasks); })).andThen(() => { - /** @type {ResultAsync[]} */ + /** @type {ResultAsync[]} */ const tasks = []; if (removeIfEmpty) { if (temp) tasks.push(FileUtils.removeDirIfEmpty(this.getTempPath())); @@ -153,7 +153,7 @@ export class LaunchpadContent { return ResultAsync.combine(tasks); }) .map(() => undefined) // return void instead of void[] - .mapErr(error => new ContentError(`Failed to clear directories: ${error instanceof Error ? error.message : String(error)}`)); + .mapErr(error => new ContentError('Failed to clear directories', { cause: error })); } /** @@ -176,7 +176,7 @@ export class LaunchpadContent { return FileUtils.copy(downloadPath, backupPath); }); })) - .mapErr(e => new ContentError(`Failed to backup sources: ${e}`)) + .mapErr(e => new ContentError('Failed to backup sources', { cause: e })) .map(() => undefined); // return void instead of void[] } @@ -267,10 +267,10 @@ export class LaunchpadContent { ResultAsync.fromPromise( // wrap source in promise to ensure it's awaited Promise.resolve(source), - error => new ContentError(error instanceof Error ? error.message : String(error)) + error => new ContentError('Failed to build source', { cause: error }) ).andThen(awaited => { if ('value' in awaited || 'error' in awaited) { - return awaited.mapErr(e => new ContentError(e instanceof Error ? e.message : String(e))); + return awaited.mapErr(e => new ContentError('Failed to build source', { cause: e })); } return ok(awaited); }) @@ -296,7 +296,7 @@ export class LaunchpadContent { .asyncAndThen(calls => { return ResultAsync.combine(calls.map(call => call.dataPromise)); }) - .mapErr(e => new ContentError(`Failed to fetch source ${source.id}: ${e instanceof Error ? e.message : String(e)}`)) + .mapErr(e => new ContentError(`Failed to fetch source ${source.id}`, { cause: e })) .andThrough(fetchResults => { /** @type {Map} */ const map = new Map(); @@ -305,7 +305,7 @@ export class LaunchpadContent { map.set(result.id, result.data); } - return this._dataStore.createNamespaceFromMap(source.id, map).mapErr(e => new ContentError(`Unable to create namespace for source ${source.id}: ${e}`)); + return this._dataStore.createNamespaceFromMap(source.id, map).mapErr(e => new ContentError(`Unable to create namespace for source ${source.id}`, { cause: e })); }); })) .map(() => undefined); // return void instead of void[] @@ -328,7 +328,7 @@ export class LaunchpadContent { return FileUtils.saveJson(document.data, filePath); }) )) - .mapErr(e => new ContentError(`Failed to write data store to disk: ${e}`)) + .mapErr(e => new ContentError('Failed to write data store to disk', { cause: e })) .map(() => undefined); // return void instead of void[] } @@ -351,7 +351,7 @@ export class LaunchpadContent { return okAsync(undefined); }); - }).mapErr(e => new ContentError(e)); + }).mapErr(e => new ContentError(`Failed to clear directory: ${dirPath}`, { cause: e })); } /** diff --git a/packages/content/lib/plugins/__tests__/md-to-html.test.js b/packages/content/lib/plugins/__tests__/md-to-html.test.js index b7bb2746..2666a381 100644 --- a/packages/content/lib/plugins/__tests__/md-to-html.test.js +++ b/packages/content/lib/plugins/__tests__/md-to-html.test.js @@ -57,6 +57,6 @@ describe('mdToHtml plugin', () => { ctx.data.insert('test', 'doc1', { content: { foo: 'bar' } }); const plugin = mdToHtml({ path: '$.content' }); - expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Can\'t convert non-string content to html.'); + expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Error applying content transform'); }); }); diff --git a/packages/content/lib/plugins/__tests__/sanity-to-html.test.js b/packages/content/lib/plugins/__tests__/sanity-to-html.test.js index e3b4177e..4bb845ec 100644 --- a/packages/content/lib/plugins/__tests__/sanity-to-html.test.js +++ b/packages/content/lib/plugins/__tests__/sanity-to-html.test.js @@ -45,6 +45,6 @@ describe('sanityToHtml plugin', () => { ctx.data.insert('test', 'doc1', { content: 'not a block' }); const plugin = sanityToHtml({ path: '$.content' }); - expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Content is not a valid Sanity text block'); + expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Error applying content transform'); }); }); diff --git a/packages/content/lib/plugins/__tests__/sanity-to-markdown.test.js b/packages/content/lib/plugins/__tests__/sanity-to-markdown.test.js index 068e0d9b..de7c11d6 100644 --- a/packages/content/lib/plugins/__tests__/sanity-to-markdown.test.js +++ b/packages/content/lib/plugins/__tests__/sanity-to-markdown.test.js @@ -45,6 +45,6 @@ describe('sanityToMd plugin', () => { ctx.data.insert('test', 'doc1', { content: 'not a block' }); const plugin = sanityToMd({ path: '$.content' }); - expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Content is not a valid Sanity text block'); + expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Error applying content transform'); }); }); diff --git a/packages/content/lib/plugins/__tests__/sanity-to-plain.test.js b/packages/content/lib/plugins/__tests__/sanity-to-plain.test.js index f33a6c99..d70d77c3 100644 --- a/packages/content/lib/plugins/__tests__/sanity-to-plain.test.js +++ b/packages/content/lib/plugins/__tests__/sanity-to-plain.test.js @@ -49,7 +49,7 @@ describe('sanityToPlain plugin', () => { ctx.data.insert('test', 'doc1', { content: 'not a block' }); const plugin = sanityToPlain({ path: '$.content' }); - expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Content is not a valid Sanity text block'); + expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Error applying content transform'); }); it('should throw error for block without children', () => { @@ -60,7 +60,7 @@ describe('sanityToPlain plugin', () => { ctx.data.insert('test', 'doc1', { content: invalidBlock }); const plugin = sanityToPlain({ path: '$.content' }); - expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Content is not a valid Sanity text block'); + expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Error applying content transform'); }); it('should throw error for block with invalid children', () => { @@ -77,7 +77,7 @@ describe('sanityToPlain plugin', () => { ctx.data.insert('test', 'doc1', { content: invalidBlock }); const plugin = sanityToPlain({ path: '$.content' }); - expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Content is not a valid Sanity text block'); + expect(() => plugin.hooks.onContentFetchDone(ctx)).toThrow('Error applying content transform'); }); it('should concatenate multiple text spans', () => { diff --git a/packages/content/lib/sources/__tests__/airtable-source.test.js b/packages/content/lib/sources/__tests__/airtable-source.test.js index bcca8ebe..fc1648c6 100644 --- a/packages/content/lib/sources/__tests__/airtable-source.test.js +++ b/packages/content/lib/sources/__tests__/airtable-source.test.js @@ -4,6 +4,7 @@ import { http, HttpResponse } from 'msw'; import airtableSource from '../airtable-source.js'; import { createMockLogger } from '@bluecadet/launchpad-testing/test-utils.js'; import { DataStore } from '../../utils/data-store.js'; +import { SourceFetchError, SourceParseError } from '../source.js'; const server = setupServer(); @@ -206,7 +207,7 @@ describe('airtableSource', () => { const fetchPromises = result._unsafeUnwrap(); const data = await fetchPromises[0].dataPromise; expect(data).toBeErr(); - expect(data._unsafeUnwrapErr().type).toBe('fetch'); + expect(data._unsafeUnwrapErr()).toBeInstanceOf(SourceFetchError); expect(data._unsafeUnwrapErr().message).toContain('Failed to fetch data from Airtable'); }); @@ -242,8 +243,10 @@ describe('airtableSource', () => { const data = await fetchPromises[0].dataPromise; expect(data).toBeErr(); - expect(data._unsafeUnwrapErr().type).toBe('parse'); - expect(data._unsafeUnwrapErr().message).toContain('At least 2 columns required'); + expect(data._unsafeUnwrapErr()).toBeInstanceOf(SourceParseError); + expect(data._unsafeUnwrapErr().message).toContain('Error processing table invalid-table from Airtable'); + // @ts-expect-error cause is unknown + expect(data._unsafeUnwrapErr().cause.message).toContain('At least 2 columns required'); }); it('should handle unauthorized access', async () => { @@ -269,7 +272,7 @@ describe('airtableSource', () => { const fetchPromises = result._unsafeUnwrap(); const data = await fetchPromises[0].dataPromise; expect(data).toBeErr(); - expect(data._unsafeUnwrapErr().type).toBe('fetch'); + expect(data._unsafeUnwrapErr()).toBeInstanceOf(SourceFetchError); expect(data._unsafeUnwrapErr().message).toContain('Failed to fetch data from Airtable'); }); }); diff --git a/packages/content/lib/sources/__tests__/contentful-source.test.js b/packages/content/lib/sources/__tests__/contentful-source.test.js index 09efbf42..d3834a75 100644 --- a/packages/content/lib/sources/__tests__/contentful-source.test.js +++ b/packages/content/lib/sources/__tests__/contentful-source.test.js @@ -4,6 +4,7 @@ import { http, HttpResponse } from 'msw'; import contentfulSource from '../contentful-source.js'; import { createMockLogger } from '@bluecadet/launchpad-testing/test-utils.js'; import { DataStore } from '../../utils/data-store.js'; +import { SourceConfigError, SourceFetchError } from '../source.js'; const server = setupServer(); @@ -37,7 +38,7 @@ describe('contentfulSource', () => { }); expect(result).toBeErr(); - expect(result._unsafeUnwrapErr().type).toBe('config'); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SourceConfigError); expect(result._unsafeUnwrapErr().message).toContain('no deliveryToken is provided'); }); @@ -51,7 +52,7 @@ describe('contentfulSource', () => { }); expect(result).toBeErr(); - expect(result._unsafeUnwrapErr().type).toBe('config'); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SourceConfigError); expect(result._unsafeUnwrapErr().message).toContain('no previewToken is provided'); }); @@ -251,7 +252,7 @@ describe('contentfulSource', () => { const fetchPromises = result._unsafeUnwrap(); const data = await fetchPromises[0].dataPromise; expect(data).toBeErr(); - expect(data._unsafeUnwrapErr().type).toBe('fetch'); + expect(data._unsafeUnwrapErr()).toBeInstanceOf(SourceFetchError); expect(data._unsafeUnwrapErr().message).toContain('Error fetching page'); }); @@ -280,7 +281,7 @@ describe('contentfulSource', () => { const fetchPromises = result._unsafeUnwrap(); const data = await fetchPromises[0].dataPromise; expect(data).toBeErr(); - expect(data._unsafeUnwrapErr().type).toBe('fetch'); + expect(data._unsafeUnwrapErr()).toBeInstanceOf(SourceFetchError); expect(data._unsafeUnwrapErr().message).toContain('Invalid content type'); }); diff --git a/packages/content/lib/sources/__tests__/json-source.test.js b/packages/content/lib/sources/__tests__/json-source.test.js index 74a20c09..09437430 100644 --- a/packages/content/lib/sources/__tests__/json-source.test.js +++ b/packages/content/lib/sources/__tests__/json-source.test.js @@ -4,6 +4,7 @@ import { http, HttpResponse } from 'msw'; import jsonSource from '../json-source.js'; import { createMockLogger } from '@bluecadet/launchpad-testing/test-utils.js'; import { DataStore } from '../../utils/data-store.js'; +import { SourceFetchError, SourceParseError } from '../source.js'; const server = setupServer(); @@ -89,7 +90,7 @@ describe('jsonSource', () => { const data = await fetchPromises[0].dataPromise; expect(data).toBeErr(); - expect(data._unsafeUnwrapErr().type).toMatch('fetch'); + expect(data._unsafeUnwrapErr()).toBeInstanceOf(SourceFetchError); }); it('should handle parse errors', async () => { @@ -119,7 +120,7 @@ describe('jsonSource', () => { const data = await fetchPromises[0].dataPromise; expect(data).toBeErr(); - expect(data._unsafeUnwrapErr().type).toMatch('parse'); + expect(data._unsafeUnwrapErr()).toBeInstanceOf(SourceParseError); }); it('should respect the maxTimeout option', async () => { @@ -151,7 +152,11 @@ describe('jsonSource', () => { const data = await fetchPromises[0].dataPromise; expect(data).toBeErr(); - expect(data._unsafeUnwrapErr().type).toMatch('fetch'); - expect(data._unsafeUnwrapErr().message).toContain('Request timed out'); + expect(data._unsafeUnwrapErr()).toBeInstanceOf(SourceFetchError); + expect(data._unsafeUnwrapErr().message).toContain('Could not fetch json from https://api.example.com/slow'); + // @ts-expect-error cause is unknown + expect(data._unsafeUnwrapErr().cause.message).toContain('Error during request'); + // @ts-expect-error cause is unknown + expect(data._unsafeUnwrapErr().cause.cause.message).toContain('Request timed out'); }); }); diff --git a/packages/content/lib/sources/__tests__/sanity-source.test.js b/packages/content/lib/sources/__tests__/sanity-source.test.js index f6bbf0c6..964f3ece 100644 --- a/packages/content/lib/sources/__tests__/sanity-source.test.js +++ b/packages/content/lib/sources/__tests__/sanity-source.test.js @@ -4,6 +4,7 @@ import { http, HttpResponse } from 'msw'; import sanitySource from '../sanity-source.js'; import { createMockLogger } from '@bluecadet/launchpad-testing/test-utils.js'; import { DataStore } from '../../utils/data-store.js'; +import { SourceConfigError, SourceFetchError } from '../source.js'; const server = setupServer(); @@ -35,7 +36,7 @@ describe('sanitySource', () => { }); expect(result).toBeErr(); - expect(result._unsafeUnwrapErr().type).toBe('config'); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SourceConfigError); expect(result._unsafeUnwrapErr().message).toContain('Missing projectId and/or apiToken'); }); @@ -203,7 +204,7 @@ describe('sanitySource', () => { const fetchPromises = result._unsafeUnwrap(); const data = await fetchPromises[0].dataPromise; expect(data).toBeErr(); - expect(data._unsafeUnwrapErr().type).toBe('fetch'); + expect(data._unsafeUnwrapErr()).toBeInstanceOf(SourceFetchError); expect(data._unsafeUnwrapErr().message).toContain('Could not fetch page'); }); diff --git a/packages/content/lib/sources/__tests__/strapi-source.test.js b/packages/content/lib/sources/__tests__/strapi-source.test.js index 98df91b4..cab20e59 100644 --- a/packages/content/lib/sources/__tests__/strapi-source.test.js +++ b/packages/content/lib/sources/__tests__/strapi-source.test.js @@ -4,6 +4,7 @@ import { http, HttpResponse } from 'msw'; import strapiSource from '../strapi-source.js'; import { createMockLogger } from '@bluecadet/launchpad-testing/test-utils.js'; import { DataStore } from '../../utils/data-store.js'; +import { SourceConfigError, SourceFetchError } from '../source.js'; const server = setupServer(); @@ -39,7 +40,7 @@ describe('strapiSource', () => { }); expect(result).toBeErr(); - expect(result._unsafeUnwrapErr().type).toBe('config'); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SourceConfigError); expect(result._unsafeUnwrapErr().message).toContain('Unsupported strapi version'); }); @@ -314,8 +315,8 @@ describe('strapiSource', () => { expect(source).toBeErr(); const sourceValue = source._unsafeUnwrapErr(); - expect(sourceValue.type).toBe('fetch'); - expect(sourceValue.message).toContain('401'); + expect(sourceValue).toBeInstanceOf(SourceFetchError); + expect(sourceValue.message).toContain('Could not complete request to get JWT for test@example.com'); }); it('should handle API errors', async () => { @@ -347,7 +348,7 @@ describe('strapiSource', () => { const data = await fetchPromises[0].dataPromise; expect(data).toBeErr(); - expect(data._unsafeUnwrapErr().type).toBe('fetch'); + expect(data._unsafeUnwrapErr()).toBeInstanceOf(SourceFetchError); expect(data._unsafeUnwrapErr().message).toContain('Could not fetch page'); }); }); diff --git a/packages/content/lib/sources/airtable-source.js b/packages/content/lib/sources/airtable-source.js index 26bfff6e..99b71592 100644 --- a/packages/content/lib/sources/airtable-source.js +++ b/packages/content/lib/sources/airtable-source.js @@ -1,6 +1,5 @@ import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow'; -import { defineSource } from './source.js'; -import { configError, fetchError, parseError } from './source-errors.js'; +import { defineSource, SourceConfigError, SourceFetchError, SourceMissingDependencyError, SourceParseError } from './source.js'; /** * @typedef AirtableOptions @@ -34,7 +33,7 @@ const AIRTABLE_OPTION_DEFAULTS = { * @param {import("airtable").Base} base * @param {string} tableId * @param {string} [defaultView] - * @returns {ResultAsync[], import('./source-errors.js').SourceError>} + * @returns {ResultAsync[], SourceFetchError>} */ function fetchData(base, tableId, defaultView) { return ResultAsync.fromPromise( @@ -70,7 +69,7 @@ function fetchData(base, tableId, defaultView) { } ); }), - (error) => fetchError(`Failed to fetch data from Airtable: ${error instanceof Error ? error.message : error}`) + (error) => new SourceFetchError('Failed to fetch data from Airtable', { cause: error }) ); } @@ -91,7 +90,7 @@ function isBoolStr(value) { /** * @param {import("airtable").Record[]} tableData * @param {boolean} isKeyValueTable - * @returns {Result} + * @returns {Result} */ function processTableToSimplified(tableData, isKeyValueTable) { if (isKeyValueTable) { @@ -104,7 +103,7 @@ function processTableToSimplified(tableData, isKeyValueTable) { const fields = row._rawJson.fields; if (Object.keys(fields).length < 2) { - return err(parseError('At least 2 columns required to map table to a key-value pair')); + return err(new SourceParseError('At least 2 columns required to map table to a key-value pair')); } const regex = /(.*)\[([0-9]*)\]$/g; @@ -148,14 +147,14 @@ export default function airtableSource(options) { }; if (!assembledOptions.apiKey) { - return errAsync(configError('apiKey is required')); + return errAsync(new SourceConfigError('apiKey is required')); } if (!assembledOptions.baseId) { - return errAsync(configError('baseId is required')); + return errAsync(new SourceConfigError('baseId is required')); } - return ResultAsync.fromPromise(import('airtable'), () => configError('Could not find module "airtable". Make sure you have installed it.')) + return ResultAsync.fromPromise(import('airtable'), () => new SourceMissingDependencyError('Could not find module "airtable". Make sure you have installed it.')) .map(({ default: Airtable }) => { Airtable.configure({ endpointUrl: assembledOptions.endpointUrl, @@ -173,7 +172,7 @@ export default function airtableSource(options) { * @param {string} tableId * @param {boolean} force * @param {import("@bluecadet/launchpad-utils").Logger} logger - * @returns {ResultAsync[], import('./source-errors.js').SourceError>} + * @returns {ResultAsync[], SourceFetchError>} */ function getDataCached(tableId, force = false, logger) { logger.debug(`Fetching ${tableId} from Airtable`); @@ -208,8 +207,8 @@ export default function airtableSource(options) { const simplifiedTable = processTableToSimplified(data, false); if (simplifiedTable.isErr()) { - ctx.logger.error(`Error processing ${tableId} from Airtable: ${simplifiedTable.error}`); - return err(simplifiedTable.error); + ctx.logger.error(`Error processing ${tableId} from Airtable`); + return err(new SourceParseError(`Error processing table ${tableId} from Airtable`, { cause: simplifiedTable.error })); } return ok([{ @@ -230,8 +229,8 @@ export default function airtableSource(options) { const simplifiedTable = processTableToSimplified(data, true); if (simplifiedTable.isErr()) { - ctx.logger.error(`Error processing ${tableId} from Airtable: ${simplifiedTable.error}`); - return err(simplifiedTable.error); + ctx.logger.error(`Error processing ${tableId} from Airtable`); + return err(new SourceParseError(`Error processing table ${tableId} from Airtable`, { cause: simplifiedTable.error })); } return ok([{ diff --git a/packages/content/lib/sources/contentful-source.js b/packages/content/lib/sources/contentful-source.js index b97268fc..401a2e62 100644 --- a/packages/content/lib/sources/contentful-source.js +++ b/packages/content/lib/sources/contentful-source.js @@ -1,6 +1,5 @@ import { err, errAsync, ok, ResultAsync } from 'neverthrow'; -import { defineSource } from './source.js'; -import { configError, fetchError } from './source-errors.js'; +import { defineSource, SourceConfigError, SourceFetchError, SourceMissingDependencyError } from './source.js'; import { fetchPaginated } from '../utils/fetch-paginated.js'; /** @@ -75,13 +74,13 @@ export default function contentfulSource(options) { if (assembled.usePreviewApi) { if (!assembled.previewToken) { - return errAsync(configError('usePreviewApi is set to true, but no previewToken is provided')); + return errAsync(new SourceConfigError('usePreviewApi is set to true, but no previewToken is provided')); } assembled.host = 'preview.contentful.com'; assembled.accessToken = assembled.previewToken; } else { if (!('deliveryToken' in assembled) || !assembled.deliveryToken) { - return errAsync(configError('usePreviewApi is set to false, but no deliveryToken is provided')); + return errAsync(new SourceConfigError('usePreviewApi is set to false, but no deliveryToken is provided')); } assembled.accessToken = assembled.deliveryToken; @@ -92,7 +91,7 @@ export default function contentfulSource(options) { } return ResultAsync - .fromPromise(import('contentful'), () => configError('Could not find module "contentful". Make sure you have installed it.')) + .fromPromise(import('contentful'), () => new SourceMissingDependencyError('Could not find module "contentful". Make sure you have installed it.')) .map(({ createClient }) => { const client = createClient(assembled); @@ -104,10 +103,10 @@ export default function contentfulSource(options) { /** @type {ReturnType[], assets: import('contentful').Asset[]}>>} */ const fetchResult = fetchPaginated({ fetchPageFn: (params) => { - return ResultAsync.fromPromise(client.getEntries({ ...assembled.searchParams, skip: params.offset, limit: params.limit }), (error) => fetchError(`Error fetching page: ${error instanceof Error ? error.message : error}`)) + return ResultAsync.fromPromise(client.getEntries({ ...assembled.searchParams, skip: params.offset, limit: params.limit }), (error) => new SourceFetchError(`Error fetching page: ${error instanceof Error ? error.message : error}`)) .andThen((rawPage) => { if (rawPage.errors) { - return err(fetchError(`Error fetching page: ${rawPage.errors.map(e => e.message).join(', ')}`)); + return err(new SourceFetchError(`Error fetching page: ${rawPage.errors.map(e => e.message).join(', ')}`)); } const page = rawPage.toPlainObject(); diff --git a/packages/content/lib/sources/json-source.js b/packages/content/lib/sources/json-source.js index ba58cdb4..67e2fafd 100644 --- a/packages/content/lib/sources/json-source.js +++ b/packages/content/lib/sources/json-source.js @@ -1,8 +1,7 @@ import chalk from 'chalk'; import { ok, okAsync } from 'neverthrow'; -import { defineSource } from './source.js'; -import { fetchError, parseError } from './source-errors.js'; -import { safeKy, SafeKyError } from '../utils/safe-ky.js'; +import { defineSource, SourceFetchError, SourceParseError } from './source.js'; +import { safeKy, SafeKyParseError } from '../utils/safe-ky.js'; /** * @typedef {object} JsonSourceOptions @@ -25,10 +24,10 @@ export default function jsonSource({ id, files, maxTimeout = 30_000 }) { id: key, dataPromise: safeKy(url, { timeout: maxTimeout }).json() .mapErr((e) => { - if (e instanceof SafeKyError.ParseError) { - return parseError(e.message); + if (e instanceof SafeKyParseError) { + return new SourceParseError(`Could not parse json from ${url}`, { cause: e }); } - return fetchError(e.message); + return new SourceFetchError(`Could not fetch json from ${url}`, { cause: e }); }) .map(data => { return [{ diff --git a/packages/content/lib/sources/sanity-source.js b/packages/content/lib/sources/sanity-source.js index 368b7b60..c4f78855 100644 --- a/packages/content/lib/sources/sanity-source.js +++ b/packages/content/lib/sources/sanity-source.js @@ -1,7 +1,6 @@ import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow'; -import { defineSource } from './source.js'; +import { defineSource, SourceConfigError, SourceFetchError, SourceMissingDependencyError } from './source.js'; import { fetchPaginated } from '../utils/fetch-paginated.js'; -import { configError, fetchError } from './source-errors.js'; /** * @typedef BaseSanityOptions @@ -34,7 +33,7 @@ const SANITY_OPTION_DEFAULTS = { */ export default function sanitySource(options) { if (!options.projectId || !options.apiToken) { - return errAsync(configError('Missing projectId and/or apiToken')); + return errAsync(new SourceConfigError('Missing projectId and/or apiToken')); } const assembledOptions = { @@ -42,7 +41,7 @@ export default function sanitySource(options) { ...options }; - return ResultAsync.fromPromise(import('@sanity/client'), () => configError('Could not find "@sanity/client". Make sure you have installed it.')) + return ResultAsync.fromPromise(import('@sanity/client'), () => new SourceMissingDependencyError('Could not find "@sanity/client". Make sure you have installed it.')) .map(({ createClient }) => { const sanityClient = createClient({ projectId: assembledOptions.projectId, @@ -96,7 +95,7 @@ export default function sanitySource(options) { dataPromise: fetchPaginated({ fetchPageFn: (params) => { const q = `${queryFull}[${params.offset}..${params.offset + params.limit - 1}]`; - return ResultAsync.fromPromise(sanityClient.fetch(q), (e) => fetchError(`Could not fetch page with query: '${q}'`)); + return ResultAsync.fromPromise(sanityClient.fetch(q), (e) => new SourceFetchError(`Could not fetch page with query: '${q}'`, { cause: e })); }, limit: assembledOptions.limit, logger: ctx.logger @@ -108,7 +107,7 @@ export default function sanitySource(options) { dataPromise: fetchPaginated({ fetchPageFn: (params) => { const q = `${query.query}[${params.offset}..${params.offset + params.limit - 1}]`; - return ResultAsync.fromPromise(sanityClient.fetch(q), (e) => fetchError(`Could not fetch page with query: '${q}'`)); + return ResultAsync.fromPromise(sanityClient.fetch(q), (e) => new SourceFetchError(`Could not fetch page with query: '${q}'`, { cause: e })); }, limit: assembledOptions.limit, logger: ctx.logger @@ -116,7 +115,7 @@ export default function sanitySource(options) { }); } else { ctx.logger.error(`Invalid query: ${query}`); - return err(configError(`Invalid query: ${query}`)); + return err(new SourceFetchError(`Invalid query: ${query}`)); } } diff --git a/packages/content/lib/sources/source-errors.js b/packages/content/lib/sources/source-errors.js deleted file mode 100644 index 02366276..00000000 --- a/packages/content/lib/sources/source-errors.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @param {string} [message] - */ -export const fetchError = (message) => ({ - type: /** @type {const} */ ('fetch'), - message -}); - -/** - * @param {string} [message] - */ -export const parseError = (message) => ({ - type: /** @type {const} */ ('parse'), - message -}); - -/** - * @param {string} [message] - */ -export const configError = (message) => ({ - type: /** @type {const} */ ('config'), - message -}); - -/** - * @typedef {ReturnType | ReturnType | ReturnType} SourceError - */ diff --git a/packages/content/lib/sources/source.js b/packages/content/lib/sources/source.js index 7b21621c..6d111d88 100644 --- a/packages/content/lib/sources/source.js +++ b/packages/content/lib/sources/source.js @@ -11,7 +11,7 @@ * @template [T=unknown] * @typedef {object} SourceFetchPromise * @prop {string} id Id of the fetch request, used for logging and debugging - * @prop {import('neverthrow').ResultAsync>, import('./source-errors.js').SourceError>} dataPromise Promise that resolves to an array of documents + * @prop {import('neverthrow').ResultAsync>, SourceFetchError | SourceParseError>} dataPromise Promise that resolves to an array of documents */ /** @@ -24,13 +24,13 @@ * @template [T=unknown] * @typedef {object} ContentSource * @prop {string} id Id of the source. This will be the 'namespace' for the documents fetched from this source. - * @prop {(ctx: FetchContext) => import('neverthrow').Result>, import('./source-errors.js').SourceError>} fetch + * @prop {(ctx: FetchContext) => import('neverthrow').Result>, SourceFetchError | SourceParseError>} fetch */ /** * @template O * @template [T=unknown] - * @typedef {(options: O) => import('neverthrow').ResultAsync, import('./source-errors.js').SourceError>} ContentSourceBuilder + * @typedef {(options: O) => import('neverthrow').ResultAsync, SourceConfigError | SourceMissingDependencyError>} ContentSourceBuilder */ /** @@ -42,3 +42,51 @@ export function defineSource(src) { return src; } + +export class SourceFetchError extends Error { + /** + * @param {string} message + * @param {object} [options] + * @param {unknown} [options.cause] + */ + constructor(message, { cause } = {}) { + super(message, { cause }); + this.name = 'SourceFetchError'; + } +} + +export class SourceConfigError extends Error { + /** + * @param {string} message + * @param {object} [options] + * @param {unknown} [options.cause] + */ + constructor(message, { cause } = {}) { + super(message, { cause }); + this.name = 'SourceConfigError'; + } +} + +export class SourceParseError extends Error { + /** + * @param {string} message + * @param {object} [options] + * @param {unknown} [options.cause] + */ + constructor(message, { cause } = {}) { + super(message, { cause }); + this.name = 'SourceParseError'; + } +} + +export class SourceMissingDependencyError extends Error { + /** + * @param {string} message + * @param {object} [options] + * @param {unknown} [options.cause] + */ + constructor(message, { cause } = {}) { + super(message, { cause }); + this.name = 'SourceMissingDependencyError'; + } +} diff --git a/packages/content/lib/sources/strapi-source.js b/packages/content/lib/sources/strapi-source.js index 91668d99..7703356c 100644 --- a/packages/content/lib/sources/strapi-source.js +++ b/packages/content/lib/sources/strapi-source.js @@ -1,7 +1,6 @@ import qs from 'qs'; -import { defineSource } from './source.js'; +import { defineSource, SourceConfigError, SourceFetchError } from './source.js'; import { errAsync, ok, okAsync } from 'neverthrow'; -import { configError, fetchError } from './source-errors.js'; import { fetchPaginated } from '../utils/fetch-paginated.js'; import { safeKy } from '../utils/safe-ky.js'; @@ -252,7 +251,7 @@ class StrapiV3 extends StrapiVersionUtils { * @param {string} baseUrl * @param {string} identifier * @param {string} password - * @returns {import('neverthrow').ResultAsync} The JSON web token generated by Strapi + * @returns {import('neverthrow').ResultAsync} The JSON web token generated by Strapi */ function getJwt(baseUrl, identifier, password) { const url = new URL('/auth/local', baseUrl); @@ -262,7 +261,7 @@ function getJwt(baseUrl, identifier, password) { json: { identifier, password } }).json() .map(response => response.jwt) - .mapErr(e => fetchError(`Could not complete request to get JWT for ${identifier}: ${e.message}`)); + .mapErr(e => new SourceFetchError(`Could not complete request to get JWT for ${identifier}`, { cause: e })); } /** @@ -286,7 +285,7 @@ export default function strapiSource(options) { }; if (assembledOptions.version !== '4' && assembledOptions.version !== '3') { - return errAsync(configError(`Unsupported strapi version '${assembledOptions.version}'`)); + return errAsync(new SourceConfigError(`Unsupported strapi version '${assembledOptions.version}'`)); } return getToken(assembledOptions).map(token => @@ -339,7 +338,7 @@ export default function strapiSource(options) { return transformedContent; }) - .mapErr(e => fetchError(`Could not fetch page ${pageNum} of ${parsedQuery.contentType}: ${e.message}`)); + .mapErr(e => new SourceFetchError(`Could not fetch page ${pageNum} of ${parsedQuery.contentType}`, { cause: e })); }, limit: assembledOptions.limit, logger: ctx.logger diff --git a/packages/content/lib/utils/__tests__/content-transform-utils.test.js b/packages/content/lib/utils/__tests__/content-transform-utils.test.js index f1f2887f..a8e6053f 100644 --- a/packages/content/lib/utils/__tests__/content-transform-utils.test.js +++ b/packages/content/lib/utils/__tests__/content-transform-utils.test.js @@ -65,7 +65,7 @@ describe('content-transform-utils', () => { transformFn, logger, keys: ['test'] - })).toThrow(/Transform error/); + })).toThrow(/Error applying content transform/); }); }); diff --git a/packages/content/lib/utils/__tests__/data-store.test.js b/packages/content/lib/utils/__tests__/data-store.test.js index 54efec09..c5a08321 100644 --- a/packages/content/lib/utils/__tests__/data-store.test.js +++ b/packages/content/lib/utils/__tests__/data-store.test.js @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { DataStore, Document } from '../data-store.js'; +import { DataStore, DataStoreError, Document } from '../data-store.js'; describe('Document', () => { it('should create a document with id and data', () => { @@ -27,7 +27,11 @@ describe('Document', () => { throw new Error('Test error'); }); expect(result).toBeErr(); - expect(result._unsafeUnwrapErr().message).toBe('Test error'); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(DataStoreError); + expect(result._unsafeUnwrapErr().message).toBe('Error applying content transform'); + expect(result._unsafeUnwrapErr().cause).toBeInstanceOf(Error); + // @ts-expect-error cause is unknown + expect(result._unsafeUnwrapErr().cause.message).toBe('Test error'); }); }); @@ -43,7 +47,8 @@ describe('DataStore', () => { store.createNamespace('test-namespace'); const result = store.createNamespace('test-namespace'); expect(result).toBeErr(); - expect(result._unsafeUnwrapErr()).toBe('Namespace test-namespace already exists in data store'); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(DataStoreError); + expect(result._unsafeUnwrapErr().message).toBe('Namespace test-namespace already exists in data store'); }); it('should insert a document into a namespace', () => { @@ -59,7 +64,8 @@ describe('DataStore', () => { store.insert('test-namespace', 'test-doc', { content: 'test content' }); const result = store.insert('test-namespace', 'test-doc', { content: 'duplicate content' }); expect(result).toBeErr(); - expect(result._unsafeUnwrapErr()).toBe('Document test-doc already exists in namespace test-namespace'); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(DataStoreError); + expect(result._unsafeUnwrapErr().message).toBe('Document test-doc already exists in namespace test-namespace'); }); it('should get a document from a namespace', () => { @@ -76,7 +82,8 @@ describe('DataStore', () => { store.createNamespace('test-namespace'); const result = store.get('test-namespace', 'non-existent-doc'); expect(result).toBeErr(); - expect(result._unsafeUnwrapErr()).toBe('Document non-existent-doc not found in namespace test-namespace'); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(DataStoreError); + expect(result._unsafeUnwrapErr().message).toBe('Document non-existent-doc not found in namespace test-namespace'); }); it('should delete a document from a namespace', () => { diff --git a/packages/content/lib/utils/__tests__/fetch-paginated.test.js b/packages/content/lib/utils/__tests__/fetch-paginated.test.js index 6eb6c27a..92b02793 100644 --- a/packages/content/lib/utils/__tests__/fetch-paginated.test.js +++ b/packages/content/lib/utils/__tests__/fetch-paginated.test.js @@ -4,7 +4,7 @@ import { http, HttpResponse } from 'msw'; import { fetchPaginated } from '../fetch-paginated.js'; import { ResultAsync } from 'neverthrow'; import { createMockLogger } from '@bluecadet/launchpad-testing/test-utils.js'; -import { fetchError } from '../../sources/source-errors.js'; +import { SourceFetchError } from '../../sources/source.js'; const server = setupServer(); @@ -45,7 +45,7 @@ describe('fetchPaginated', () => { fetchPageFn: ({ limit, offset }) => ResultAsync.fromPromise( fetch(`http://example.com/api?limit=${limit}&offset=${offset}`).then(res => res.json()), - () => fetchError('Failed to fetch') + (e) => new SourceFetchError('Failed to fetch', { cause: e }) ) }); @@ -74,7 +74,7 @@ describe('fetchPaginated', () => { fetchPageFn: ({ limit, offset }) => ResultAsync.fromPromise( fetch(`http://example.com/api?limit=${limit}&offset=${offset}`).then(res => res.json()), - () => fetchError('Failed to fetch') + (e) => new SourceFetchError('Failed to fetch', { cause: e }) ) }); @@ -100,13 +100,13 @@ describe('fetchPaginated', () => { if (!res.ok) throw new Error('API error'); return res.json(); }), - () => fetchError('Failed to fetch') + (e) => new SourceFetchError('Failed to fetch', { cause: e }) ) }); expect(result).toBeErr(); const error = result._unsafeUnwrapErr(); - expect(error.type).toBe('fetch'); + expect(error).toBeInstanceOf(SourceFetchError); expect(error.message).toBe('Failed to fetch'); }); @@ -129,7 +129,7 @@ describe('fetchPaginated', () => { fetchPageFn: ({ limit, offset }) => ResultAsync.fromPromise( fetch(`http://example.com/api?limit=${limit}&offset=${offset}`).then(res => res.json()), - () => fetchError('Failed to fetch') + (e) => new SourceFetchError('Failed to fetch', { cause: e }) ) }); diff --git a/packages/content/lib/utils/__tests__/file-utils.test.js b/packages/content/lib/utils/__tests__/file-utils.test.js index 92ce7f82..ef19314b 100644 --- a/packages/content/lib/utils/__tests__/file-utils.test.js +++ b/packages/content/lib/utils/__tests__/file-utils.test.js @@ -55,7 +55,7 @@ describe('FileUtils', () => { '/test-dir': { 'file1.txt': 'content', 'file2.json': '{}', - 'subdir': { + subdir: { 'file3.csv': 'data' } } @@ -71,7 +71,7 @@ describe('FileUtils', () => { it('should exclude specified files', async () => { const result = await FileUtils.removeFilesFromDir('/test-dir', ['*.json', '**/*.csv']); expect(result).toBeOk(); - expect(vol.readdirSync('/test-dir', {recursive: true})).toEqual(expect.arrayContaining(['file2.json', 'subdir/file3.csv', 'subdir'])); + expect(vol.readdirSync('/test-dir', { recursive: true })).toEqual(expect.arrayContaining(['file2.json', 'subdir/file3.csv', 'subdir'])); }); }); diff --git a/packages/content/lib/utils/__tests__/markdown-it-italic-bold.test.js b/packages/content/lib/utils/__tests__/markdown-it-italic-bold.test.js index 43e9df11..9be85869 100644 --- a/packages/content/lib/utils/__tests__/markdown-it-italic-bold.test.js +++ b/packages/content/lib/utils/__tests__/markdown-it-italic-bold.test.js @@ -1,47 +1,47 @@ -import { describe, it, expect } from 'vitest' -import MarkdownIt from 'markdown-it' -import italicBoldPlugin from '../markdown-it-italic-bold.js' +import { describe, it, expect } from 'vitest'; +import MarkdownIt from 'markdown-it'; +import italicBoldPlugin from '../markdown-it-italic-bold.js'; describe('markdown-it-italic-bold plugin', () => { - it('should convert * to for italics', () => { - const md = new MarkdownIt() - md.use(italicBoldPlugin) - const result = md.render('*italic*') - expect(result.trim()).toBe('

italic

') - }) + it('should convert * to for italics', () => { + const md = new MarkdownIt(); + md.use(italicBoldPlugin); + const result = md.render('*italic*'); + expect(result.trim()).toBe('

italic

'); + }); - it('should convert _ to for italics', () => { - const md = new MarkdownIt() - md.use(italicBoldPlugin) - const result = md.render('_italic_') - expect(result.trim()).toBe('

italic

') - }) + it('should convert _ to for italics', () => { + const md = new MarkdownIt(); + md.use(italicBoldPlugin); + const result = md.render('_italic_'); + expect(result.trim()).toBe('

italic

'); + }); - it('should convert ** to for bold', () => { - const md = new MarkdownIt() - md.use(italicBoldPlugin) - const result = md.render('**bold**') - expect(result.trim()).toBe('

bold

') - }) + it('should convert ** to for bold', () => { + const md = new MarkdownIt(); + md.use(italicBoldPlugin); + const result = md.render('**bold**'); + expect(result.trim()).toBe('

bold

'); + }); - it('should convert __ to for bold', () => { - const md = new MarkdownIt() - md.use(italicBoldPlugin) - const result = md.render('__bold__') - expect(result.trim()).toBe('

bold

') - }) + it('should convert __ to for bold', () => { + const md = new MarkdownIt(); + md.use(italicBoldPlugin); + const result = md.render('__bold__'); + expect(result.trim()).toBe('

bold

'); + }); - it('should handle mixed italic and bold', () => { - const md = new MarkdownIt() - md.use(italicBoldPlugin) - const result = md.render('*italic* and **bold**') - expect(result.trim()).toBe('

italic and bold

') - }) + it('should handle mixed italic and bold', () => { + const md = new MarkdownIt(); + md.use(italicBoldPlugin); + const result = md.render('*italic* and **bold**'); + expect(result.trim()).toBe('

italic and bold

'); + }); - it('should handle nested italic and bold', () => { - const md = new MarkdownIt() - md.use(italicBoldPlugin) - const result = md.render('**bold *italic* text**') - expect(result.trim()).toBe('

bold italic text

') - }) -}) + it('should handle nested italic and bold', () => { + const md = new MarkdownIt(); + md.use(italicBoldPlugin); + const result = md.render('**bold *italic* text**'); + expect(result.trim()).toBe('

bold italic text

'); + }); +}); diff --git a/packages/content/lib/utils/__tests__/safe-ky.test.js b/packages/content/lib/utils/__tests__/safe-ky.test.js index 2d16b0a6..d65d027f 100644 --- a/packages/content/lib/utils/__tests__/safe-ky.test.js +++ b/packages/content/lib/utils/__tests__/safe-ky.test.js @@ -1,7 +1,7 @@ import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'; import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; -import { safeKy, SafeKyError } from '../safe-ky.js'; +import { safeKy, SafeKyFetchError, SafeKyParseError } from '../safe-ky.js'; import { ok } from 'neverthrow'; const server = setupServer(); @@ -53,7 +53,7 @@ describe('safeKy', () => { const jsonResult = await result.json(); expect(jsonResult).toBeErr(); - expect(jsonResult._unsafeUnwrapErr()).toBeInstanceOf(SafeKyError.FetchError); + expect(jsonResult._unsafeUnwrapErr()).toBeInstanceOf(SafeKyFetchError); expect(jsonResult._unsafeUnwrapErr().message).toContain('Error during request'); }); @@ -70,7 +70,7 @@ describe('safeKy', () => { const jsonResult = await result.json(); expect(jsonResult).toBeErr(); - expect(jsonResult._unsafeUnwrapErr()).toBeInstanceOf(SafeKyError.ParseError); + expect(jsonResult._unsafeUnwrapErr()).toBeInstanceOf(SafeKyParseError); expect(jsonResult._unsafeUnwrapErr().message).toContain('Error parsing JSON'); }); }); diff --git a/packages/content/lib/utils/content-transform-utils.js b/packages/content/lib/utils/content-transform-utils.js index dbc822dd..be59e6f4 100644 --- a/packages/content/lib/utils/content-transform-utils.js +++ b/packages/content/lib/utils/content-transform-utils.js @@ -1,10 +1,10 @@ import chalk from 'chalk'; -import { err, ok, Result } from 'neverthrow'; +import { ok } from 'neverthrow'; /** * @param {import('./data-store.js').DataStore} dataStore * @param {import('./data-store.js').DataKeys } [ids] A list containing a combination of namespace ids, and namespace/document id tuples. If not provided, all documents will be matched. - * @returns {import('neverthrow').Result, string>} + * @returns {import('neverthrow').Result, Error>} */ export function getMatchingDocuments(dataStore, ids) { if (!ids) { @@ -29,7 +29,7 @@ export function applyTransformToFiles({ dataStore, path, transformFn, logger, ke const matchingDocuments = getMatchingDocuments(dataStore, keys); if (matchingDocuments.isErr()) { - throw new Error(matchingDocuments.error); + throw matchingDocuments.error; } for (const document of matchingDocuments.value) { diff --git a/packages/content/lib/utils/data-store.js b/packages/content/lib/utils/data-store.js index a75b9124..6e22a830 100644 --- a/packages/content/lib/utils/data-store.js +++ b/packages/content/lib/utils/data-store.js @@ -1,6 +1,16 @@ import { JSONPath } from 'jsonpath-plus'; import { ok, err, Result } from 'neverthrow'; +export class DataStoreError extends Error { + /** + * @param {ConstructorParameters} args + */ + constructor(...args) { + super(...args); + this.name = 'DataStoreError'; + } +} + /** * @typedef {Array} DataKeys A list containing a combination of namespace ids, and namespace/document id tuples. */ @@ -54,7 +64,7 @@ export class Document { * Apply a function to each element matching the given jsonpath. * @param {string} pathExpression * @param {(x: unknown) => unknown} fn - * @returns {Result} + * @returns {Result} */ apply(pathExpression, fn) { // catch errrors thrown from JSONPath OR the fn callback @@ -70,11 +80,7 @@ export class Document { return ok(undefined); } catch (e) { - if (e instanceof Error) { - return err(e); - } - - return err(new Error(String(e))); + return err(new DataStoreError('Error applying content transform', { cause: e })); } } } @@ -107,11 +113,11 @@ class Namespace { /** * @param {string} id * @param {unknown} data - * @returns {Result} + * @returns {Result} */ insert(id, data) { if (this.#documents.has(id)) { - return err(`Document ${id} already exists in namespace ${this.#id}`); + return err(new DataStoreError(`Document ${id} already exists in namespace ${this.#id}`)); } this.#documents.set(id, new Document(id, data)); @@ -120,12 +126,12 @@ class Namespace { /** * @param {string} id - * @returns {Result} + * @returns {Result} */ get(id) { const document = this.#documents.get(id); if (!document) { - return err(`Document ${id} not found in namespace ${this.#id}`); + return err(new DataStoreError(`Document ${id} not found in namespace ${this.#id}`)); } return ok(document); @@ -137,7 +143,7 @@ class Namespace { /** * @param {string} id - * @returns {Result} + * @returns {Result} */ delete(id) { this.#documents.delete(id); @@ -147,7 +153,7 @@ class Namespace { /** * @param {string} id * @param {unknown} data - * @returns {Result} + * @returns {Result} */ update(id, data) { this.#documents.set(id, new Document(id, data)); @@ -168,12 +174,12 @@ export class DataStore { /** * @param {string} namespaceId * @param {string} documentId - * @returns {Result} + * @returns {Result} */ get(namespaceId, documentId) { const namespace = this.#namespaces.get(namespaceId); if (!namespace) { - return err(`Namespace ${namespaceId} not found in data store`); + return err(new DataStoreError(`Namespace ${namespaceId} not found in data store`)); } return namespace.get(documentId); @@ -181,12 +187,12 @@ export class DataStore { /** * @param {string} namespaceId - * @returns {Result, string>} + * @returns {Result, DataStoreError>} */ namespace(namespaceId) { const namespace = this.#namespaces.get(namespaceId); if (!namespace) { - return err(`Namespace ${namespaceId} not found in data store`); + return err(new DataStoreError(`Namespace ${namespaceId} not found in data store`)); } return ok(namespace.documents()); @@ -207,11 +213,11 @@ export class DataStore { /** * @param {string} namespaceId - * @returns {Result} + * @returns {Result} */ createNamespace(namespaceId) { if (this.#namespaces.has(namespaceId)) { - return err(`Namespace ${namespaceId} already exists in data store`); + return err(new DataStoreError(`Namespace ${namespaceId} already exists in data store`)); } this.#namespaces.set(namespaceId, new Namespace(namespaceId)); @@ -221,7 +227,7 @@ export class DataStore { /** * @param {string} namespaceId * @param {Map} map - * @returns {Result} + * @returns {Result} */ createNamespaceFromMap(namespaceId, map) { const namespaceResult = this.createNamespace(namespaceId); @@ -244,12 +250,12 @@ export class DataStore { * @param {string} namespaceId * @param {string} documentId * @param {unknown} data - * @returns {Result} + * @returns {Result} */ insert(namespaceId, documentId, data) { const namespace = this.#namespaces.get(namespaceId); if (!namespace) { - return err(`Namespace ${namespaceId} not found in data store`); + return err(new DataStoreError(`Namespace ${namespaceId} not found in data store`)); } return namespace.insert(documentId, data); @@ -258,12 +264,12 @@ export class DataStore { /** * @param {string} namespaceId * @param {string} documentId - * @returns {Result} + * @returns {Result} */ delete(namespaceId, documentId) { const namespace = this.#namespaces.get(namespaceId); if (!namespace) { - return err(`Namespace ${namespaceId} not found in data store`); + return err(new DataStoreError(`Namespace ${namespaceId} not found in data store`)); } return namespace.delete(documentId); @@ -272,7 +278,7 @@ export class DataStore { /** * Get lists of documents matching the passed DataKeys grouped by namespace. * @param {DataKeys} [ids] A list containing a combination of namespace ids, and namespace/document id tuples. If not provided, all documents will be matched. - * @returns {Result }>, string>} + * @returns {Result }>, DataStoreError>} */ filter(ids) { if (!ids) { diff --git a/packages/content/lib/utils/fetch-paginated.js b/packages/content/lib/utils/fetch-paginated.js index 9baf6b87..4fe140e3 100644 --- a/packages/content/lib/utils/fetch-paginated.js +++ b/packages/content/lib/utils/fetch-paginated.js @@ -1,19 +1,19 @@ import { ResultAsync, err, errAsync, ok, okAsync } from 'neverthrow'; -import { fetchError } from '../sources/source-errors.js'; +import { SourceFetchError } from '../sources/source.js'; /** * @template {unknown} T * @typedef FetchPaginatedOptions * @property {number} limit The number of items to fetch per page * @property {number} [maxFetchCount] The maximum number of pages to fetch. If this is reached, the fetch will be terminated early. - * @property {(params: {limit: number, offset: number}) => ResultAsync} fetchPageFn A function that takes a params object and returns a ResultAsync of an array of T. To indicate the end of pagination, return an empty array, or null. + * @property {(params: {limit: number, offset: number}) => ResultAsync} fetchPageFn A function that takes a params object and returns a ResultAsync of an array of T. To indicate the end of pagination, return an empty array, or null. * @property {import('@bluecadet/launchpad-utils').Logger} logger A logger instance */ /** * @template {unknown} T * @template {unknown} M - * @typedef {ResultAsync} : {pages: Array, meta: M}, import('../sources/source-errors.js').SourceError>} FetchPaginatedResult + * @typedef {ResultAsync} : {pages: Array, meta: M}, SourceFetchError>} FetchPaginatedResult */ /** @@ -29,7 +29,7 @@ export function fetchPaginated({ fetchPageFn, limit, logger, maxFetchCount = 100 let page = 0; /** - * @returns {ResultAsync} + * @returns {ResultAsync} */ const fetchNextPage = () => { logger.debug(`Fetching page ${page}`); @@ -42,7 +42,7 @@ export function fetchPaginated({ fetchPageFn, limit, logger, maxFetchCount = 100 page++; if (page >= maxFetchCount) { - return errAsync(fetchError('Maximum fetch count reached. This is likely a bug.')); + return errAsync(new SourceFetchError('Maximum fetch count reached. This is likely a bug. Make sure your fetchPageFn ret')); } return fetchNextPage(); diff --git a/packages/content/lib/utils/file-utils.js b/packages/content/lib/utils/file-utils.js index 629381c2..be281345 100644 --- a/packages/content/lib/utils/file-utils.js +++ b/packages/content/lib/utils/file-utils.js @@ -3,6 +3,16 @@ import fs from 'fs'; import { glob } from 'glob'; import { okAsync, ResultAsync } from 'neverthrow'; +export class FileUtilsError extends Error { + /** + * @param {ConstructorParameters} args + */ + constructor(...args) { + super(...args); + this.name = 'FileUtilsError'; + } +} + /** * @param {string} dirPath */ @@ -15,7 +25,7 @@ export function isDir(dirPath) { * @param {unknown} json * @param {string} filePath * @param {boolean} appendJsonExtension - * @returns {ResultAsync} + * @returns {ResultAsync} */ export function saveJson(json, filePath, appendJsonExtension = true) { if (appendJsonExtension && !(filePath + '').endsWith('.json')) { @@ -24,14 +34,14 @@ export function saveJson(json, filePath, appendJsonExtension = true) { const jsonStr = (typeof json === 'string') ? json : JSON.stringify(json, null, 0); return ensureDir(path.dirname(filePath)) - .andThen(() => ResultAsync.fromPromise(fs.promises.writeFile(filePath, jsonStr), (_) => `Could not write file ${filePath}`)); + .andThen(() => ResultAsync.fromPromise(fs.promises.writeFile(filePath, jsonStr), (e) => new FileUtilsError(`Could not write file ${filePath}`, { cause: e }))); } /** * Removes all files and subdirectories of `dirPath`, except for `exclude`. * @param {string} dirPath Any absolute directory path * @param {string[]} [exclude] Array of glob patterns to exclude (e.g. ['*.json', '** /*.csv', 'my-important-folder/**']). Glob patterns are relative to `dirPath`. - * @returns {ResultAsync} + * @returns {ResultAsync} */ export function removeFilesFromDir(dirPath, exclude = []) { return ResultAsync.fromPromise( @@ -40,13 +50,13 @@ export function removeFilesFromDir(dirPath, exclude = []) { dot: true, nodir: true // Only match files, not directories }), - (error) => `Failed to glob directory ${dirPath}: ${error}` + (error) => new FileUtilsError(`Failed to glob directory ${dirPath}`, { cause: error }) ) .andThen((files) => { const deletePromises = files.map((file) => ResultAsync.fromPromise( fs.promises.unlink(file), // Use unlink instead of rm to only remove files - (error) => `Failed to remove ${file}: ${error}` + (error) => new FileUtilsError(`Failed to remove ${file}`, { cause: error }) ) ); return ResultAsync.combine(deletePromises); @@ -59,7 +69,7 @@ export function removeFilesFromDir(dirPath, exclude = []) { dot: true, nodir: false // Match directories this time }), - (error) => `Failed to glob directories in ${dirPath}: ${error}` + (error) => new FileUtilsError(`Failed to glob directories in ${dirPath}`, { cause: error }) ) .andThen((dirs) => { // Sort directories by depth (deepest first) @@ -68,7 +78,7 @@ export function removeFilesFromDir(dirPath, exclude = []) { const removeDirPromises = dirs.map((dir) => ResultAsync.fromPromise( fs.promises.rmdir(dir), - (error) => `Failed to remove directory ${dir}: ${error}` + (error) => new FileUtilsError(`Failed to remove directory ${dir}`, { cause: error }) ).orElse(() => okAsync(undefined)) // Ignore errors if directory is not empty ); return ResultAsync.combine(removeDirPromises); @@ -95,7 +105,7 @@ export function pad(num, size) { /** * @param {string} dirPath - * @returns {ResultAsync} + * @returns {ResultAsync} */ export function removeDirIfEmpty(dirPath) { if (!isDir(dirPath)) { @@ -111,47 +121,47 @@ export function removeDirIfEmpty(dirPath) { /** * @param {string} dirPath - * @returns {ResultAsync} + * @returns {ResultAsync} */ export function isDirEmpty(dirPath) { // @see https://stackoverflow.com/a/39218759/782899 - return ResultAsync.fromPromise(fs.promises.readdir(dirPath), (_) => `Could not read dir ${dirPath}`) + return ResultAsync.fromPromise(fs.promises.readdir(dirPath), (e) => new FileUtilsError(`Could not read dir ${dirPath}`, { cause: e })) .andThen(files => okAsync(files.length === 0)); } /** * Ensures that the directory exists. If the directory structure does not exist, it is created. * @param {string} dirPath - * @returns {ResultAsync} + * @returns {ResultAsync} */ export function ensureDir(dirPath) { return ResultAsync.fromPromise( fs.promises.mkdir(dirPath, { recursive: true }), - wrapError(`Failed to create directory ${dirPath}`) + (e) => new FileUtilsError(`Failed to create directory ${dirPath}`, { cause: e }) ).map(() => undefined); // return void on success } /** * Removes a file or directory. The directory can have contents. If the path does not exist, silently does nothing. * @param {string} dir - * @returns {ResultAsync} + * @returns {ResultAsync} */ export function remove(dir) { return ResultAsync.fromPromise( fs.promises.rm(dir, { recursive: true, force: true }), - wrapError(`Failed to remove ${dir}`) + (e) => new FileUtilsError(`Failed to remove ${dir}`, { cause: e }) ); } /** * returns true if the path exists, false otherwise * @param {string} dir - * @returns {ResultAsync} + * @returns {ResultAsync} */ export function pathExists(dir) { return ResultAsync.fromPromise( fs.promises.access(dir).then(() => true).catch(() => false), - wrapError(`Failed to check if path exists ${dir}`) + (e) => new FileUtilsError(`Failed to check if path exists ${dir}`, { cause: e }) ); } @@ -161,12 +171,12 @@ export function pathExists(dir) { * @param {string} dest * @param {object} [options] * @param {boolean} [options.preserveTimestamps] - * @returns {ResultAsync} + * @returns {ResultAsync} */ export function copy(src, dest, options = { preserveTimestamps: true }) { return ResultAsync.fromPromise( fs.promises.stat(src), - wrapError(`Failed to get file stats for ${src}`) + (e) => new FileUtilsError(`Failed to get file stats for ${src}`, { cause: e }) ).andThrough((stats) => { if (stats.isDirectory()) { return copyDir(src, dest, options); @@ -175,7 +185,7 @@ export function copy(src, dest, options = { preserveTimestamps: true }) { } }).andThen((stats) => { if (options.preserveTimestamps) { - return ResultAsync.fromPromise(fs.promises.utimes(dest, stats.atime, stats.mtime), wrapError(`Failed to set file timestamps for ${dest}`)); + return ResultAsync.fromPromise(fs.promises.utimes(dest, stats.atime, stats.mtime), (e) => new FileUtilsError(`Failed to set file timestamps for ${dest}`, { cause: e })); } return okAsync(undefined); }); @@ -187,11 +197,11 @@ export function copy(src, dest, options = { preserveTimestamps: true }) { * @param {string} dest * @param {object} [options] * @param {boolean} [options.preserveTimestamps] - * @returns {ResultAsync} + * @returns {ResultAsync} */ function copyDir(src, dest, options = { preserveTimestamps: true }) { return ensureDir(dest) - .andThen(() => ResultAsync.fromPromise(fs.promises.readdir(src), wrapError(`Failed to read dir ${src}`))) + .andThen(() => ResultAsync.fromPromise(fs.promises.readdir(src), (e) => new FileUtilsError(`Failed to read dir ${src}`, { cause: e }))) .andThen((entries) => ResultAsync.combine(entries.map((entry) => copy(path.join(src, entry), path.join(dest, entry), options) @@ -203,15 +213,11 @@ function copyDir(src, dest, options = { preserveTimestamps: true }) { * Copies a file from `src` to `dest`. * @param {string} src * @param {string} dest - * @returns {ResultAsync} + * @returns {ResultAsync} */ function copyFile(src, dest) { return ResultAsync.fromPromise( fs.promises.copyFile(src, dest), - wrapError(`Failed to copy file ${src} to ${dest}`) + (e) => new FileUtilsError(`Failed to copy file ${src} to ${dest}`, { cause: e }) ); } - -const wrapError = (/** @type string */ message) => (/** @type unknown */ error) => { - return (error instanceof Error) ? `${message}: ${error.message}` : `${message}: ${error}`; -}; diff --git a/packages/content/lib/utils/safe-ky.js b/packages/content/lib/utils/safe-ky.js index fd503902..5771dfa4 100644 --- a/packages/content/lib/utils/safe-ky.js +++ b/packages/content/lib/utils/safe-ky.js @@ -1,26 +1,25 @@ import ky from 'ky'; import { Err, err, Ok, ok, ResultAsync } from 'neverthrow'; -class BaseSafeKyError extends Error { +export class SafeKyFetchError extends Error { /** - * @param {string} message - * @param {unknown} [cause] - */ - constructor(message, cause) { - if (cause === undefined) { - super(message); - } else if (cause instanceof Error) { - super(`${message}: ${cause.message}`, { cause }); - } else { - super(`${message}: ${cause}`); - } + * @param {ConstructorParameters} args + */ + constructor(...args) { + super(...args); + this.name = 'SafeKyFetchError'; } } -export const SafeKyError = { - FetchError: class extends BaseSafeKyError { }, - ParseError: class extends BaseSafeKyError { } -}; +export class SafeKyParseError extends Error { + /** + * @param {ConstructorParameters} args + */ + constructor(...args) { + super(...args); + this.name = 'SafeKyParseError'; + } +} /** * Wraps a ky request in a ResultAsync @@ -36,17 +35,17 @@ export function safeKy(input, options) { * @template [T=unknown] * @typedef { Omit, 'json' | 'text' | 'arrayBuffer' | 'blob'> * & { - * json: () => import('neverthrow').ResultAsync, - * text: () => import('neverthrow').ResultAsync, - * arrayBuffer: () => import('neverthrow').ResultAsync, - * blob: () => import('neverthrow').ResultAsync + * json: () => import('neverthrow').ResultAsync, + * text: () => import('neverthrow').ResultAsync, + * arrayBuffer: () => import('neverthrow').ResultAsync, + * blob: () => import('neverthrow').ResultAsync * } * } SafeKyResponseResult */ /** * @template T - * @extends {ResultAsync, BaseSafeKyError>} + * @extends {ResultAsync, SafeKyFetchError | SafeKyParseError>} */ class SafeKyResultAsync extends ResultAsync { /** @@ -66,16 +65,16 @@ class SafeKyResultAsync extends ResultAsync { url: res.url, redirected: res.redirected, body: res.body, - json: () => ResultAsync.fromPromise(res.json(), (error) => new SafeKyError.ParseError('Error parsing JSON', error)), - text: () => ResultAsync.fromPromise(res.text(), (error) => new SafeKyError.ParseError('Error parsing text', error)), - arrayBuffer: () => ResultAsync.fromPromise(res.arrayBuffer(), (error) => new SafeKyError.ParseError('Error parsing array buffer', error)), - blob: () => ResultAsync.fromPromise(res.blob(), (error) => new SafeKyError.ParseError('Error parsing blob', error)) + json: () => ResultAsync.fromPromise(res.json(), (error) => new SafeKyParseError('Error parsing JSON', { cause: error })), + text: () => ResultAsync.fromPromise(res.text(), (error) => new SafeKyParseError('Error parsing text', { cause: error })), + arrayBuffer: () => ResultAsync.fromPromise(res.arrayBuffer(), (error) => new SafeKyParseError('Error parsing array buffer', { cause: error })), + blob: () => ResultAsync.fromPromise(res.blob(), (error) => new SafeKyParseError('Error parsing blob', { cause: error })) }; - return /** @type {Ok} */ (new Ok(remapped)); + return /** @type {Ok} */ (new Ok(remapped)); }) .catch((error) => { - return /** @type {Err} */(new Err(new SafeKyError.FetchError('Error during request', error))); + return /** @type {Err} */(new Err(new SafeKyFetchError('Error during request', { cause: error }))); }); return new SafeKyResultAsync(newPromise); From 07b76968bee15111d1232379246a6e0b9b82570f Mon Sep 17 00:00:00 2001 From: Clay Tercek <30105080+claytercek@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:23:44 -0400 Subject: [PATCH 2/4] fix some tests and types --- .../__tests__/content-plugin-driver.test.js | 18 ++++++++++-------- packages/content/lib/content-options.js | 4 +++- packages/utils/lib/plugin-driver.js | 1 + 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/content/lib/__tests__/content-plugin-driver.test.js b/packages/content/lib/__tests__/content-plugin-driver.test.js index f6ff5073..ddc4eac0 100644 --- a/packages/content/lib/__tests__/content-plugin-driver.test.js +++ b/packages/content/lib/__tests__/content-plugin-driver.test.js @@ -15,14 +15,14 @@ describe('ContentPluginDriver', () => { }); const paths = { - /** @param {string | undefined} source */ + /** @param {string} [source] */ getDownloadPath: (source) => source ? `/downloads/${source}` : '/downloads', /** - * @param {string | undefined} source - * @param {string | undefined} plugin + * @param {string} [source] + * @param {string} [plugin] */ getTempPath: (source, plugin) => source ? `/temp/${plugin}/${source}` : `/temp/${plugin}`, - /** @param {string | undefined} source */ + /** @param {string} [source] */ getBackupPath: (source) => source ? `/backups/${source}` : '/backups' }; @@ -46,8 +46,8 @@ describe('ContentPluginDriver', () => { onContentFetchDone(ctx) { expect(ctx.data).toBe(dataStore); expect(ctx.contentOptions).toBe(options); - expect(ctx.paths.getDownloadPath).toBe(paths.getDownloadPath); - expect(ctx.paths.getBackupPath).toBe(paths.getBackupPath); + expect(ctx.paths.getDownloadPath()).toBe(paths.getDownloadPath()); + expect(ctx.paths.getBackupPath()).toBe(paths.getBackupPath()); // Test plugin-specific temp path expect(ctx.paths.getTempPath('source')).toBe('/temp/test-plugin/source'); } @@ -55,7 +55,8 @@ describe('ContentPluginDriver', () => { }); contentDriver.add(plugin); - await contentDriver.runHookSequential('onContentFetchDone'); + const result = await contentDriver.runHookSequential('onContentFetchDone'); + expect(result).toBeOk(); }); it('should handle plugin-specific temp paths correctly', async () => { @@ -88,7 +89,8 @@ describe('ContentPluginDriver', () => { contentDriver.add(plugin1); contentDriver.add(plugin2); - await contentDriver.runHookSequential('onContentFetchDone'); + const result = await contentDriver.runHookSequential('onContentFetchDone'); + expect(result).toBeOk(); }); }); diff --git a/packages/content/lib/content-options.js b/packages/content/lib/content-options.js index aa36ad01..c0232362 100644 --- a/packages/content/lib/content-options.js +++ b/packages/content/lib/content-options.js @@ -1,10 +1,12 @@ +import { SourceConfigError } from './sources/source.js'; + export const DOWNLOAD_PATH_TOKEN = '%DOWNLOAD_PATH%'; export const TIMESTAMP_TOKEN = '%TIMESTAMP%'; /** * @typedef {import('./sources/source.js').ContentSource * | Promise - * | import('neverthrow').ResultAsync + * | ReturnType> * } ConfigContentSource */ diff --git a/packages/utils/lib/plugin-driver.js b/packages/utils/lib/plugin-driver.js index 6c981e09..94c3b677 100644 --- a/packages/utils/lib/plugin-driver.js +++ b/packages/utils/lib/plugin-driver.js @@ -1,6 +1,7 @@ import chalk from 'chalk'; import onExit from './on-exit.js'; import { err, ok, okAsync, ResultAsync } from 'neverthrow'; +import { AssertionError } from 'assert'; class PluginError extends Error { /** From 704b36a587636881d47ddd388756c3f6df65d34b Mon Sep 17 00:00:00 2001 From: Clay Tercek <30105080+claytercek@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:03:57 -0400 Subject: [PATCH 3/4] fix exports --- package-lock.json | 228 ++++++++++++++------------ packages/cli/package.json | 3 +- packages/content/lib/plugins/index.js | 1 + packages/content/package.json | 6 +- 4 files changed, 132 insertions(+), 106 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfb896ec..52c71e26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -394,9 +394,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz", - "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1682,9 +1682,9 @@ } }, "node_modules/@mswjs/interceptors": { - "version": "0.36.6", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.36.6.tgz", - "integrity": "sha512-issnYydStyH0wPEeU7CMwfO7kI668ffVtzKRMRS7H7BliOYuPuwEZxh9dwiXV+oeHBxT5SXT0wPwV8T7V2PJUA==", + "version": "0.36.7", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.36.7.tgz", + "integrity": "sha512-sdx02Wlus5hv6Bx7uUDb25gb0WGjCuSgnJB2LVERemoSGuqkZMe3QI6nEXhieFGtYwPrZbYrT2vPbsFN2XfbUw==", "dev": true, "license": "MIT", "dependencies": { @@ -2578,9 +2578,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.3.tgz", - "integrity": "sha512-tSQrmKKatLDGnG92h40GD7FzUt0MjahaHwOME4VAFeeA/Xopayq5qLyQRy7Jg/pjgKIFBXuKcGhJo+UdYG55jQ==", + "version": "20.17.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.4.tgz", + "integrity": "sha512-Fi1Bj8qTJr4f1FDdHFR7oMlOawEYSzkHNdBJK+aRjcDDNHwEV3jPPjuZP2Lh2QNgXeqzM8Y+U6b6urKAog2rZw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2892,23 +2892,23 @@ } }, "node_modules/@vue/devtools-api": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.6.0.tgz", - "integrity": "sha512-FGxX7jatS0ZcsglIBcdxAQciYSUpb/eXt610x0YDVBdIJaH0x6iDg0vs4MhbzSIrRCywjFmW+ZwpYus/eFIS8Q==", + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.6.1.tgz", + "integrity": "sha512-W99uw1nJKWeG4V2Bgp/pR1vnIREfixmnYW71wjtID7Gn/XnHH+nhfJmNy/0DjxcXOM14POVBDkl9JGlsOx1UjQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^7.6.0" + "@vue/devtools-kit": "^7.6.1" } }, "node_modules/@vue/devtools-kit": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.0.tgz", - "integrity": "sha512-82Mhrk/PRiTGfLwj73mXfrrocnCbWEPLLk3r4HhIkcieTa610Snlqc0a9OBiSsldX98YI6rOcL04+Dud1tLIKg==", + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.1.tgz", + "integrity": "sha512-cCcdskZDLqKwJUHq1+kH9zAfYM+y9OFq8J2NT0xcAUYpu8K5IJd03ydZkvr7ydOd9UKBxrGyZpYe9PpJ0ChrVw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^7.6.0", + "@vue/devtools-shared": "^7.6.1", "birpc": "^0.2.19", "hookable": "^5.5.3", "mitt": "^3.0.1", @@ -2918,9 +2918,9 @@ } }, "node_modules/@vue/devtools-shared": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.0.tgz", - "integrity": "sha512-ANh0nvp/oQXaz5PaSXL78I9X3v767kD0Cbit8u6mOd1oZmC+sj115/1CBH8BBBdRuLg8HCjpE2444a6cbXjKTA==", + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.1.tgz", + "integrity": "sha512-SdIif2YoOWo8/s8c1+NU67jcx8qoSjM9caetQnjl3++Kufo0qa5JRZe95iV6vvupQzVGGo3ACY0LTyAsMfGeCg==", "dev": true, "license": "MIT", "dependencies": { @@ -2983,15 +2983,15 @@ "license": "MIT" }, "node_modules/@vueuse/core": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.1.0.tgz", - "integrity": "sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.2.0.tgz", + "integrity": "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA==", "dev": true, "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "11.1.0", - "@vueuse/shared": "11.1.0", + "@vueuse/metadata": "11.2.0", + "@vueuse/shared": "11.2.0", "vue-demi": ">=0.14.10" }, "funding": { @@ -3026,14 +3026,14 @@ } }, "node_modules/@vueuse/integrations": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-11.1.0.tgz", - "integrity": "sha512-O2ZgrAGPy0qAjpoI2YR3egNgyEqwG85fxfwmA9BshRIGjV4G6yu6CfOPpMHAOoCD+UfsIl7Vb1bXJ6ifrHYDDA==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-11.2.0.tgz", + "integrity": "sha512-zGXz3dsxNHKwiD9jPMvR3DAxQEOV6VWIEYTGVSB9PNpk4pTWR+pXrHz9gvXWcP2sTk3W2oqqS6KwWDdntUvNVA==", "dev": true, "license": "MIT", "dependencies": { - "@vueuse/core": "11.1.0", - "@vueuse/shared": "11.1.0", + "@vueuse/core": "11.2.0", + "@vueuse/shared": "11.2.0", "vue-demi": ">=0.14.10" }, "funding": { @@ -3120,9 +3120,9 @@ } }, "node_modules/@vueuse/metadata": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.1.0.tgz", - "integrity": "sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.2.0.tgz", + "integrity": "sha512-L0ZmtRmNx+ZW95DmrgD6vn484gSpVeRbgpWevFKXwqqQxW9hnSi2Ppuh2BzMjnbv4aJRiIw8tQatXT9uOB23dQ==", "dev": true, "license": "MIT", "funding": { @@ -3130,9 +3130,9 @@ } }, "node_modules/@vueuse/shared": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.1.0.tgz", - "integrity": "sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.2.0.tgz", + "integrity": "sha512-VxFjie0EanOudYSgMErxXfq6fo8vhr5ICI+BuE3I9FnX7ePllEsVrRQ7O6Q1TLgApeLuPKcHQxAXpP+KnlrJsg==", "dev": true, "license": "MIT", "dependencies": { @@ -3431,24 +3431,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ansi-styles/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ansi-styles/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -4222,18 +4204,21 @@ } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/color-string": { @@ -4246,6 +4231,21 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -7950,6 +7950,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/minisearch": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.0.tgz", @@ -8746,6 +8755,31 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", + "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -11151,9 +11185,9 @@ } }, "node_modules/vitepress": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.4.2.tgz", - "integrity": "sha512-10v92Lqx0N4r7YC3cQLBvu+gRS2rHviE7vgdKiwlupUGfSWkyiQDqYccxM5iPStDGSi1Brnec1lf+lmhaQcZXw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.4.3.tgz", + "integrity": "sha512-956c2K2Mr0ubY9bTc2lCJD3g0mgo0mARB1iJC/BqUt4s0AM8Wl60wSU4zbFnzV7X2miFK1XJDKzGZnuEN90umw==", "dev": true, "license": "MIT", "dependencies": { @@ -11698,10 +11732,10 @@ }, "packages/cli": { "name": "@bluecadet/launchpad-cli", - "version": "1.0.0", + "version": "2.0.0-next.0", "license": "ISC", "dependencies": { - "@bluecadet/launchpad-utils": "~1.5.0", + "@bluecadet/launchpad-utils": "~2.0.0-next.0", "auto-bind": "^5.0.1", "chalk": "^5.0.0", "dotenv": "^16.4.5", @@ -11719,15 +11753,13 @@ "npm": ">=8.5.1" }, "optionalDependencies": { - "@bluecadet/launchpad-content": "~1.14.0", - "@bluecadet/launchpad-monitor": "~1.8.0", - "@bluecadet/launchpad-scaffold": "~1.8.0" + "@bluecadet/launchpad-content": "~2.0.0-next.0", + "@bluecadet/launchpad-monitor": "~2.0.0-next.0", + "@bluecadet/launchpad-scaffold": "~2.0.0-next.0" } }, "packages/cli/node_modules/dotenv": { "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -11738,10 +11770,10 @@ }, "packages/content": { "name": "@bluecadet/launchpad-content", - "version": "1.14.1", + "version": "2.0.0-next.0", "license": "ISC", "dependencies": { - "@bluecadet/launchpad-utils": "~1.5.0", + "@bluecadet/launchpad-utils": "~2.0.0-next.0", "@portabletext/to-html": "2.0.0", "@sanity/block-content-to-markdown": "^0.0.5", "chalk": "^5.0.0", @@ -11751,6 +11783,7 @@ "markdown-it": "^12.2.0", "neverthrow": "^8.0.0", "p-queue": "^7.1.0", + "path-scurry": "^2.0.0", "qs": "^6.11.1", "sanitize-html": "^2.5.1" }, @@ -11767,6 +11800,9 @@ "vitest": "^2.1.3" }, "optionalDependencies": { + "@bluecadet/launchpad-content": "~2.0.0-next.0", + "@bluecadet/launchpad-monitor": "~2.0.0-next.0", + "@bluecadet/launchpad-scaffold": "~2.0.0-next.0", "@sanity/client": "^6.4.9", "airtable": "^0.11.1", "contentful": "^9.0.0" @@ -11774,8 +11810,6 @@ }, "packages/content/node_modules/@types/markdown-it": { "version": "12.2.3", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", - "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11785,8 +11819,6 @@ }, "packages/content/node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "packages/content/node_modules/brace-expansion": { @@ -11798,8 +11830,6 @@ }, "packages/content/node_modules/entities": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -11828,8 +11858,6 @@ }, "packages/content/node_modules/linkify-it": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", "license": "MIT", "dependencies": { "uc.micro": "^1.0.1" @@ -11837,8 +11865,6 @@ }, "packages/content/node_modules/markdown-it": { "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", "license": "MIT", "dependencies": { "argparse": "^2.0.1", @@ -11853,8 +11879,6 @@ }, "packages/content/node_modules/mdurl": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "license": "MIT" }, "packages/content/node_modules/minimatch": { @@ -11872,16 +11896,14 @@ }, "packages/content/node_modules/uc.micro": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "license": "MIT" }, "packages/dashboard": { "name": "@bluecadet/launchpad-dashboard", - "version": "1.5.0", + "version": "2.0.0-next.0", "license": "ISC", "dependencies": { - "@bluecadet/launchpad-utils": "~1.5.0" + "@bluecadet/launchpad-utils": "~2.0.0-next.0" }, "bin": { "launchpad-dashboard": "index.js" @@ -11892,15 +11914,15 @@ }, "packages/launchpad": { "name": "@bluecadet/launchpad", - "version": "1.5.2", + "version": "2.0.0-next.0", "license": "ISC", "dependencies": { - "@bluecadet/launchpad-cli": "~1.0.0", - "@bluecadet/launchpad-content": "~1.14.0", - "@bluecadet/launchpad-dashboard": "~1.5.0", - "@bluecadet/launchpad-monitor": "~1.8.0", - "@bluecadet/launchpad-scaffold": "~1.8.0", - "@bluecadet/launchpad-utils": "~1.5.0", + "@bluecadet/launchpad-cli": "~2.0.0-next.0", + "@bluecadet/launchpad-content": "~2.0.0-next.0", + "@bluecadet/launchpad-dashboard": "~2.0.0-next.0", + "@bluecadet/launchpad-monitor": "~2.0.0-next.0", + "@bluecadet/launchpad-scaffold": "~2.0.0-next.0", + "@bluecadet/launchpad-utils": "~2.0.0-next.0", "auto-bind": "^5.0.1", "chalk": "^5.0.0" }, @@ -11914,10 +11936,10 @@ }, "packages/monitor": { "name": "@bluecadet/launchpad-monitor", - "version": "1.8.0", + "version": "2.0.0-next.0", "license": "ISC", "dependencies": { - "@bluecadet/launchpad-utils": "~1.5.0", + "@bluecadet/launchpad-utils": "~2.0.0-next.0", "auto-bind": "^5.0.1", "axon": "^2.0.3", "chalk": "^5.0.0", @@ -11938,10 +11960,10 @@ }, "packages/scaffold": { "name": "@bluecadet/launchpad-scaffold", - "version": "1.8.1", + "version": "2.0.0-next.0", "license": "ISC", "dependencies": { - "@bluecadet/launchpad-utils": "~1.5.0", + "@bluecadet/launchpad-utils": "~2.0.0-next.0", "sudo-prompt": "^9.2.1" }, "bin": { @@ -11960,7 +11982,7 @@ }, "devDependencies": { "@bluecadet/launchpad-tsconfig": "0.1.0", - "@bluecadet/launchpad-utils": "*" + "@bluecadet/launchpad-utils": "2.0.0-next.0" } }, "packages/tsconfig": { @@ -11969,7 +11991,7 @@ }, "packages/utils": { "name": "@bluecadet/launchpad-utils", - "version": "1.5.2", + "version": "2.0.0-next.0", "license": "ISC", "dependencies": { "@sindresorhus/slugify": "^2.1.0", @@ -11991,8 +12013,6 @@ }, "packages/utils/node_modules/strip-json-comments": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-4.0.0.tgz", - "integrity": "sha512-LzWcbfMbAsEDTRmhjWIioe8GcDRl0fa35YMXFoJKDdiD/quGFmjJjdgPjFJJNwCMaLyQqFIDqCdHD2V4HfLgYA==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" diff --git a/packages/cli/package.json b/packages/cli/package.json index 52420ecc..6793700b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -17,6 +17,7 @@ "bin": { "launchpad": "./lib/cli.js" }, + "main": "./lib/index.js", "files": [ "types", "index.js", @@ -101,4 +102,4 @@ "tools", "windows-desktop" ] -} +} \ No newline at end of file diff --git a/packages/content/lib/plugins/index.js b/packages/content/lib/plugins/index.js index 36bf041d..b5d314d6 100644 --- a/packages/content/lib/plugins/index.js +++ b/packages/content/lib/plugins/index.js @@ -2,3 +2,4 @@ export { default as mdToHtml } from './md-to-html.js'; export { default as sanityToHtml } from './sanity-to-html.js'; export { default as sanityToPlain } from './sanity-to-plain.js'; export { default as sanityToMd } from './sanity-to-markdown.js'; +export { default as mediaDownloader } from './media-downloader.js'; diff --git a/packages/content/package.json b/packages/content/package.json index 78cb5dfc..d5472ff4 100644 --- a/packages/content/package.json +++ b/packages/content/package.json @@ -41,10 +41,14 @@ "markdown-it": "^12.2.0", "neverthrow": "^8.0.0", "p-queue": "^7.1.0", + "path-scurry": "^2.0.0", "qs": "^6.11.1", "sanitize-html": "^2.5.1" }, "optionalDependencies": { + "@bluecadet/launchpad-content": "~2.0.0-next.0", + "@bluecadet/launchpad-monitor": "~2.0.0-next.0", + "@bluecadet/launchpad-scaffold": "~2.0.0-next.0", "@sanity/client": "^6.4.9", "airtable": "^0.11.1", "contentful": "^9.0.0" @@ -61,4 +65,4 @@ "msw": "^2.5.1", "vitest": "^2.1.3" } -} \ No newline at end of file +} From 166ce90fbe30908ae1c832e0ddb6d3993e9aa7e3 Mon Sep 17 00:00:00 2001 From: Clay Tercek <30105080+claytercek@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:02:52 -0400 Subject: [PATCH 4/4] super verbose logging --- packages/cli/lib/commands/content.js | 24 ++-- packages/cli/lib/commands/monitor.js | 39 ++++--- packages/cli/lib/commands/scaffold.js | 4 +- packages/cli/lib/commands/start.js | 51 ++++---- packages/cli/lib/utils/command-utils.js | 109 ++++++++++++++++++ packages/cli/lib/utils/load-config-and-env.js | 48 -------- packages/content/lib/launchpad-content.js | 17 ++- packages/monitor/lib/launchpad-monitor.js | 8 +- packages/scaffold/index.js | 4 - packages/utils/lib/log-manager.js | 3 +- 10 files changed, 186 insertions(+), 121 deletions(-) create mode 100644 packages/cli/lib/utils/command-utils.js delete mode 100644 packages/cli/lib/utils/load-config-and-env.js diff --git a/packages/cli/lib/commands/content.js b/packages/cli/lib/commands/content.js index 2660882c..24f5a9f4 100644 --- a/packages/cli/lib/commands/content.js +++ b/packages/cli/lib/commands/content.js @@ -1,21 +1,23 @@ -import { ResultAsync } from 'neverthrow'; +import { err, ok, ResultAsync } from 'neverthrow'; import { ImportError } from '../errors.js'; -import { loadConfigAndEnv } from '../utils/load-config-and-env.js'; +import { handleFatalError, initializeLogger, loadConfigAndEnv } from '../utils/command-utils.js'; /** * @param {import("../cli.js").LaunchpadArgv} argv */ export function content(argv) { - return loadConfigAndEnv(argv).andThen(config => { - return importLaunchpadContent().map(({ LaunchpadContent }) => { - const contentInstance = new LaunchpadContent(config.content); - return contentInstance.download(); + return loadConfigAndEnv(argv) + .andThen(initializeLogger) + .andThen(({ config, rootLogger }) => { + return importLaunchpadContent().andThen(({ LaunchpadContent }) => { + if (!config.content) { + return err(new Error('No content config found in your config file.')); + } + + const contentInstance = new LaunchpadContent(config.content, rootLogger); + return contentInstance.download(); + }).orElse(error => handleFatalError(error, rootLogger)); }); - }).mapErr(error => { - console.error('Content failed to download.'); - console.error(error.message); - process.exit(1); - }); } export function importLaunchpadContent() { diff --git a/packages/cli/lib/commands/monitor.js b/packages/cli/lib/commands/monitor.js index e3188c88..34e93008 100644 --- a/packages/cli/lib/commands/monitor.js +++ b/packages/cli/lib/commands/monitor.js @@ -1,27 +1,30 @@ -import { ResultAsync } from 'neverthrow'; +import { err, ok, ResultAsync } from 'neverthrow'; import { ImportError, MonitorError } from '../errors.js'; -import { loadConfigAndEnv } from '../utils/load-config-and-env.js'; +import { handleFatalError, initializeLogger, loadConfigAndEnv } from '../utils/command-utils.js'; /** * @param {import("../cli.js").LaunchpadArgv} argv */ export function monitor(argv) { - return loadConfigAndEnv(argv).andThen(config => { - return importLaunchpadMonitor() - .map(({ LaunchpadMonitor }) => { - return new LaunchpadMonitor(config.monitor); - }) - .andThrough((monitorInstance) => { - return ResultAsync.fromPromise(monitorInstance.connect(), () => new MonitorError('Failed to connect to monitor')); - }) - .andThrough((monitorInstance) => { - return ResultAsync.fromPromise(monitorInstance.start(), () => new MonitorError('Failed to start monitor')); - }); - }).mapErr(error => { - console.error('Monitor failed to start.'); - console.error(error.message); - process.exit(1); - }); + return loadConfigAndEnv(argv) + .andThen(initializeLogger) + .andThen(({ config, rootLogger }) => { + return importLaunchpadMonitor() + .andThen(({ LaunchpadMonitor }) => { + if (!config.monitor) { + return err(new Error('No monitor config found in your config file.')); + } + + const monitorInstance = new LaunchpadMonitor(config.monitor, rootLogger); + return ok(monitorInstance); + }) + .andThrough((monitorInstance) => { + return ResultAsync.fromPromise(monitorInstance.connect(), () => new MonitorError('Failed to connect to monitor')); + }) + .andThrough((monitorInstance) => { + return ResultAsync.fromPromise(monitorInstance.start(), () => new MonitorError('Failed to start monitor')); + }).orElse(error => handleFatalError(error, rootLogger)); + }); } export function importLaunchpadMonitor() { diff --git a/packages/cli/lib/commands/scaffold.js b/packages/cli/lib/commands/scaffold.js index f9a3eb37..998a19d3 100644 --- a/packages/cli/lib/commands/scaffold.js +++ b/packages/cli/lib/commands/scaffold.js @@ -1,8 +1,10 @@ import { launchScaffold } from '@bluecadet/launchpad-scaffold'; +import { LogManager } from '@bluecadet/launchpad-utils'; /** * @param {import("../cli.js").LaunchpadArgv} argv */ export async function scaffold(argv) { - await launchScaffold(); + const rootLogger = LogManager.configureRootLogger(); + await launchScaffold(rootLogger); } diff --git a/packages/cli/lib/commands/start.js b/packages/cli/lib/commands/start.js index eceb08e8..906fac61 100644 --- a/packages/cli/lib/commands/start.js +++ b/packages/cli/lib/commands/start.js @@ -1,30 +1,39 @@ -import { loadConfigAndEnv } from '../utils/load-config-and-env.js'; +import { handleFatalError, initializeLogger, loadConfigAndEnv } from '../utils/command-utils.js'; import { importLaunchpadMonitor } from './monitor.js'; import { importLaunchpadContent } from './content.js'; -import { ResultAsync } from 'neverthrow'; +import { err, ok, ResultAsync } from 'neverthrow'; import { MonitorError } from '../errors.js'; /** * @param {import("../cli.js").LaunchpadArgv} argv */ export async function start(argv) { - return loadConfigAndEnv(argv).andThen(config => { - return importLaunchpadContent().map(({ LaunchpadContent }) => { - const contentInstance = new LaunchpadContent(config.content); - return contentInstance.start(); - }).andThen(() => importLaunchpadMonitor()) - .map(({ LaunchpadMonitor }) => { - return new LaunchpadMonitor(config.monitor); - }) - .andThrough((monitorInstance) => { - return ResultAsync.fromPromise(monitorInstance.connect(), () => new MonitorError('Failed to connect to monitor')); - }) - .andThrough((monitorInstance) => { - return ResultAsync.fromPromise(monitorInstance.start(), () => new MonitorError('Failed to start monitor')); - }); - }).mapErr(error => { - console.error('Launchpad failed to start.'); - console.error(error.message); - process.exit(1); - }); + return loadConfigAndEnv(argv) + .andThen(initializeLogger) + .andThen(({ config, rootLogger }) => { + return importLaunchpadContent() + .andThen(({ LaunchpadContent }) => { + if (!config.content) { + return err(new Error('No content config found in your config file.')); + } + + const contentInstance = new LaunchpadContent(config.content, rootLogger); + return contentInstance.start(); + }) + .andThen(() => importLaunchpadMonitor()) + .andThen(({ LaunchpadMonitor }) => { + if (!config.monitor) { + return err(new Error('No monitor config found in your config file.')); + } + + const monitorInstance = new LaunchpadMonitor(config.monitor, rootLogger); + return ok(monitorInstance); + }) + .andThrough((monitorInstance) => { + return ResultAsync.fromPromise(monitorInstance.connect(), () => new MonitorError('Failed to connect to monitor')); + }) + .andThrough((monitorInstance) => { + return ResultAsync.fromPromise(monitorInstance.start(), () => new MonitorError('Failed to start monitor')); + }).orElse(error => handleFatalError(error, rootLogger)); + }); } diff --git a/packages/cli/lib/utils/command-utils.js b/packages/cli/lib/utils/command-utils.js new file mode 100644 index 00000000..6b2fbedb --- /dev/null +++ b/packages/cli/lib/utils/command-utils.js @@ -0,0 +1,109 @@ +import { err, errAsync, ok, ResultAsync } from 'neverthrow'; +import { findConfig, loadConfigFromFile } from './config.js'; +import { ConfigError } from '../errors.js'; +import path from 'path'; +import { resolveEnv } from './env.js'; +import { resolveLaunchpadOptions } from '../launchpad-options.js'; +import chalk from 'chalk'; +import { LogManager } from '@bluecadet/launchpad-utils'; + +/** + * @param {import("../cli.js").LaunchpadArgv} argv + * @returns {import('neverthrow').ResultAsync} + */ +export function loadConfigAndEnv(argv) { + const configPath = argv.config ?? findConfig(); + + if (!configPath) { + return errAsync(new ConfigError('No config file found.')); + } + + const configDir = path.dirname(configPath); + + if (argv.env) { + // if env arg is passed, resolve paths relative to the CWD + const rootDir = process.env.INIT_CWD ?? ''; + resolveEnv( + argv.env.map(p => path.resolve(rootDir, p.toString())) + ); + } else if (argv.envCascade) { + // if env-cascade arg is passed, resolve paths relative to the config file + + // Load order: .env < .env.local < .env.[override] < .env.[override].local + resolveEnv([ + path.resolve(configDir, '.env'), + path.resolve(configDir, '.env.local'), + path.resolve(configDir, `.env.${argv.envCascade}`), + path.resolve(configDir, `.env.${argv.envCascade}.local`) + ]); + } else { + // default to loading .env and .env.local in the config dir + resolveEnv([ + path.resolve(configDir, '.env'), + path.resolve(configDir, '.env.local') + ]); + } + + return ResultAsync.fromPromise(loadConfigFromFile(configPath), (e) => new ConfigError(`Failed to load config file at path: ${chalk.white(configPath)}`)) + .map(config => resolveLaunchpadOptions(config)); +} + +/** + * + * @param {import('../launchpad-options.js').LaunchpadOptions} config + */ +export function initializeLogger(config) { + const rootLogger = LogManager.configureRootLogger(config.logging); + + return ok({ config, rootLogger }); +} + +/** + * + * @param {Error} error + * @param {import('@bluecadet/launchpad-utils').Logger} rootLogger + * @returns {never} + */ +export function handleFatalError(error, rootLogger) { + rootLogger.error('Content failed to download.'); + logFullErrorChain(rootLogger, error); + process.exit(1); +} + +/** + * @param {import('@bluecadet/launchpad-utils').Logger} logger + * @param {Error} error + */ +export function logFullErrorChain(logger, error) { + /** @type {Error | undefined} */ + let currentError = error; + while (currentError) { + logger.error(`${chalk.red('┌─')} ${chalk.red.bold(currentError.name)}: ${chalk.red(currentError.message)}`); + const callstack = currentError.stack; + // logger.error(`${chalk.red(callstack ? '│' : '└')} `); + if (callstack) { + const lines = callstack.split('\n').slice(1); + // log up to 3 lines of the callstack + let loggedLines = 0; + for (const line of lines) { + const isLastLine = loggedLines === lines.length - 1 || loggedLines > 2; + logger.error(`${chalk.red('│')} ${chalk.red.dim((isLastLine && lines.length > 3) ? '...' : line.trim())}`); + if (isLastLine) { + logger.error(`${chalk.red('└──────────────────')}`); + } + loggedLines++; + + if (loggedLines > 3) { + break; + } + } + } + if (currentError.cause && currentError.cause instanceof Error) { + currentError = currentError.cause; + logger.error(` ${chalk.red.dim('│')} ${chalk.red.dim('Caused by:')}`); + logger.error(` ${chalk.red.dim('│')}`); + } else { + currentError = undefined; + } + } +} diff --git a/packages/cli/lib/utils/load-config-and-env.js b/packages/cli/lib/utils/load-config-and-env.js deleted file mode 100644 index 87f2a472..00000000 --- a/packages/cli/lib/utils/load-config-and-env.js +++ /dev/null @@ -1,48 +0,0 @@ -import { err, errAsync, ok, ResultAsync } from 'neverthrow'; -import { findConfig, loadConfigFromFile } from './config.js'; -import { ConfigError } from '../errors.js'; -import path from 'path'; -import { resolveEnv } from './env.js'; -import { resolveLaunchpadOptions } from '../launchpad-options.js'; -import chalk from 'chalk'; - -/** - * @param {import("../cli.js").LaunchpadArgv} argv - * @returns {import('neverthrow').ResultAsync} - */ -export function loadConfigAndEnv(argv) { - const configPath = argv.config ?? findConfig(); - - if (!configPath) { - return errAsync(new ConfigError('No config file found.')); - } - - const configDir = path.dirname(configPath); - - if (argv.env) { - // if env arg is passed, resolve paths relative to the CWD - const rootDir = process.env.INIT_CWD ?? ''; - resolveEnv( - argv.env.map(p => path.resolve(rootDir, p.toString())) - ); - } else if (argv.envCascade) { - // if env-cascade arg is passed, resolve paths relative to the config file - - // Load order: .env < .env.local < .env.[override] < .env.[override].local - resolveEnv([ - path.resolve(configDir, '.env'), - path.resolve(configDir, '.env.local'), - path.resolve(configDir, `.env.${argv.envCascade}`), - path.resolve(configDir, `.env.${argv.envCascade}.local`) - ]); - } else { - // default to loading .env and .env.local in the config dir - resolveEnv([ - path.resolve(configDir, '.env'), - path.resolve(configDir, '.env.local') - ]); - } - - return ResultAsync.fromPromise(loadConfigFromFile(configPath), (e) => new ConfigError(`Failed to load config file at path: ${chalk.white(configPath)}`)) - .map(config => resolveLaunchpadOptions(config)); -} diff --git a/packages/content/lib/launchpad-content.js b/packages/content/lib/launchpad-content.js index 19b38656..4ba9a3eb 100644 --- a/packages/content/lib/launchpad-content.js +++ b/packages/content/lib/launchpad-content.js @@ -30,16 +30,12 @@ export class LaunchpadContent { _dataStore; /** - * @param {import('./content-options.js').ContentOptions} [config] - * @param {import('@bluecadet/launchpad-utils').Logger} [parentLogger] + * @param {import('./content-options.js').ContentOptions} config + * @param {import('@bluecadet/launchpad-utils').Logger} parentLogger */ constructor(config, parentLogger) { this._config = resolveContentOptions(config); - if (!parentLogger) { - LogManager.configureRootLogger(); - } - this._logger = LogManager.getLogger('content', parentLogger); this._dataStore = new DataStore(); @@ -93,6 +89,7 @@ export class LaunchpadContent { this._logger.error('Error in content fetch process:', e); this._logger.info('Restoring from backup...'); return this.restore(sources).andThen(() => { + console.log('HERE'); return err(new ContentError('Failed to download content. Restored from backup.', { cause: e })); }); }) @@ -108,7 +105,7 @@ export class LaunchpadContent { * Alias for start(source) * @param {import('./content-options.js').ConfigContentSource[]?} rawSources */ - async download(rawSources = null) { + download(rawSources = null) { return this.start(rawSources); } @@ -197,17 +194,17 @@ export class LaunchpadContent { } return ok(undefined); }).andTee(() => { - this._logger.info(`Restoring ${source} from backup`); + this._logger.info(`Restoring ${chalk.white(source.id)} from backup`); }).andThen(() => { return FileUtils.copy(backupPath, downloadPath, { preserveTimestamps: true }); }).andThen(() => { if (removeBackups) { - this._logger.debug(`Removing backup for ${source}`); + this._logger.debug(`Removing backup for ${chalk.white(source.id)}`); return FileUtils.remove(backupPath); } return okAsync(undefined); - }).mapErr(e => new ContentError(`Failed to restore source ${source.id}: ${e}`)); + }).mapErr(e => new ContentError(`Failed to restore source ${chalk.white(source.id)}: ${e}`)); })).map(() => undefined); // return void instead of void[] } diff --git a/packages/monitor/lib/launchpad-monitor.js b/packages/monitor/lib/launchpad-monitor.js index 2972c618..c70b36a2 100644 --- a/packages/monitor/lib/launchpad-monitor.js +++ b/packages/monitor/lib/launchpad-monitor.js @@ -72,16 +72,12 @@ export class LaunchpadMonitor { /** * - * @param {import('./monitor-options.js').MonitorOptions} [config] - * @param {import('@bluecadet/launchpad-utils').Logger} [parentLogger] + * @param {import('./monitor-options.js').MonitorOptions} config + * @param {import('@bluecadet/launchpad-utils').Logger} parentLogger */ constructor(config, parentLogger) { autoBind(this); - if (!parentLogger) { - LogManager.configureRootLogger(); - } - this._logger = LogManager.getLogger('monitor', parentLogger); this._config = resolveMonitorConfig(config); diff --git a/packages/scaffold/index.js b/packages/scaffold/index.js index a07cb423..7ef23cdb 100755 --- a/packages/scaffold/index.js +++ b/packages/scaffold/index.js @@ -8,10 +8,6 @@ import * as path from 'path'; * @param {import('@bluecadet/launchpad-utils').Logger} [parentLogger] */ export function launchScaffold(parentLogger) { - if (!parentLogger) { - LogManager.configureRootLogger(); - } - const logger = LogManager.getLogger('scaffold', parentLogger); if (process.platform !== 'win32') { diff --git a/packages/utils/lib/log-manager.js b/packages/utils/lib/log-manager.js index d2b79601..306ad446 100644 --- a/packages/utils/lib/log-manager.js +++ b/packages/utils/lib/log-manager.js @@ -8,7 +8,6 @@ import 'winston-daily-rotate-file'; import slugify from '@sindresorhus/slugify'; import moment from 'moment'; import chalk from 'chalk'; -import { text } from 'stream/consumers'; /** * @callback createChildLogger @@ -160,7 +159,7 @@ export class LogManager { this._instance._rootLogger.warn('Root logger already configured. Ignoring.'); } - return this._instance; + return this._instance._rootLogger; } /**