Skip to content

Commit 96a9f4a

Browse files
authored
feat: Add ensureFileFrom helper (#6)
* feat: Add ensureFileFrom helper * Run prettier
1 parent 0259ae9 commit 96a9f4a

12 files changed

+190
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'workspace-meta': patch
3+
---
4+
5+
Add ensureFileFrom helper and configDirectory to PluginContext. The ensureFileFrom helper allows copying template files from the config directory, and all plugins now have access to the configDirectory path (inferred as `${workspacePath}/.workspace-meta`) in the plugin context.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { readFile } from 'node:fs/promises';
2+
import path from 'node:path';
3+
import { describe, expect, it, vi } from 'vitest';
4+
5+
import { ensureFileFrom } from './ensure-file-from.js';
6+
7+
vi.mock('node:fs/promises', () => ({
8+
readFile: vi.fn(),
9+
}));
10+
11+
describe('ensureFileFrom', () => {
12+
it('should read template file from config directory and write to package', async () => {
13+
const mockReadFile = vi.mocked(readFile);
14+
const mockWriteFile = vi.fn();
15+
const templateContent = 'This is template content';
16+
17+
mockReadFile.mockResolvedValue(templateContent);
18+
19+
const plugin = ensureFileFrom('.eslintrc.json', 'templates/eslintrc.json');
20+
21+
const ctx = {
22+
workspacePath: '/workspace',
23+
packagePath: '/workspace/packages/test',
24+
packageName: 'test',
25+
packageJson: { name: 'test' },
26+
configDirectory: '/workspace/config',
27+
isCheckMode: false,
28+
readFile: vi.fn(),
29+
writeFile: mockWriteFile,
30+
};
31+
32+
await plugin(ctx);
33+
34+
expect(mockReadFile).toHaveBeenCalledWith(
35+
path.resolve('/workspace/config', 'templates/eslintrc.json'),
36+
'utf8',
37+
);
38+
expect(mockWriteFile).toHaveBeenCalledWith(
39+
'.eslintrc.json',
40+
templateContent,
41+
);
42+
});
43+
44+
it('should handle absolute template paths correctly', async () => {
45+
const mockReadFile = vi.mocked(readFile);
46+
const mockWriteFile = vi.fn();
47+
const templateContent = 'Template content';
48+
49+
mockReadFile.mockResolvedValue(templateContent);
50+
51+
const plugin = ensureFileFrom('config.json', '../shared/config.json');
52+
53+
const ctx = {
54+
workspacePath: '/workspace',
55+
packagePath: '/workspace/packages/test',
56+
packageName: 'test',
57+
packageJson: { name: 'test' },
58+
configDirectory: '/workspace/.config',
59+
isCheckMode: false,
60+
readFile: vi.fn(),
61+
writeFile: mockWriteFile,
62+
};
63+
64+
await plugin(ctx);
65+
66+
expect(mockReadFile).toHaveBeenCalledWith(
67+
path.resolve('/workspace/.config', '../shared/config.json'),
68+
'utf8',
69+
);
70+
expect(mockWriteFile).toHaveBeenCalledWith('config.json', templateContent);
71+
});
72+
73+
it('should throw error if template file cannot be read', async () => {
74+
const mockReadFile = vi.mocked(readFile);
75+
const mockWriteFile = vi.fn();
76+
77+
mockReadFile.mockRejectedValue(new Error('File not found'));
78+
79+
const plugin = ensureFileFrom('output.txt', 'templates/missing.txt');
80+
81+
const ctx = {
82+
workspacePath: '/workspace',
83+
packagePath: '/workspace/packages/test',
84+
packageName: 'test',
85+
packageJson: { name: 'test' },
86+
configDirectory: '/workspace/config',
87+
isCheckMode: false,
88+
readFile: vi.fn(),
89+
writeFile: mockWriteFile,
90+
};
91+
92+
await expect(plugin(ctx)).rejects.toThrow(
93+
'Failed to read template file "templates/missing.txt" from config directory: File not found',
94+
);
95+
96+
expect(mockWriteFile).not.toHaveBeenCalled();
97+
});
98+
99+
it('should handle non-Error exceptions when reading template', async () => {
100+
const mockReadFile = vi.mocked(readFile);
101+
const mockWriteFile = vi.fn();
102+
103+
mockReadFile.mockRejectedValue('Unknown error');
104+
105+
const plugin = ensureFileFrom('output.txt', 'template.txt');
106+
107+
const ctx = {
108+
workspacePath: '/workspace',
109+
packagePath: '/workspace/packages/test',
110+
packageName: 'test',
111+
packageJson: { name: 'test' },
112+
configDirectory: '/workspace/config',
113+
isCheckMode: false,
114+
readFile: vi.fn(),
115+
writeFile: mockWriteFile,
116+
};
117+
118+
await expect(plugin(ctx)).rejects.toThrow(
119+
'Failed to read template file "template.txt" from config directory: Unknown error',
120+
);
121+
});
122+
});

src/helpers/ensure-file-from.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { readFile } from 'node:fs/promises';
2+
import path from 'node:path';
3+
4+
import type { Plugin, PluginContext } from '#src/types/config.js';
5+
6+
/**
7+
* Ensures that a file exists with content loaded from a template file.
8+
*
9+
* @param filePath - The path to the file to ensure in the package.
10+
* @param templatePath - The path to the template file relative to the config directory.
11+
* @returns A plugin that ensures the file exists with the content from the template.
12+
*/
13+
export function ensureFileFrom(filePath: string, templatePath: string): Plugin {
14+
return async (ctx: PluginContext): Promise<void> => {
15+
const absoluteTemplatePath = path.resolve(
16+
ctx.configDirectory,
17+
templatePath,
18+
);
19+
20+
try {
21+
const content = await readFile(absoluteTemplatePath, 'utf8');
22+
await ctx.writeFile(filePath, content);
23+
} catch (error) {
24+
throw new Error(
25+
`Failed to read template file "${templatePath}" from config directory: ${
26+
error instanceof Error ? error.message : String(error)
27+
}`,
28+
);
29+
}
30+
};
31+
}

src/helpers/ensure-file.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,12 @@ describe('ensureFile', () => {
5757
'output.txt': 'Config: {"theme": "dark"}',
5858
});
5959
});
60+
61+
it('should not write file when content function returns undefined', async () => {
62+
const content = vi.fn(() => undefined);
63+
const result = await runPluginTestWrapper(ensureFile('test.txt', content));
64+
65+
expect(content).toHaveBeenCalledWith(result.context);
66+
expect(result.files).toEqual({});
67+
});
6068
});

