Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Options:

Commands:
list|ls Lists all of the components that are available for installation within your project based on the system and variant you selected
install|i [name] Install a component from within the current project's system and variant
install|i [name] [options] Install a component from within the current project's system and variant
help [command] display help for command

```
Expand Down Expand Up @@ -179,4 +179,14 @@ emulsify component install card
> Success! The card component has been added to your project.
```

If you attempt to install a component that already exists within your project, you the installation process will throw an error. However, if you wish to overwrite the existing component, pass the `--force` flag.

```bash
emulsify component install card
> Error: The component "card" already exists, and force was not passed (--force).

emulsify component install card --force
> Success! The card component has been added to your project.
```

That's pretty much it. Have fun, and please feel free to open issues if you discover a bug, or have an improvement to suggest!
3 changes: 3 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ jest.mock('simple-git', () => {
branch: jest.fn(),
checkout: jest.fn(),
fetch: jest.fn(),
pull: jest.fn(),
};

return jest.fn(() => mockGit);
Expand All @@ -23,6 +24,8 @@ jest.mock('fs', () => ({

jest.mock('fs-extra', () => ({
copy: jest.fn(),
remove: jest.fn(),
pathExists: jest.fn(),
}));

jest.mock('child_process');
20 changes: 14 additions & 6 deletions src/handlers/componentInstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EMULSIFY_PROJECT_CONFIG_FILE,
} from '../lib/constants';
import type { EmulsifySystem } from '@emulsify-cli/config';
import type { InstallComponentHandlerOptions } from '@emulsify-cli/handlers';
import getGitRepoNameFromUrl from '../util/getGitRepoNameFromUrl';
import getEmulsifyConfig from '../util/project/getEmulsifyConfig';
import getJsonFromCachedFile from '../util/cache/getJsonFromCachedFile';
Expand All @@ -14,7 +15,10 @@ import cloneIntoCache from '../util/cache/cloneIntoCache';
/**
* Handler for the `component install` command.
*/
export default async function componentInstall(name: string): Promise<void> {
export default async function componentInstall(
name: string,
{ force }: InstallComponentHandlerOptions
): Promise<void> {
const emulsifyConfig = await getEmulsifyConfig();
if (!emulsifyConfig) {
return log(
Expand Down Expand Up @@ -86,9 +90,13 @@ export default async function componentInstall(name: string): Promise<void> {
);
}

await installComponentFromCache(systemConf, variantConf, name);
return log(
'success',
`Success! The ${name} component has been added to your project.`
);
try {
await installComponentFromCache(systemConf, variantConf, name, force);
return log(
'success',
`Success! The ${name} component has been added to your project.`
);
} catch (e) {
return log('error', (e as Error).toString());
}
}
11 changes: 2 additions & 9 deletions src/handlers/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('init', () => {
});

it('can clone an Emulsify starter based on CLI input, and log a success message upon completion', async () => {
expect.assertions(3);
expect.assertions(2);
await init('cornflake', `${root}/themes/subDir`, {
starter: 'https://github.com/cornflake-ds/cornflake-drupal.git',
checkout: '5.6x',
Expand All @@ -77,14 +77,7 @@ describe('init', () => {
'/home/uname/Projects/cornflake/themes/subDir/cornflake',
{ '--branch': '5.6x' }
);
expect(logMock).toHaveBeenCalledWith(
'success',
'Created an Emulsify project in /home/uname/Projects/cornflake/themes/subDir/cornflake.'
);
expect(logMock).toHaveBeenCalledWith(
'info',
`Emulsify does not come with components by default.\nPlease use "emulsify system install" to select a design system you'd like to use.\nDoing so will install the system's default components, and allow you to install any other components made available by the design system.\nTo see a list of out-of-the-box design systems, run: "emulsify system ls"`
);
expect(logMock).toHaveBeenCalledTimes(5);
});

it('can clone an Emulsify starter without a provided checkout', async () => {
Expand Down
23 changes: 22 additions & 1 deletion src/handlers/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import R from 'ramda';
import { join } from 'path';
import { existsSync, promises as fs } from 'fs';
import simpleGit from 'simple-git';
import { cyan } from 'chalk';

import type { EmulsifyProjectConfiguration } from '@emulsify-cli/config';
import type { InitHandlerOptions } from '@emulsify-cli/handlers';
Expand Down Expand Up @@ -134,7 +135,27 @@ export default async function init(
log('success', `Created an Emulsify project in ${target}.`);
log(
'info',
`Emulsify does not come with components by default.\nPlease use "emulsify system install" to select a design system you'd like to use.\nDoing so will install the system's default components, and allow you to install any other components made available by the design system.\nTo see a list of out-of-the-box design systems, run: "emulsify system ls"`
`Make sure you install the modules your Emulsify-based theme requires in order to function.`
);
log(
'verbose',
`
- composer require drupal/components
- composer require drupal/emulsify_twig
- drush en components emulsify_twig -y
`
);
log(
'info',
`Once the requirements have been installed, you will need to select a system to use, as Emulsify does not come with components by default.`
);
log(
'verbose',
`
${cyan('List systems')}: emulsify system list
${cyan('Install a system')}: emulsify system install "system-name"
${cyan('Install default system')}: emulsify system install compound
`
);
} catch (e) {
log('error', `Unable to pull down ${repository}: ${String(e)}`, EXIT_ERROR);
Expand Down
7 changes: 6 additions & 1 deletion src/handlers/systemInstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,12 @@ export default async function systemInstall(
({ required }) => required === true
);
for (const component of requiredComponents) {
await installComponentFromCache(systemConf, variantConf, component.name);
await installComponentFromCache(
systemConf,
variantConf,
component.name,
true
);
}

// Install all global files and folders.
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ component
.action(componentList);
component
.command('install [name]')
.option(
'-f --force',
'Use this to overwrite a component that is already installed'
)
.alias('i')
.description(
"Install a component from within the current project's system and variant"
Expand Down
4 changes: 4 additions & 0 deletions src/types/handlers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ declare module '@emulsify-cli/handlers' {
checkout?: string | void;
variant?: string | void;
};

export type InstallComponentHandlerOptions = {
force?: boolean;
};
}
5 changes: 4 additions & 1 deletion src/util/cache/cloneIntoCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const mkdirMock = fs.promises.mkdir as jest.Mock;
const gitCloneMock = git().clone as jest.Mock;
const gitFetchMock = git().fetch as jest.Mock;
const gitCheckoutMock = git().checkout as jest.Mock;
const gitPullMock = git().pull as jest.Mock;

(findFileInCurrentPath as jest.Mock).mockReturnValue(
'/home/uname/projects/emulsify'
Expand All @@ -25,6 +26,7 @@ describe('cloneIntoCache', () => {
gitCloneMock.mockClear();
gitFetchMock.mockClear();
gitCheckoutMock.mockClear();
gitPullMock.mockClear();
});

const cloneOptions = {
Expand All @@ -33,11 +35,12 @@ describe('cloneIntoCache', () => {
};

it('can ensure that the correct branch is checked out, and return early if the cache item already exists', async () => {
expect.assertions(4);
expect.assertions(5);
existsSyncMock.mockReturnValueOnce(true);
await cloneIntoCache('systems', ['cornflake'])(cloneOptions);
expect(existsSyncMock).toHaveBeenCalledTimes(1);
expect(gitFetchMock).toHaveBeenCalledTimes(1);
expect(gitPullMock).toHaveBeenCalledTimes(1);
expect(gitCheckoutMock).toHaveBeenCalledWith('branch-name');
expect(gitCloneMock).not.toHaveBeenCalled();
});
Expand Down
1 change: 1 addition & 0 deletions src/util/cache/cloneIntoCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default function cloneIntoCache(
git = simpleGit(destination);
await git.fetch();
await git.checkout(checkout);
await git.pull();
}
return;
}
Expand Down
13 changes: 12 additions & 1 deletion src/util/cache/copyItemFromCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ jest.mock('./getCachedItemPath', () =>
'/home/uname/.emulsify/cache/systems/12345/compound/components/00-base/colors'
)
);
import { copy } from 'fs-extra';
import { copy, remove } from 'fs-extra';
import copyItemFromCache from './copyItemFromCache';

describe('copyItemFromCache', () => {
Expand All @@ -19,4 +19,15 @@ describe('copyItemFromCache', () => {
'/home/uname/Projects/drupal/web/themes/custom/cornflake/components/00-base/colors'
);
});
it('can remove a destination before copying items from the cache if "force" is true', async () => {
await copyItemFromCache(
'systems',
['compound', 'components', '00-base', 'colors'],
'/home/uname/Projects/drupal/web/themes/custom/cornflake/components/00-base/colors',
true
);
expect(remove).toHaveBeenCalledWith(
'/home/uname/Projects/drupal/web/themes/custom/cornflake/components/00-base/colors'
);
});
});
11 changes: 9 additions & 2 deletions src/util/cache/copyItemFromCache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CacheBucket, CacheItemPath } from '@emulsify-cli/cache';
import { copy } from 'fs-extra';
import { copy, remove } from 'fs-extra';
import getCachedItemPath from './getCachedItemPath';

/**
Expand All @@ -10,12 +10,19 @@ import getCachedItemPath from './getCachedItemPath';
* @param itemPath array containing segments of the path to the cached item within the specified bucket.
* @param item name of the cached item.
* @param destination full path to the destination to which the cached item should be copied.
* @param force if true, removes any file/folder at the destination before copying.
*/
export default async function copyFileFromCache(
bucket: CacheBucket,
itemPath: CacheItemPath,
destination: string
destination: string,
force = false
): Promise<void> {
const source = getCachedItemPath(bucket, itemPath);

if (force) {
await remove(destination);
}

return copy(source, destination);
}
15 changes: 14 additions & 1 deletion src/util/project/installComponentFromCache.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
jest.mock('../cache/copyItemFromCache', () => jest.fn());
jest.mock('../fs/findFileInCurrentPath', () => jest.fn());

import { pathExists } from 'fs-extra';
import type { EmulsifySystem, EmulsifyVariant } from '@emulsify-cli/config';
import copyItemFromCache from '../cache/copyItemFromCache';
import findFileInCurrentPath from '../fs/findFileInCurrentPath';
Expand All @@ -9,6 +10,7 @@ import installComponentFromCache from './installComponentFromCache';
const findFileMock = (findFileInCurrentPath as jest.Mock).mockReturnValue(
'/home/uname/Projects/cornflake/web/themes/custom/cornflake/project.emulsify.json'
);
const pathExistsMock = (pathExists as jest.Mock).mockResolvedValue(false);

describe('installComponentFromCache', () => {
const system = {
Expand Down Expand Up @@ -69,13 +71,24 @@ describe('installComponentFromCache', () => {
);
});

it('throws an error if the component is already installed, and force is false', async () => {
expect.assertions(1);
pathExistsMock.mockResolvedValueOnce(true);
await expect(
installComponentFromCache(system, variant, 'link', false)
).rejects.toThrow(
'The component "link" already exists, and force was not passed (--force).'
);
});

it('copies the component from the cached item into the correct destination', async () => {
expect.assertions(1);
await installComponentFromCache(system, variant, 'link');
expect(copyItemFromCache as jest.Mock).toHaveBeenCalledWith(
'systems',
['compound', './components/00-base', 'link'],
'/home/uname/Projects/cornflake/web/themes/custom/cornflake/components/00-base/link'
'/home/uname/Projects/cornflake/web/themes/custom/cornflake/components/00-base/link',
false
);
});
});
17 changes: 15 additions & 2 deletions src/util/project/installComponentFromCache.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { pathExists } from 'fs-extra';
import type { EmulsifySystem, EmulsifyVariant } from '@emulsify-cli/config';
import { join, dirname } from 'path';
import { EMULSIFY_PROJECT_CONFIG_FILE } from '../../lib/constants';
Expand All @@ -10,12 +11,14 @@ import copyItemFromCache from '../cache/copyItemFromCache';
* @param system EmulsifySystem object depicting the system from which the component should be installed.
* @param variant EmulsifyVariant object containing information about the component, where it lives, and how it should be installed.
* @param componentName string name of the component that should be installed.
* @param force if true, replaces an existing component (if any).
* @returns
*/
export default async function installComponentFromCache(
system: EmulsifySystem,
variant: EmulsifyVariant,
componentName: string
componentName: string,
force = false
): Promise<void> {
// Gather information about the current Emulsify project. If none exists,
// throw an error.
Expand Down Expand Up @@ -51,9 +54,19 @@ export default async function installComponentFromCache(
// Calculate the destination path based on the path to the Emulsify project, the structure of the
// component, and the component's name.
const destination = join(dirname(path), structure.directory, component.name);

// If the component already exists within the project, and force is not true,
// throw an error.
if ((await pathExists(destination)) && !force) {
throw new Error(
`The component "${component.name}" already exists, and force was not passed (--force).`
);
}

return copyItemFromCache(
'systems',
[system.name, structure.directory, component.name],
destination
destination,
force
);
}
6 changes: 4 additions & 2 deletions src/util/project/installGeneralAssetsFromCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ describe('installGeneralAssetsFromCache', () => {
1,
'systems',
['compound', './components/00-base/00-defaults'],
'/home/uname/Projects/cornflake/web/themes/custom/cornflake/components/00-base/00-defaults'
'/home/uname/Projects/cornflake/web/themes/custom/cornflake/components/00-base/00-defaults',
true
);
expect(copyItemMock).toHaveBeenNthCalledWith(
2,
'systems',
['compound', './components/style.scss'],
'/home/uname/Projects/cornflake/web/themes/custom/cornflake/components/style.scss'
'/home/uname/Projects/cornflake/web/themes/custom/cornflake/components/style.scss',
true
);
});

Expand Down
7 changes: 6 additions & 1 deletion src/util/project/installGeneralAssetsFromCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ export default async function installGeneralAssetsFromCache(
const destination = join(dirname(path), asset.destinationPath);
promises.push(
catchLater(
copyItemFromCache('systems', [system.name, asset.path], destination)
copyItemFromCache(
'systems',
[system.name, asset.path],
destination,
true
)
)
);
}
Expand Down