diff --git a/packages/ui/atomic/create-atomic-component-project/index.js b/packages/ui/atomic/create-atomic-component-project/index.js index cccdcea8b5..8fb17d516b 100755 --- a/packages/ui/atomic/create-atomic-component-project/index.js +++ b/packages/ui/atomic/create-atomic-component-project/index.js @@ -48,7 +48,7 @@ const main = () => { } else { handleErrors( new InvalidProjectDirectory( - 'Current working directory is either not empty or not an npm project (no package.json found). Please try again in an empty directory.' + 'Current working directory is either not empty or not an npm project (no package.json found). Please try again in a valid project (see [`atomic:cmp` documentation](https://docs.coveo.com/en/cli/#coveo-atomiccmp-name)).' ) ); } diff --git a/packages/ui/atomic/create-atomic-component-project/tests/test.spec.ts b/packages/ui/atomic/create-atomic-component-project/tests/test.spec.ts index b2329e74e2..47f671469d 100644 --- a/packages/ui/atomic/create-atomic-component-project/tests/test.spec.ts +++ b/packages/ui/atomic/create-atomic-component-project/tests/test.spec.ts @@ -1,6 +1,6 @@ import {ChildProcess} from 'node:child_process'; import {join} from 'node:path'; -import {mkdirSync} from 'node:fs'; +import {mkdirSync, writeFileSync, rmSync} from 'node:fs'; import {lookup} from 'node:dns/promises'; import {npmSync, npmAsync} from '@coveo/do-npm'; import {getStdoutStderrBuffersFromProcess} from '@coveo/process-helpers'; @@ -135,4 +135,74 @@ describe(PACKAGE_NAME, () => { expect(interceptedRequests.some(isSearchRequestOrResponse)).toBe(true); }); }); + + describe('ensureDirectoryValidity', () => { + const assertAggregateErrorsNotFired = ( + tag: string, + expectedErrorMessages: string + ) => { + const {stderr} = npmSync( + ['init', PACKAGE_NAME.replace('/create-', '/'), '--', tag], + { + env: { + ...process.env, + npm_config_registry: verdaccioUrl, + npm_config_cache: dirSync({unsafeCleanup: true}).name, + }, + cwd: testDirectory, + } + ); + + expect(stderr.toString()).not.toEqual( + expect.stringContaining(expectedErrorMessages) + ); + }; + + beforeEach(() => { + testDirectory = join(tempDirectory.name, 'dir-validity'); + mkdirSync(testDirectory, {recursive: true}); + }); + + afterEach(() => { + rmSync(testDirectory, {recursive: true}); + }); + + it('should not error when in empty directory', () => { + assertAggregateErrorsNotFired( + 'component-name', + 'Invalid project directory' + ); + }); + + it('should not error when in non-empty directory with package.json', () => { + writeFileSync(join(testDirectory, 'package.json'), '{}'); + assertAggregateErrorsNotFired( + 'component-name', + 'Invalid project directory' + ); + }); + + it('should error when in non-empty directory without package.json', () => { + writeFileSync( + join(testDirectory, '__init__.py'), + "# Wait, this isn't a Headless project :O" + ); + + const {stderr} = npmSync( + ['init', PACKAGE_NAME.replace('/create-', '/'), '--', 'component-name'], + { + env: { + ...process.env, + npm_config_registry: verdaccioUrl, + npm_config_cache: dirSync({unsafeCleanup: true}).name, + }, + cwd: testDirectory, + } + ); + + expect(stderr.toString()).toEqual( + expect.stringContaining('Invalid project directory') + ); + }); + }); }); diff --git a/packages/ui/atomic/create-atomic-component/index.js b/packages/ui/atomic/create-atomic-component/index.js index df89658d97..1a8a28c665 100755 --- a/packages/ui/atomic/create-atomic-component/index.js +++ b/packages/ui/atomic/create-atomic-component/index.js @@ -7,6 +7,7 @@ import { unlinkSync, writeFileSync, readFileSync, + readdirSync, } from 'node:fs'; import {cwd} from 'node:process'; import {fileURLToPath} from 'node:url'; @@ -27,6 +28,20 @@ class SerializableAggregateError extends AggregateError { }; } } +class InvalidProjectDirectory extends Error { + name = 'Invalid project directory'; + constructor(message) { + super(message); + } + + toJSON() { + return { + name: this.name, + message: this.message, + stack: this.stack, + }; + } +} const handleErrors = (error) => { if (process.channel) { @@ -63,6 +78,7 @@ const camelize = (str) => str .replace(/-(.)/g, (_, group) => group.toUpperCase()) .replace(/^./, (match) => match.toUpperCase()); + const transform = (transformers) => { for (const {srcPath, destPath, transform} of transformers) { if (!srcPath) { @@ -120,11 +136,27 @@ const ensureComponentValidity = (tag) => { } }; +const ensureDirectoryValidity = () => { + const cwdFiles = readdirSync(cwd(), {withFileTypes: true}); + const hasPackageInCwd = cwdFiles.some( + (dirent) => dirent.name === 'package.json' + ); + if (cwdFiles.length > 0 && !hasPackageInCwd) { + handleErrors( + new InvalidProjectDirectory( + 'Current working directory is either not empty or not an npm project (no package.json found). Please try again in a valid project (see [`atomic:cmp` documentation](https://docs.coveo.com/en/cli/#coveo-atomiccmp-name)).' + ) + ); + } +}; + /***********************************/ const __dirname = dirname(fileURLToPath(import.meta.url)); const templateRelativeDir = 'template'; const templateDirPath = resolve(__dirname, templateRelativeDir); +ensureDirectoryValidity(); + cpSync(templateDirPath, cwd(), { recursive: true, }); diff --git a/packages/ui/atomic/create-atomic-component/tests/test.spec.ts b/packages/ui/atomic/create-atomic-component/tests/test.spec.ts index 34795a6e69..1e61527769 100644 --- a/packages/ui/atomic/create-atomic-component/tests/test.spec.ts +++ b/packages/ui/atomic/create-atomic-component/tests/test.spec.ts @@ -1,6 +1,6 @@ import {ChildProcess} from 'node:child_process'; import {join} from 'node:path'; -import {mkdirSync} from 'node:fs'; +import {mkdirSync, writeFileSync, rmSync} from 'node:fs'; import {npmSync} from '@coveo/do-npm'; import {startVerdaccio} from '@coveo/verdaccio-starter'; import {hashElement} from 'folder-hash'; @@ -18,6 +18,29 @@ describe(PACKAGE_NAME, () => { let npmCache: string; let verdaccioUrl: string; + const assertAllAggregateErrorsFired = ( + tag: string, + ...expectedErrorMessages: string[] + ) => { + const {stderr} = npmSync( + ['init', PACKAGE_NAME.replace('/create-', '/'), '--', tag], + { + env: { + ...process.env, + npm_config_registry: verdaccioUrl, + npm_config_cache: npmCache, + }, + cwd: testDirectory, + } + ); + + expectedErrorMessages.forEach((expectedMessage) => { + expect(stderr.toString()).toEqual( + expect.stringContaining(expectedMessage) + ); + }); + }; + beforeAll(async () => { ({verdaccioUrl, verdaccioProcess} = await startVerdaccio([ PACKAGE_NAME, @@ -52,29 +75,6 @@ describe(PACKAGE_NAME, () => { const leadingDashTag = '-dash'; const messedUpTag = '-My#--Component@-'; - const assertAllAggregateErrorsFired = ( - tag: string, - ...expectedErrorMessages: string[] - ) => { - const {stderr} = npmSync( - ['init', PACKAGE_NAME.replace('/create-', '/'), '--', tag], - { - env: { - ...process.env, - npm_config_registry: verdaccioUrl, - npm_config_cache: npmCache, - }, - cwd: testDirectory, - } - ); - - expectedErrorMessages.forEach((expectedMessage) => { - expect(stderr.toString()).toEqual( - expect.stringContaining(expectedMessage) - ); - }); - }; - it.each([ leadingSpaceTag, trailingSpaceTag, @@ -148,6 +148,65 @@ describe(PACKAGE_NAME, () => { }); }); + describe('ensureDirectoryValidity', () => { + const assertAggregateErrorsNotFired = ( + tag: string, + expectedErrorMessages: string + ) => { + const {stderr} = npmSync( + ['init', PACKAGE_NAME.replace('/create-', '/'), '--', tag], + { + env: { + ...process.env, + npm_config_registry: verdaccioUrl, + npm_config_cache: npmCache, + }, + cwd: testDirectory, + } + ); + + expect(stderr.toString()).not.toEqual( + expect.stringContaining(expectedErrorMessages) + ); + }; + + beforeEach(() => { + testDirectory = join(tempDirectory.name, 'dir-validity'); + mkdirSync(testDirectory, {recursive: true}); + }); + + afterEach(() => { + rmSync(testDirectory, {recursive: true}); + }); + + it('should not error when in empty directory', () => { + assertAggregateErrorsNotFired( + 'component-name', + 'Invalid project directory' + ); + }); + + it('should not error when in non-empty directory with package.json', () => { + writeFileSync(join(testDirectory, 'package.json'), '{}'); + assertAggregateErrorsNotFired( + 'component-name', + 'Invalid project directory' + ); + }); + + it('should error when in non-empty directory without package.json', () => { + writeFileSync( + join(testDirectory, '__init__.py'), + "# Wait, this isn't a Headless project :O" + ); + + assertAllAggregateErrorsFired( + 'component-name', + 'Invalid project directory' + ); + }); + }); + describe.each([ { describeName: 'when called without any args', diff --git a/packages/ui/atomic/create-atomic-result-component/index.js b/packages/ui/atomic/create-atomic-result-component/index.js index 45a2cf7786..df0e99429a 100755 --- a/packages/ui/atomic/create-atomic-result-component/index.js +++ b/packages/ui/atomic/create-atomic-result-component/index.js @@ -7,6 +7,7 @@ import { unlinkSync, writeFileSync, readFileSync, + readdirSync, } from 'node:fs'; import {cwd} from 'node:process'; import {fileURLToPath} from 'node:url'; @@ -28,6 +29,21 @@ class SerializableAggregateError extends AggregateError { } } +class InvalidProjectDirectory extends Error { + name = 'Invalid project directory'; + constructor(message) { + super(message); + } + + toJSON() { + return { + name: this.name, + message: this.message, + stack: this.stack, + }; + } +} + const handleErrors = (error) => { if (process.channel) { process.send(error); @@ -120,11 +136,27 @@ const ensureComponentValidity = (tag) => { } }; +const ensureDirectoryValidity = () => { + const cwdFiles = readdirSync(cwd(), {withFileTypes: true}); + const hasPackageInCwd = cwdFiles.some( + (dirent) => dirent.name === 'package.json' + ); + if (cwdFiles.length > 0 && !hasPackageInCwd) { + handleErrors( + new InvalidProjectDirectory( + 'Current working directory is either not empty or not an npm project (no package.json found). Please try again in a valid project (see [`atomic:cmp` documentation](https://docs.coveo.com/en/cli/#coveo-atomiccmp-name)).' + ) + ); + } +}; + /***********************************/ const __dirname = dirname(fileURLToPath(import.meta.url)); const templateRelativeDir = 'template'; const templateDirPath = resolve(__dirname, templateRelativeDir); +ensureDirectoryValidity(); + cpSync(templateDirPath, cwd(), { recursive: true, }); diff --git a/packages/ui/atomic/create-atomic-result-component/tests/test.spec.ts b/packages/ui/atomic/create-atomic-result-component/tests/test.spec.ts index 7f8ce3bf97..b767e6a070 100644 --- a/packages/ui/atomic/create-atomic-result-component/tests/test.spec.ts +++ b/packages/ui/atomic/create-atomic-result-component/tests/test.spec.ts @@ -1,6 +1,6 @@ import {ChildProcess} from 'node:child_process'; import {join} from 'node:path'; -import {mkdirSync} from 'node:fs'; +import {mkdirSync, writeFileSync, rmSync} from 'node:fs'; import {npmSync} from '@coveo/do-npm'; import {startVerdaccio} from '@coveo/verdaccio-starter'; import {hashElement} from 'folder-hash'; @@ -18,6 +18,29 @@ describe(PACKAGE_NAME, () => { let npmConfigCache: string; let verdaccioUrl: string; + const assertAllAggregateErrorsFired = ( + tag: string, + ...expectedErrorMessages: string[] + ) => { + const {stderr} = npmSync( + ['init', PACKAGE_NAME.replace('/create-', '/'), '--', tag], + { + env: { + ...process.env, + npm_config_registry: verdaccioUrl, + npm_config_cache: npmConfigCache, + }, + cwd: testDirectory, + } + ); + + expectedErrorMessages.forEach((expectedMessage) => { + expect(stderr.toString()).toEqual( + expect.stringContaining(expectedMessage) + ); + }); + }; + beforeAll(async () => { ({verdaccioUrl, verdaccioProcess} = await startVerdaccio([ PACKAGE_NAME, @@ -52,29 +75,6 @@ describe(PACKAGE_NAME, () => { const leadingDashTag = '-dash'; const messedUpTag = '-My#--Component@-'; - const assertAllAggregateErrorsFired = ( - tag: string, - ...expectedErrorMessages: string[] - ) => { - const {stderr} = npmSync( - ['init', PACKAGE_NAME.replace('/create-', '/'), '--', tag], - { - env: { - ...process.env, - npm_config_registry: verdaccioUrl, - npm_config_cache: npmConfigCache, - }, - cwd: testDirectory, - } - ); - - expectedErrorMessages.forEach((expectedMessage) => { - expect(stderr.toString()).toEqual( - expect.stringContaining(expectedMessage) - ); - }); - }; - it.each([ leadingSpaceTag, trailingSpaceTag, @@ -148,6 +148,65 @@ describe(PACKAGE_NAME, () => { }); }); + describe('ensureDirectoryValidity', () => { + const assertAggregateErrorsNotFired = ( + tag: string, + expectedErrorMessages: string + ) => { + const {stderr} = npmSync( + ['init', PACKAGE_NAME.replace('/create-', '/'), '--', tag], + { + env: { + ...process.env, + npm_config_registry: verdaccioUrl, + npm_config_cache: npmConfigCache, + }, + cwd: testDirectory, + } + ); + + expect(stderr.toString()).not.toEqual( + expect.stringContaining(expectedErrorMessages) + ); + }; + + beforeEach(() => { + testDirectory = join(tempDirectory.name, 'dir-validity'); + mkdirSync(testDirectory, {recursive: true}); + }); + + afterEach(() => { + rmSync(testDirectory, {recursive: true}); + }); + + it('should not error when in empty directory', () => { + assertAggregateErrorsNotFired( + 'component-name', + 'Invalid project directory' + ); + }); + + it('should not error when in non-empty directory with package.json', () => { + writeFileSync(join(testDirectory, 'package.json'), '{}'); + assertAggregateErrorsNotFired( + 'component-name', + 'Invalid project directory' + ); + }); + + it('should error when in non-empty directory without package.json', () => { + writeFileSync( + join(testDirectory, '__init__.py'), + "# Wait, this isn't a Headless project :O" + ); + + assertAllAggregateErrorsFired( + 'component-name', + 'Invalid project directory' + ); + }); + }); + describe.each([ { describeName: 'when called without any args',