Skip to content

Commit ce032cd

Browse files
authored
Merge pull request #52 from dlabaj/design-comments
feat: added design comment system support to cli to make it easier fo…
2 parents 027ae7b + 2ae50d8 commit ce032cd

3 files changed

Lines changed: 328 additions & 0 deletions

File tree

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
jest.mock('fs-extra', () => ({
2+
__esModule: true,
3+
default: {
4+
pathExists: jest.fn(),
5+
},
6+
}));
7+
8+
jest.mock('execa', () => ({
9+
__esModule: true,
10+
execa: jest.fn(),
11+
}));
12+
13+
jest.mock('../git-user-config.js', () => ({
14+
promptAndSetLocalGitUser: jest.fn(),
15+
}));
16+
17+
import path from 'path';
18+
import fs from 'fs-extra';
19+
import { execa } from 'execa';
20+
import { promptAndSetLocalGitUser } from '../git-user-config.js';
21+
import { runAddDesignComments } from '../add-design-comments.js';
22+
23+
const mockPathExists = fs.pathExists as jest.MockedFunction<typeof fs.pathExists>;
24+
const mockExeca = execa as jest.MockedFunction<typeof execa>;
25+
const mockPromptAndSetLocalGitUser = promptAndSetLocalGitUser as jest.MockedFunction<
26+
typeof promptAndSetLocalGitUser
27+
>;
28+
29+
const cwd = '/tmp/design-comments-project';
30+
31+
function mockPathExistsForProject(options: { hasPackageJson?: boolean; hasGit?: boolean; lockFile?: 'yarn' | 'pnpm' | 'none' }): void {
32+
const { hasPackageJson = true, hasGit = true, lockFile = 'none' } = options;
33+
mockPathExists.mockImplementation(async (p: string) => {
34+
if (p === path.join(cwd, 'package.json')) return hasPackageJson;
35+
if (p === path.join(cwd, '.git')) return hasGit;
36+
if (p === path.join(cwd, 'yarn.lock')) return lockFile === 'yarn';
37+
if (p === path.join(cwd, 'pnpm-lock.yaml')) return lockFile === 'pnpm';
38+
return false;
39+
});
40+
}
41+
42+
describe('runAddDesignComments', () => {
43+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
44+
45+
beforeEach(() => {
46+
jest.clearAllMocks();
47+
mockExeca.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 } as Awaited<
48+
ReturnType<typeof execa>
49+
>);
50+
});
51+
52+
afterAll(() => {
53+
consoleLogSpy.mockRestore();
54+
});
55+
56+
it('throws when package.json is missing', async () => {
57+
mockPathExistsForProject({ hasPackageJson: false });
58+
59+
await expect(runAddDesignComments({ cwd })).rejects.toThrow(/No package\.json found/);
60+
expect(mockExeca).not.toHaveBeenCalled();
61+
});
62+
63+
it('initializes git when .git is missing', async () => {
64+
mockPathExistsForProject({ hasGit: false });
65+
66+
await runAddDesignComments({ cwd });
67+
68+
expect(mockExeca).toHaveBeenCalledWith('git', ['init'], { stdio: 'inherit', cwd });
69+
expect(consoleLogSpy).toHaveBeenCalledWith('✅ Git repository initialized.\n');
70+
});
71+
72+
it('skips git init when .git already exists', async () => {
73+
mockPathExistsForProject({ hasGit: true });
74+
75+
await runAddDesignComments({ cwd });
76+
77+
expect(mockExeca).not.toHaveBeenCalledWith('git', ['init'], expect.anything());
78+
});
79+
80+
it('installs with npm and runs design-comments init by default', async () => {
81+
mockPathExistsForProject({ lockFile: 'none' });
82+
83+
await runAddDesignComments({ cwd });
84+
85+
expect(mockExeca).toHaveBeenCalledWith(
86+
'npm',
87+
['install', '@patternfly/design-comments'],
88+
{ cwd, stdio: 'inherit' },
89+
);
90+
expect(mockExeca).toHaveBeenCalledWith('npx', ['design-comments', 'init'], {
91+
cwd,
92+
stdio: 'inherit',
93+
});
94+
});
95+
96+
it('uses yarn when yarn.lock is present', async () => {
97+
mockPathExistsForProject({ lockFile: 'yarn' });
98+
99+
await runAddDesignComments({ cwd });
100+
101+
expect(mockExeca).toHaveBeenCalledWith(
102+
'yarn',
103+
['add', '@patternfly/design-comments'],
104+
{ cwd, stdio: 'inherit' },
105+
);
106+
});
107+
108+
it('uses pnpm when pnpm-lock.yaml is present', async () => {
109+
mockPathExistsForProject({ lockFile: 'pnpm' });
110+
111+
await runAddDesignComments({ cwd });
112+
113+
expect(mockExeca).toHaveBeenCalledWith(
114+
'pnpm',
115+
['add', '@patternfly/design-comments'],
116+
{ cwd, stdio: 'inherit' },
117+
);
118+
});
119+
120+
it('prompts for local git user config when gitInit is true', async () => {
121+
mockPathExistsForProject({});
122+
123+
await runAddDesignComments({ cwd, gitInit: true });
124+
125+
expect(mockPromptAndSetLocalGitUser).toHaveBeenCalledWith(cwd);
126+
});
127+
128+
it('does not prompt for local git user config by default', async () => {
129+
mockPathExistsForProject({});
130+
131+
await runAddDesignComments({ cwd });
132+
133+
expect(mockPromptAndSetLocalGitUser).not.toHaveBeenCalled();
134+
});
135+
136+
it('runs git init, install, and design-comments init in order', async () => {
137+
mockPathExistsForProject({ hasGit: false, lockFile: 'none' });
138+
const callOrder: string[] = [];
139+
mockExeca.mockImplementation(async (command, args) => {
140+
if (command === 'git' && args?.[0] === 'init') callOrder.push('git-init');
141+
if (command === 'npm') callOrder.push('install');
142+
if (command === 'npx') callOrder.push('design-comments-init');
143+
return { stdout: '', stderr: '', exitCode: 0 } as Awaited<ReturnType<typeof execa>>;
144+
});
145+
146+
await runAddDesignComments({ cwd });
147+
148+
expect(callOrder).toEqual(['git-init', 'install', 'design-comments-init']);
149+
});
150+
151+
it('runs git user config after git init when gitInit is true', async () => {
152+
mockPathExistsForProject({ hasGit: false });
153+
const callOrder: string[] = [];
154+
mockExeca.mockImplementation(async (command, args) => {
155+
if (command === 'git' && args?.[0] === 'init') callOrder.push('git-init');
156+
if (command === 'npm') callOrder.push('install');
157+
return { stdout: '', stderr: '', exitCode: 0 } as Awaited<ReturnType<typeof execa>>;
158+
});
159+
mockPromptAndSetLocalGitUser.mockImplementation(async () => {
160+
callOrder.push('git-user-config');
161+
});
162+
163+
await runAddDesignComments({ cwd, gitInit: true });
164+
165+
expect(callOrder).toEqual(['git-init', 'git-user-config', 'install']);
166+
});
167+
168+
it('runs npx design-comments init after yarn install', async () => {
169+
mockPathExistsForProject({ lockFile: 'yarn' });
170+
171+
await runAddDesignComments({ cwd });
172+
173+
const yarnIdx = mockExeca.mock.calls.findIndex(([cmd]) => cmd === 'yarn');
174+
const npxIdx = mockExeca.mock.calls.findIndex(([cmd]) => cmd === 'npx');
175+
expect(yarnIdx).toBeGreaterThan(-1);
176+
expect(npxIdx).toBeGreaterThan(yarnIdx);
177+
expect(mockExeca).toHaveBeenCalledWith('npx', ['design-comments', 'init'], {
178+
cwd,
179+
stdio: 'inherit',
180+
});
181+
});
182+
183+
it('runs npx design-comments init after pnpm install', async () => {
184+
mockPathExistsForProject({ lockFile: 'pnpm' });
185+
186+
await runAddDesignComments({ cwd });
187+
188+
const pnpmIdx = mockExeca.mock.calls.findIndex(([cmd]) => cmd === 'pnpm');
189+
const npxIdx = mockExeca.mock.calls.findIndex(([cmd]) => cmd === 'npx');
190+
expect(pnpmIdx).toBeGreaterThan(-1);
191+
expect(npxIdx).toBeGreaterThan(pnpmIdx);
192+
});
193+
194+
it('logs install and success messages', async () => {
195+
mockPathExistsForProject({});
196+
197+
await runAddDesignComments({ cwd });
198+
199+
expect(consoleLogSpy).toHaveBeenCalledWith('📦 Installing @patternfly/design-comments...\n');
200+
expect(consoleLogSpy).toHaveBeenCalledWith('\n🔧 Running design-comments setup...\n');
201+
expect(consoleLogSpy).toHaveBeenCalledWith(
202+
'\n✨ design-comments installed and integrated. Start your dev server to add comments on your UI.\n',
203+
);
204+
});
205+
206+
it('propagates errors from package install', async () => {
207+
mockPathExistsForProject({});
208+
const installError = new Error('npm install failed');
209+
mockExeca.mockImplementation(async (command) => {
210+
if (command === 'npm') throw installError;
211+
return { stdout: '', stderr: '', exitCode: 0 } as Awaited<ReturnType<typeof execa>>;
212+
});
213+
214+
await expect(runAddDesignComments({ cwd })).rejects.toThrow('npm install failed');
215+
expect(mockExeca).not.toHaveBeenCalledWith('npx', ['design-comments', 'init'], expect.anything());
216+
});
217+
218+
it('propagates errors from design-comments init', async () => {
219+
mockPathExistsForProject({});
220+
const initError = new Error('design-comments init failed');
221+
mockExeca.mockImplementation(async (command) => {
222+
if (command === 'npx') throw initError;
223+
return { stdout: '', stderr: '', exitCode: 0 } as Awaited<ReturnType<typeof execa>>;
224+
});
225+
226+
await expect(runAddDesignComments({ cwd })).rejects.toThrow('design-comments init failed');
227+
expect(mockExeca).toHaveBeenCalledWith(
228+
'npm',
229+
['install', '@patternfly/design-comments'],
230+
{ cwd, stdio: 'inherit' },
231+
);
232+
});
233+
});