src/helpers/ensure-file.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import type { Plugin, PluginContext } from '#src/types/config.js';
22

3+
/**
4+
* Ensures that a file exists with the given content.
5+
*
6+
* @param filePath - The path to the file to ensure.
7+
* @param content - The content to write to the file. If a function is provided, it will be called with the plugin context.
8+
* @returns A plugin that ensures the file exists with the given content.
9+
*/
310
export function ensureFile(
411
filePath: string,
512
content:

src/helpers/ensure-package-json.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe('ensurePackageJson', () => {
2727
name: 'test-package',
2828
version: '1.0.0',
2929
}),
30+
result.context,
3031
);
3132

3233
const packageJsonContents = result.files['package.json'] ?? '';

src/helpers/ensure-package-json.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import type { Plugin, PluginContext } from '#src/types/config.js';
99
* @returns A plugin that ensures a package.json file exists and updates it with the provided updater
1010
*/
1111
export function ensurePackageJson(
12-
updater: (packageJson: PackageJson) => PackageJson | undefined,
12+
updater: (
13+
packageJson: PackageJson,
14+
ctx: PluginContext,
15+
) => Promise<PackageJson | undefined> | PackageJson | undefined,
1316
): Plugin {
1417
return async (ctx: PluginContext): Promise<void> => {
15-
const result = updater(ctx.packageJson);
18+
const result = await updater(ctx.packageJson, ctx);
1619
const finalPackageJson = result ?? ctx.packageJson;
1720

1821
const content = `${JSON.stringify(finalPackageJson, null, 2)}\n`;

src/helpers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './conditional-file.js';
22
export * from './define-workspace-meta-config.js';
3+
export * from './ensure-file-from.js';
34
export * from './ensure-file.js';
45
export * from './ensure-package-json.js';
56
export * from './prettier-formatter.js';

src/services/plugin-runner.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ describe('PluginRunner', () => {
4545
packagePath: mockPackage.path,
4646
packageName: mockPackage.name,
4747
packageJson: mockPackage.packageJson,
48+
configDirectory: '/workspace/.workspace-meta',
4849
isCheckMode: false,
4950
}),
5051
);

src/services/plugin-runner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export class PluginRunner {
7373
packagePath: pkg.path,
7474
packageName: pkg.name,
7575
packageJson: pkg.packageJson,
76+
configDirectory: path.join(this.workspacePath, '.workspace-meta'),
7677
isCheckMode: options.isCheckMode,
7778
readFile: this.createReadFile(pkg.path),
7879
writeFile: this.createWriteFile(

0 commit comments

Comments
 (0)