src/add-design-comments.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import path from 'node:path';
2+
import { execa } from 'execa';
3+
import fs from 'fs-extra';
4+
import { promptAndSetLocalGitUser } from './git-user-config.js';
5+
6+
const DESIGN_COMMENTS_PACKAGE = '@patternfly/design-comments';
7+
8+
export type RunAddDesignCommentsOptions = {
9+
/** Project root to install into. */
10+
cwd: string;
11+
/** When true, prompt for git user.name and user.email and store them locally. */
12+
gitInit?: boolean;
13+
};
14+
15+
async function getPackageManager(cwd: string): Promise<'yarn' | 'pnpm' | 'npm'> {
16+
if (await fs.pathExists(path.join(cwd, 'yarn.lock'))) return 'yarn';
17+
if (await fs.pathExists(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
18+
return 'npm';
19+
}
20+
21+
function getInstallArgs(packageManager: 'yarn' | 'pnpm' | 'npm'): string[] {
22+
switch (packageManager) {
23+
case 'yarn':
24+
return ['add', DESIGN_COMMENTS_PACKAGE];
25+
case 'pnpm':
26+
return ['add', DESIGN_COMMENTS_PACKAGE];
27+
default:
28+
return ['install', DESIGN_COMMENTS_PACKAGE];
29+
}
30+
}
31+
32+
async function ensureGitRepository(cwd: string): Promise<void> {
33+
const gitDir = path.join(cwd, '.git');
34+
if (!(await fs.pathExists(gitDir))) {
35+
await execa('git', ['init'], { stdio: 'inherit', cwd });
36+
console.log('✅ Git repository initialized.\n');
37+
}
38+
}
39+
40+
/**
41+
* Install @patternfly/design-comments and run its integration script so users can pin comments on UI elements.
42+
*/
43+
export async function runAddDesignComments(options: RunAddDesignCommentsOptions): Promise<void> {
44+
const { cwd, gitInit = false } = options;
45+
46+
const pkgJsonPath = path.join(cwd, 'package.json');
47+
if (!(await fs.pathExists(pkgJsonPath))) {
48+
throw new Error(
49+
`No package.json found in ${cwd}.\n` +
50+
'Run this command from a Node.js project root (or create one with "patternfly-cli create").',
51+
);
52+
}
53+
54+
await ensureGitRepository(cwd);
55+
56+
if (gitInit) {
57+
await promptAndSetLocalGitUser(cwd);
58+
}
59+
60+
const packageManager = await getPackageManager(cwd);
61+
const installArgs = getInstallArgs(packageManager);
62+
63+
console.log(`📦 Installing ${DESIGN_COMMENTS_PACKAGE}...\n`);
64+
await execa(packageManager, installArgs, { cwd, stdio: 'inherit' });
65+
66+
console.log('\n🔧 Running design-comments setup...\n');
67+
await execa('npx', ['design-comments', 'init'], { cwd, stdio: 'inherit' });
68+
69+
console.log(
70+
'\n✨ design-comments installed and integrated. Start your dev server to add comments on your UI.\n',
71+
);
72+
}

src/cli.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { runSave } from './save.js';
1313
import { runLoad } from './load.js';
1414
import { runDeployToGitHubPages } from './gh-pages.js';
1515
import { runAddAiContext } from './add-ai-context.js';
16+
import { runAddDesignComments } from './add-design-comments.js';
1617
import { readPackageVersion } from './read-package-version.js';
1718
import { promptAndSetLocalGitUser } from './git-user-config.js';
1819
import { runBumpPrerelease } from './bump-prerelease.js';
@@ -148,6 +149,28 @@ program
148149
console.log('\n✨ All updates completed successfully! ✨');
149150
});
150151

152+
/** Command to install and integrate @patternfly/design-comments */
153+
program
154+
.command('add-design-comments')
155+
.description(
156+
'Install @patternfly/design-comments and integrate the commenting overlay into a React project',
157+
)
158+
.argument('[path]', 'Path to the project (defaults to current directory)')
159+
.option('--git-init', 'Prompt for git user.name and user.email and store them locally for this repository')
160+
.action(async (projectPath, options) => {
161+
const cwd = projectPath ? path.resolve(projectPath) : process.cwd();
162+
try {
163+
await runAddDesignComments({ cwd, gitInit: Boolean(options.gitInit) });
164+
} catch (error) {
165+
if (error instanceof Error) {
166+
console.error(`\n❌ ${error.message}\n`);
167+
} else {
168+
console.error(error);
169+
}
170+
process.exit(1);
171+
}
172+
});
173+
151174
/** Command to run the PatternFly context-for-ai codemod (semantic data-* attributes) */
152175
program
153176
.command('add-ai-context')

0 commit comments

Comments
 (0)