@launch77/cli 1.4.0 ā 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/modules/plugin/services/plugin-svc.d.ts +21 -0
- package/dist/modules/plugin/services/plugin-svc.d.ts.map +1 -1
- package/dist/modules/plugin/services/plugin-svc.js +107 -13
- package/dist/modules/plugin/services/plugin-svc.js.map +1 -1
- package/dist/modules/plugin/services/plugin-svc.test.d.ts +2 -0
- package/dist/modules/plugin/services/plugin-svc.test.d.ts.map +1 -0
- package/dist/modules/plugin/services/plugin-svc.test.js +362 -0
- package/dist/modules/plugin/services/plugin-svc.test.js.map +1 -0
- package/dist/templates/workspace/package.json +2 -1
- package/package.json +1 -1
- package/src/modules/plugin/services/plugin-svc.test.ts +418 -0
- package/src/modules/plugin/services/plugin-svc.ts +122 -15
- package/templates/workspace/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import type { Launch77Context } from '@launch77/plugin-runtime';
|
|
2
2
|
import type { InstallPluginRequest, InstallPluginResult } from '../types/plugin-types.js';
|
|
3
3
|
export declare class PluginService {
|
|
4
|
+
/**
|
|
5
|
+
* Validate that we're in a valid package directory and return the target type
|
|
6
|
+
*/
|
|
7
|
+
private validateContext;
|
|
8
|
+
/**
|
|
9
|
+
* Validate plugin name, resolve its location, and download if needed
|
|
10
|
+
*/
|
|
11
|
+
private validateAndResolvePlugin;
|
|
12
|
+
/**
|
|
13
|
+
* Read plugin metadata and validate it supports the current target
|
|
14
|
+
*/
|
|
15
|
+
private validatePluginTargets;
|
|
16
|
+
/**
|
|
17
|
+
* Check if plugin is already installed and return early-exit result if so
|
|
18
|
+
*/
|
|
19
|
+
private checkExistingInstallation;
|
|
4
20
|
/**
|
|
5
21
|
* Install a plugin to the current package
|
|
6
22
|
*/
|
|
@@ -11,5 +27,10 @@ export declare class PluginService {
|
|
|
11
27
|
private downloadNpmPlugin;
|
|
12
28
|
private getPackagePath;
|
|
13
29
|
private runGenerator;
|
|
30
|
+
private isPluginInstalled;
|
|
31
|
+
/**
|
|
32
|
+
* Write plugin installation metadata to the target package's package.json
|
|
33
|
+
*/
|
|
34
|
+
private writePluginManifest;
|
|
14
35
|
}
|
|
15
36
|
//# sourceMappingURL=plugin-svc.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-svc.d.ts","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin-svc.d.ts","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAA0F,MAAM,0BAA0B,CAAA;AACvJ,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAoBzF,qBAAa,aAAa;IACxB;;OAEG;IACH,OAAO,CAAC,eAAe;IAOvB;;OAEG;YACW,wBAAwB;IAkCtC;;OAEG;YACW,qBAAqB;IAcnC;;OAEG;YACW,yBAAyB;IAmBvC;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA6B3J;;OAEG;YACW,iBAAiB;IAkB/B,OAAO,CAAC,cAAc;YAgBR,YAAY;YAeZ,iBAAiB;IAmB/B;;OAEG;YACW,mBAAmB;CA4BlC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import { execa } from 'execa';
|
|
4
5
|
import { readPluginMetadata } from '@launch77/plugin-runtime';
|
|
@@ -23,17 +24,20 @@ function locationTypeToTarget(locationType) {
|
|
|
23
24
|
}
|
|
24
25
|
export class PluginService {
|
|
25
26
|
/**
|
|
26
|
-
*
|
|
27
|
+
* Validate that we're in a valid package directory and return the target type
|
|
27
28
|
*/
|
|
28
|
-
|
|
29
|
-
const { pluginName } = request;
|
|
30
|
-
// Must be in a package directory (app, library, plugin, or app-template)
|
|
29
|
+
validateContext(context) {
|
|
31
30
|
const currentTarget = locationTypeToTarget(context.locationType);
|
|
32
31
|
if (!currentTarget)
|
|
33
32
|
throw createInvalidContextError(context.locationType);
|
|
34
33
|
if (!context.appName)
|
|
35
34
|
throw new InvalidPluginContextError('Could not determine package name. This is a bug. Please report it.');
|
|
36
|
-
|
|
35
|
+
return currentTarget;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validate plugin name, resolve its location, and download if needed
|
|
39
|
+
*/
|
|
40
|
+
async validateAndResolvePlugin(pluginName, workspaceRoot, logger) {
|
|
37
41
|
logger(chalk.blue(`\nš Resolving plugin "${pluginName}"...`));
|
|
38
42
|
logger(` āā Validating plugin name...`);
|
|
39
43
|
const validation = validatePluginInput(pluginName);
|
|
@@ -41,9 +45,8 @@ export class PluginService {
|
|
|
41
45
|
throw new PluginResolutionError(pluginName, validation.error || 'Invalid plugin name');
|
|
42
46
|
}
|
|
43
47
|
logger(` ā āā ${chalk.green('ā')} Valid plugin name`);
|
|
44
|
-
// Step 2: Resolve plugin location
|
|
45
48
|
logger(` āā Checking local workspace: ${chalk.dim(`plugins/${pluginName}`)}`);
|
|
46
|
-
const resolution = await resolvePluginLocation(pluginName,
|
|
49
|
+
const resolution = await resolvePluginLocation(pluginName, workspaceRoot);
|
|
47
50
|
let pluginPath;
|
|
48
51
|
if (resolution.source === 'local') {
|
|
49
52
|
logger(` ā āā ${chalk.green('ā')} Found local plugin`);
|
|
@@ -52,11 +55,19 @@ export class PluginService {
|
|
|
52
55
|
else {
|
|
53
56
|
logger(` ā āā ${chalk.dim('Not found locally')}`);
|
|
54
57
|
logger(` āā Resolving to npm package: ${chalk.cyan(resolution.npmPackage)}`);
|
|
55
|
-
|
|
56
|
-
pluginPath = await this.downloadNpmPlugin(resolution.npmPackage, context.workspaceRoot, logger);
|
|
58
|
+
pluginPath = await this.downloadNpmPlugin(resolution.npmPackage, workspaceRoot, logger);
|
|
57
59
|
}
|
|
58
60
|
logger(` āā ${chalk.green('ā')} Plugin resolved\n`);
|
|
59
|
-
|
|
61
|
+
return {
|
|
62
|
+
pluginPath,
|
|
63
|
+
source: resolution.source,
|
|
64
|
+
npmPackage: resolution.npmPackage,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read plugin metadata and validate it supports the current target
|
|
69
|
+
*/
|
|
70
|
+
async validatePluginTargets(pluginPath, pluginName, currentTarget) {
|
|
60
71
|
const metadata = await readPluginMetadata(pluginPath);
|
|
61
72
|
if (!metadata.targets || metadata.targets.length === 0) {
|
|
62
73
|
throw new MissingPluginTargetsError(pluginName);
|
|
@@ -64,10 +75,49 @@ export class PluginService {
|
|
|
64
75
|
if (!metadata.targets.includes(currentTarget)) {
|
|
65
76
|
throw createInvalidTargetError(pluginName, currentTarget, metadata.targets);
|
|
66
77
|
}
|
|
67
|
-
|
|
78
|
+
return metadata;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if plugin is already installed and return early-exit result if so
|
|
82
|
+
*/
|
|
83
|
+
async checkExistingInstallation(pluginName, packagePath, logger) {
|
|
84
|
+
const existingInstallation = await this.isPluginInstalled(pluginName, packagePath);
|
|
85
|
+
if (existingInstallation) {
|
|
86
|
+
logger(chalk.yellow(`\nā¹ļø Plugin '${pluginName}' is already installed in this package.\n`));
|
|
87
|
+
logger(`Package: ${chalk.cyan(existingInstallation.package)} (${existingInstallation.source})`);
|
|
88
|
+
logger(`Version: ${existingInstallation.version}`);
|
|
89
|
+
logger(`Installed: ${existingInstallation.installedAt}\n`);
|
|
90
|
+
logger(chalk.gray('To reinstall: Remove from package.json launch77.installedPlugins'));
|
|
91
|
+
logger(chalk.gray('(plugin:remove command coming soon)\n'));
|
|
92
|
+
return {
|
|
93
|
+
pluginName,
|
|
94
|
+
filesInstalled: false,
|
|
95
|
+
packageJsonUpdated: false,
|
|
96
|
+
dependenciesInstalled: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Install a plugin to the current package
|
|
103
|
+
*/
|
|
104
|
+
async installPlugin(request, context, logger = console.log) {
|
|
105
|
+
const { pluginName } = request;
|
|
106
|
+
const currentTarget = this.validateContext(context);
|
|
107
|
+
const { pluginPath, source, npmPackage } = await this.validateAndResolvePlugin(pluginName, context.workspaceRoot, logger);
|
|
108
|
+
const metadata = await this.validatePluginTargets(pluginPath, pluginName, currentTarget);
|
|
68
109
|
const packagePath = this.getPackagePath(context);
|
|
69
|
-
|
|
110
|
+
const earlyExit = await this.checkExistingInstallation(pluginName, packagePath, logger);
|
|
111
|
+
if (earlyExit)
|
|
112
|
+
return earlyExit;
|
|
70
113
|
await this.runGenerator(pluginPath, packagePath, context);
|
|
114
|
+
const packageName = source === 'npm' ? npmPackage : pluginName;
|
|
115
|
+
await this.writePluginManifest(packagePath, {
|
|
116
|
+
pluginName,
|
|
117
|
+
packageName,
|
|
118
|
+
version: metadata.version,
|
|
119
|
+
source,
|
|
120
|
+
});
|
|
71
121
|
return {
|
|
72
122
|
pluginName,
|
|
73
123
|
filesInstalled: true,
|
|
@@ -112,7 +162,8 @@ export class PluginService {
|
|
|
112
162
|
async runGenerator(pluginPath, appPath, context) {
|
|
113
163
|
try {
|
|
114
164
|
const generatorPath = path.join(pluginPath, 'dist/generator.js');
|
|
115
|
-
|
|
165
|
+
const args = [generatorPath, `--appPath=${appPath}`, `--appName=${context.appName}`, `--workspaceName=${context.workspaceName}`, `--pluginPath=${pluginPath}`];
|
|
166
|
+
await execa('node', args, {
|
|
116
167
|
cwd: pluginPath,
|
|
117
168
|
stdio: 'inherit',
|
|
118
169
|
});
|
|
@@ -121,5 +172,48 @@ export class PluginService {
|
|
|
121
172
|
throw new PluginInstallationError(`Generator failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
|
|
122
173
|
}
|
|
123
174
|
}
|
|
175
|
+
async isPluginInstalled(pluginName, packagePath) {
|
|
176
|
+
try {
|
|
177
|
+
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
178
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
|
|
179
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
180
|
+
const manifest = packageJson.launch77;
|
|
181
|
+
if (manifest?.installedPlugins?.[pluginName]) {
|
|
182
|
+
return manifest.installedPlugins[pluginName];
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
// If package.json doesn't exist or can't be read, assume not installed
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Write plugin installation metadata to the target package's package.json
|
|
193
|
+
*/
|
|
194
|
+
async writePluginManifest(packagePath, installationInfo) {
|
|
195
|
+
try {
|
|
196
|
+
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
197
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
|
|
198
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
199
|
+
if (!packageJson.launch77) {
|
|
200
|
+
packageJson.launch77 = {};
|
|
201
|
+
}
|
|
202
|
+
if (!packageJson.launch77.installedPlugins) {
|
|
203
|
+
packageJson.launch77.installedPlugins = {};
|
|
204
|
+
}
|
|
205
|
+
const manifest = packageJson.launch77;
|
|
206
|
+
manifest.installedPlugins[installationInfo.pluginName] = {
|
|
207
|
+
package: installationInfo.packageName,
|
|
208
|
+
version: installationInfo.version,
|
|
209
|
+
installedAt: new Date().toISOString(),
|
|
210
|
+
source: installationInfo.source,
|
|
211
|
+
};
|
|
212
|
+
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
throw new PluginInstallationError(`Failed to write plugin manifest: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
124
218
|
}
|
|
125
219
|
//# sourceMappingURL=plugin-svc.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-svc.js","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"plugin-svc.js","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-svc.test.d.ts","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import { PluginService } from './plugin-svc.js';
|
|
6
|
+
describe('PluginService', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
let service;
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'plugin-svc-test-'));
|
|
11
|
+
service = new PluginService();
|
|
12
|
+
});
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await fs.remove(tempDir);
|
|
15
|
+
});
|
|
16
|
+
describe('validateContext', () => {
|
|
17
|
+
// Valid contexts - test return values
|
|
18
|
+
test('should return "app" for workspace-app location', () => {
|
|
19
|
+
const context = {
|
|
20
|
+
isValid: true,
|
|
21
|
+
locationType: 'workspace-app',
|
|
22
|
+
workspaceRoot: tempDir,
|
|
23
|
+
workspaceName: 'test-workspace',
|
|
24
|
+
workspaceVersion: '1.0.0',
|
|
25
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
26
|
+
packageName: 'test-app',
|
|
27
|
+
appName: 'test-app',
|
|
28
|
+
};
|
|
29
|
+
const result = service.validateContext(context);
|
|
30
|
+
expect(result).toBe('app');
|
|
31
|
+
});
|
|
32
|
+
test('should return "library" for workspace-library location', () => {
|
|
33
|
+
const context = {
|
|
34
|
+
isValid: true,
|
|
35
|
+
locationType: 'workspace-library',
|
|
36
|
+
workspaceRoot: tempDir,
|
|
37
|
+
workspaceName: 'test-workspace',
|
|
38
|
+
workspaceVersion: '1.0.0',
|
|
39
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
40
|
+
packageName: 'test-library',
|
|
41
|
+
appName: 'test-library',
|
|
42
|
+
};
|
|
43
|
+
const result = service.validateContext(context);
|
|
44
|
+
expect(result).toBe('library');
|
|
45
|
+
});
|
|
46
|
+
test('should return "plugin" for workspace-plugin location', () => {
|
|
47
|
+
const context = {
|
|
48
|
+
isValid: true,
|
|
49
|
+
locationType: 'workspace-plugin',
|
|
50
|
+
workspaceRoot: tempDir,
|
|
51
|
+
workspaceName: 'test-workspace',
|
|
52
|
+
workspaceVersion: '1.0.0',
|
|
53
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
54
|
+
packageName: 'test-plugin',
|
|
55
|
+
appName: 'test-plugin',
|
|
56
|
+
};
|
|
57
|
+
const result = service.validateContext(context);
|
|
58
|
+
expect(result).toBe('plugin');
|
|
59
|
+
});
|
|
60
|
+
test('should return "app-template" for workspace-app-template location', () => {
|
|
61
|
+
const context = {
|
|
62
|
+
isValid: true,
|
|
63
|
+
locationType: 'workspace-app-template',
|
|
64
|
+
workspaceRoot: tempDir,
|
|
65
|
+
workspaceName: 'test-workspace',
|
|
66
|
+
workspaceVersion: '1.0.0',
|
|
67
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
68
|
+
packageName: 'test-template',
|
|
69
|
+
appName: 'test-template',
|
|
70
|
+
};
|
|
71
|
+
const result = service.validateContext(context);
|
|
72
|
+
expect(result).toBe('app-template');
|
|
73
|
+
});
|
|
74
|
+
// Invalid contexts - test exact error messages
|
|
75
|
+
test('should throw InvalidPluginContextError for workspace-root', () => {
|
|
76
|
+
const context = {
|
|
77
|
+
isValid: true,
|
|
78
|
+
locationType: 'workspace-root',
|
|
79
|
+
workspaceRoot: tempDir,
|
|
80
|
+
workspaceName: 'test-workspace',
|
|
81
|
+
workspaceVersion: '1.0.0',
|
|
82
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
83
|
+
packageName: 'workspace',
|
|
84
|
+
appName: undefined,
|
|
85
|
+
};
|
|
86
|
+
expect(() => service.validateContext(context)).toThrow('plugin:install must be run from within a package directory.');
|
|
87
|
+
});
|
|
88
|
+
test('should throw InvalidPluginContextError for non-workspace', () => {
|
|
89
|
+
const context = {
|
|
90
|
+
isValid: false,
|
|
91
|
+
locationType: 'non-workspace',
|
|
92
|
+
workspaceRoot: tempDir,
|
|
93
|
+
workspaceName: 'test-workspace',
|
|
94
|
+
workspaceVersion: '1.0.0',
|
|
95
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
96
|
+
packageName: 'unknown',
|
|
97
|
+
appName: undefined,
|
|
98
|
+
};
|
|
99
|
+
expect(() => service.validateContext(context)).toThrow('plugin:install must be run from within a package directory.');
|
|
100
|
+
});
|
|
101
|
+
test('should throw InvalidPluginContextError when appName is missing', () => {
|
|
102
|
+
const context = {
|
|
103
|
+
isValid: true,
|
|
104
|
+
locationType: 'workspace-app',
|
|
105
|
+
workspaceRoot: tempDir,
|
|
106
|
+
workspaceName: 'test-workspace',
|
|
107
|
+
workspaceVersion: '1.0.0',
|
|
108
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
109
|
+
packageName: 'test-app',
|
|
110
|
+
appName: undefined,
|
|
111
|
+
};
|
|
112
|
+
expect(() => service.validateContext(context)).toThrow('Could not determine package name. This is a bug. Please report it.');
|
|
113
|
+
});
|
|
114
|
+
test('should throw InvalidPluginContextError when appName is empty string', () => {
|
|
115
|
+
const context = {
|
|
116
|
+
isValid: true,
|
|
117
|
+
locationType: 'workspace-app',
|
|
118
|
+
workspaceRoot: tempDir,
|
|
119
|
+
workspaceName: 'test-workspace',
|
|
120
|
+
workspaceVersion: '1.0.0',
|
|
121
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
122
|
+
packageName: 'test-app',
|
|
123
|
+
appName: '',
|
|
124
|
+
};
|
|
125
|
+
expect(() => service.validateContext(context)).toThrow('Could not determine package name. This is a bug. Please report it.');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('validatePluginTargets', () => {
|
|
129
|
+
// Valid scenarios - verify metadata returned
|
|
130
|
+
test('should return metadata when plugin targets current package type', async () => {
|
|
131
|
+
const pluginDir = path.join(tempDir, 'test-plugin');
|
|
132
|
+
await fs.ensureDir(pluginDir);
|
|
133
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
134
|
+
name: 'test-plugin',
|
|
135
|
+
version: '1.0.0',
|
|
136
|
+
targets: ['app', 'library'],
|
|
137
|
+
});
|
|
138
|
+
const result = await service.validatePluginTargets(pluginDir, 'test-plugin', 'app');
|
|
139
|
+
expect(result).toEqual({
|
|
140
|
+
name: 'test-plugin',
|
|
141
|
+
version: '1.0.0',
|
|
142
|
+
targets: ['app', 'library'],
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
test('should accept plugin with multiple targets including current', async () => {
|
|
146
|
+
const pluginDir = path.join(tempDir, 'multi-target-plugin');
|
|
147
|
+
await fs.ensureDir(pluginDir);
|
|
148
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
149
|
+
name: 'multi-target-plugin',
|
|
150
|
+
version: '2.0.0',
|
|
151
|
+
targets: ['app', 'library', 'plugin', 'app-template'],
|
|
152
|
+
});
|
|
153
|
+
const result = await service.validatePluginTargets(pluginDir, 'multi-target-plugin', 'library');
|
|
154
|
+
expect(result.targets).toContain('library');
|
|
155
|
+
expect(result.targets).toHaveLength(4);
|
|
156
|
+
});
|
|
157
|
+
test('should handle plugin.json with optional fields (pluginDependencies, libraryDependencies)', async () => {
|
|
158
|
+
const pluginDir = path.join(tempDir, 'full-plugin');
|
|
159
|
+
await fs.ensureDir(pluginDir);
|
|
160
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
161
|
+
name: 'full-plugin',
|
|
162
|
+
version: '1.5.0',
|
|
163
|
+
targets: ['app'],
|
|
164
|
+
pluginDependencies: { 'other-plugin': '^1.0.0' },
|
|
165
|
+
libraryDependencies: { react: '^18.0.0' },
|
|
166
|
+
});
|
|
167
|
+
const result = await service.validatePluginTargets(pluginDir, 'full-plugin', 'app');
|
|
168
|
+
expect(result.pluginDependencies).toEqual({ 'other-plugin': '^1.0.0' });
|
|
169
|
+
expect(result.libraryDependencies).toEqual({ react: '^18.0.0' });
|
|
170
|
+
});
|
|
171
|
+
// Error scenarios - test exact error messages
|
|
172
|
+
test('should throw MissingPluginTargetsError when targets is undefined', async () => {
|
|
173
|
+
const pluginDir = path.join(tempDir, 'no-targets-plugin');
|
|
174
|
+
await fs.ensureDir(pluginDir);
|
|
175
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
176
|
+
name: 'no-targets-plugin',
|
|
177
|
+
version: '1.0.0',
|
|
178
|
+
});
|
|
179
|
+
await expect(service.validatePluginTargets(pluginDir, 'no-targets-plugin', 'app')).rejects.toThrow("Plugin 'no-targets-plugin' is missing the required 'targets' field in plugin.json.");
|
|
180
|
+
});
|
|
181
|
+
test('should throw MissingPluginTargetsError when targets is empty array', async () => {
|
|
182
|
+
const pluginDir = path.join(tempDir, 'empty-targets-plugin');
|
|
183
|
+
await fs.ensureDir(pluginDir);
|
|
184
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
185
|
+
name: 'empty-targets-plugin',
|
|
186
|
+
version: '1.0.0',
|
|
187
|
+
targets: [],
|
|
188
|
+
});
|
|
189
|
+
await expect(service.validatePluginTargets(pluginDir, 'empty-targets-plugin', 'app')).rejects.toThrow("Plugin 'empty-targets-plugin' is missing the required 'targets' field in plugin.json.");
|
|
190
|
+
});
|
|
191
|
+
test('should throw error when targets does not include current target', async () => {
|
|
192
|
+
const pluginDir = path.join(tempDir, 'incompatible-plugin');
|
|
193
|
+
await fs.ensureDir(pluginDir);
|
|
194
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
195
|
+
name: 'incompatible-plugin',
|
|
196
|
+
version: '1.0.0',
|
|
197
|
+
targets: ['library', 'plugin'],
|
|
198
|
+
});
|
|
199
|
+
await expect(service.validatePluginTargets(pluginDir, 'incompatible-plugin', 'app')).rejects.toThrow("Plugin 'incompatible-plugin' cannot be installed in a 'app' package.");
|
|
200
|
+
});
|
|
201
|
+
test('should handle missing plugin.json file gracefully', async () => {
|
|
202
|
+
const pluginDir = path.join(tempDir, 'no-plugin-json');
|
|
203
|
+
await fs.ensureDir(pluginDir);
|
|
204
|
+
await expect(service.validatePluginTargets(pluginDir, 'no-plugin-json', 'app')).rejects.toThrow();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('checkExistingInstallation', () => {
|
|
208
|
+
const mockLogger = (message) => {
|
|
209
|
+
/* capture logs */
|
|
210
|
+
};
|
|
211
|
+
// Not installed - return null
|
|
212
|
+
test('should return null when plugin is not installed', async () => {
|
|
213
|
+
const packageDir = path.join(tempDir, 'app1');
|
|
214
|
+
await fs.ensureDir(packageDir);
|
|
215
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
216
|
+
name: 'app1',
|
|
217
|
+
version: '1.0.0',
|
|
218
|
+
launch77: {
|
|
219
|
+
installedPlugins: {},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
|
|
223
|
+
expect(result).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
test('should return null when package.json does not exist', async () => {
|
|
226
|
+
const packageDir = path.join(tempDir, 'nonexistent');
|
|
227
|
+
await fs.ensureDir(packageDir);
|
|
228
|
+
const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
|
|
229
|
+
expect(result).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
test('should return null when launch77 field is missing', async () => {
|
|
232
|
+
const packageDir = path.join(tempDir, 'app2');
|
|
233
|
+
await fs.ensureDir(packageDir);
|
|
234
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
235
|
+
name: 'app2',
|
|
236
|
+
version: '1.0.0',
|
|
237
|
+
});
|
|
238
|
+
const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
|
|
239
|
+
expect(result).toBeNull();
|
|
240
|
+
});
|
|
241
|
+
test('should return null when installedPlugins is missing', async () => {
|
|
242
|
+
const packageDir = path.join(tempDir, 'app3');
|
|
243
|
+
await fs.ensureDir(packageDir);
|
|
244
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
245
|
+
name: 'app3',
|
|
246
|
+
version: '1.0.0',
|
|
247
|
+
launch77: {},
|
|
248
|
+
});
|
|
249
|
+
const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
|
|
250
|
+
expect(result).toBeNull();
|
|
251
|
+
});
|
|
252
|
+
test('should return null when installedPlugins exists but plugin not in it', async () => {
|
|
253
|
+
const packageDir = path.join(tempDir, 'app4');
|
|
254
|
+
await fs.ensureDir(packageDir);
|
|
255
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
256
|
+
name: 'app4',
|
|
257
|
+
version: '1.0.0',
|
|
258
|
+
launch77: {
|
|
259
|
+
installedPlugins: {
|
|
260
|
+
'other-plugin': {
|
|
261
|
+
package: 'other-plugin',
|
|
262
|
+
version: '1.0.0',
|
|
263
|
+
installedAt: '2024-01-01T00:00:00.000Z',
|
|
264
|
+
source: 'local',
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
|
|
270
|
+
expect(result).toBeNull();
|
|
271
|
+
});
|
|
272
|
+
// Already installed - return result object
|
|
273
|
+
test('should return early-exit result with correct structure when plugin is installed', async () => {
|
|
274
|
+
const packageDir = path.join(tempDir, 'app5');
|
|
275
|
+
await fs.ensureDir(packageDir);
|
|
276
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
277
|
+
name: 'app5',
|
|
278
|
+
version: '1.0.0',
|
|
279
|
+
launch77: {
|
|
280
|
+
installedPlugins: {
|
|
281
|
+
'test-plugin': {
|
|
282
|
+
package: 'test-plugin',
|
|
283
|
+
version: '1.5.0',
|
|
284
|
+
installedAt: '2024-01-15T10:30:00.000Z',
|
|
285
|
+
source: 'local',
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
|
|
291
|
+
expect(result).toEqual({
|
|
292
|
+
pluginName: 'test-plugin',
|
|
293
|
+
filesInstalled: false,
|
|
294
|
+
packageJsonUpdated: false,
|
|
295
|
+
dependenciesInstalled: false,
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
test('should log correct message for local plugin (package name matches plugin name)', async () => {
|
|
299
|
+
const packageDir = path.join(tempDir, 'app6');
|
|
300
|
+
await fs.ensureDir(packageDir);
|
|
301
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
302
|
+
name: 'app6',
|
|
303
|
+
version: '1.0.0',
|
|
304
|
+
launch77: {
|
|
305
|
+
installedPlugins: {
|
|
306
|
+
release: {
|
|
307
|
+
package: 'release',
|
|
308
|
+
version: '2.0.0',
|
|
309
|
+
installedAt: '2024-02-01T12:00:00.000Z',
|
|
310
|
+
source: 'local',
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
const logs = [];
|
|
316
|
+
const captureLogger = (message) => logs.push(message);
|
|
317
|
+
const result = await service.checkExistingInstallation('release', packageDir, captureLogger);
|
|
318
|
+
expect(result).not.toBeNull();
|
|
319
|
+
expect(logs.some((log) => log.includes("Plugin 'release' is already installed"))).toBe(true);
|
|
320
|
+
expect(logs.some((log) => log.includes('release') && log.includes('local'))).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
test('should log correct message for npm plugin (package name is scoped)', async () => {
|
|
323
|
+
const packageDir = path.join(tempDir, 'app7');
|
|
324
|
+
await fs.ensureDir(packageDir);
|
|
325
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
326
|
+
name: 'app7',
|
|
327
|
+
version: '1.0.0',
|
|
328
|
+
launch77: {
|
|
329
|
+
installedPlugins: {
|
|
330
|
+
analytics: {
|
|
331
|
+
package: '@myorg/analytics-plugin',
|
|
332
|
+
version: '3.1.0',
|
|
333
|
+
installedAt: '2024-03-01T15:45:00.000Z',
|
|
334
|
+
source: 'npm',
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
const logs = [];
|
|
340
|
+
const captureLogger = (message) => logs.push(message);
|
|
341
|
+
const result = await service.checkExistingInstallation('analytics', packageDir, captureLogger);
|
|
342
|
+
expect(result).not.toBeNull();
|
|
343
|
+
expect(logs.some((log) => log.includes("Plugin 'analytics' is already installed"))).toBe(true);
|
|
344
|
+
expect(logs.some((log) => log.includes('@myorg/analytics-plugin') && log.includes('npm'))).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
// Edge cases
|
|
347
|
+
test('should handle malformed package.json gracefully (invalid JSON)', async () => {
|
|
348
|
+
const packageDir = path.join(tempDir, 'app8');
|
|
349
|
+
await fs.ensureDir(packageDir);
|
|
350
|
+
await fs.writeFile(path.join(packageDir, 'package.json'), '{ invalid json }');
|
|
351
|
+
const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
|
|
352
|
+
expect(result).toBeNull();
|
|
353
|
+
});
|
|
354
|
+
test('should handle package.json read errors (permissions, etc.)', async () => {
|
|
355
|
+
const packageDir = path.join(tempDir, 'app9');
|
|
356
|
+
// Don't create the directory - simulate permission/access error
|
|
357
|
+
const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
|
|
358
|
+
expect(result).toBeNull();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
//# sourceMappingURL=plugin-svc.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-svc.test.js","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"changeset": "changeset",
|
|
20
20
|
"version-packages": "changeset version",
|
|
21
21
|
"release": "turbo run build lint typecheck test && changeset publish",
|
|
22
|
-
"prepare": "husky"
|
|
22
|
+
"prepare": "husky",
|
|
23
|
+
"clean": "rm -rf node_modules apps/*/node_modules libraries/*/node_modules plugins/*/node_modules app-templates/*/node_modules package-lock.json apps/*/package-lock.json libraries/*/package-lock.json plugins/*/package-lock.json app-templates/*/package-lock.json dist apps/*/dist libraries/*/dist plugins/*/dist app-templates/*/dist .next apps/*/.next .turbo apps/*/.turbo libraries/*/.turbo plugins/*/.turbo app-templates/*/.turbo build apps/*/build out apps/*/out coverage apps/*/coverage .cache *.log .eslintcache"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@changesets/cli": "^2.29.8",
|
package/package.json
CHANGED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import * as os from 'os'
|
|
4
|
+
import fs from 'fs-extra'
|
|
5
|
+
import { PluginService } from './plugin-svc.js'
|
|
6
|
+
import type { Launch77Context } from '@launch77/plugin-runtime'
|
|
7
|
+
|
|
8
|
+
describe('PluginService', () => {
|
|
9
|
+
let tempDir: string
|
|
10
|
+
let service: PluginService
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'plugin-svc-test-'))
|
|
14
|
+
service = new PluginService()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await fs.remove(tempDir)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('validateContext', () => {
|
|
22
|
+
// Valid contexts - test return values
|
|
23
|
+
test('should return "app" for workspace-app location', () => {
|
|
24
|
+
const context: Launch77Context = {
|
|
25
|
+
isValid: true,
|
|
26
|
+
locationType: 'workspace-app',
|
|
27
|
+
workspaceRoot: tempDir,
|
|
28
|
+
workspaceName: 'test-workspace',
|
|
29
|
+
workspaceVersion: '1.0.0',
|
|
30
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
31
|
+
packageName: 'test-app',
|
|
32
|
+
appName: 'test-app',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = (service as any).validateContext(context)
|
|
36
|
+
expect(result).toBe('app')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('should return "library" for workspace-library location', () => {
|
|
40
|
+
const context: Launch77Context = {
|
|
41
|
+
isValid: true,
|
|
42
|
+
locationType: 'workspace-library',
|
|
43
|
+
workspaceRoot: tempDir,
|
|
44
|
+
workspaceName: 'test-workspace',
|
|
45
|
+
workspaceVersion: '1.0.0',
|
|
46
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
47
|
+
packageName: 'test-library',
|
|
48
|
+
appName: 'test-library',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = (service as any).validateContext(context)
|
|
52
|
+
expect(result).toBe('library')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('should return "plugin" for workspace-plugin location', () => {
|
|
56
|
+
const context: Launch77Context = {
|
|
57
|
+
isValid: true,
|
|
58
|
+
locationType: 'workspace-plugin',
|
|
59
|
+
workspaceRoot: tempDir,
|
|
60
|
+
workspaceName: 'test-workspace',
|
|
61
|
+
workspaceVersion: '1.0.0',
|
|
62
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
63
|
+
packageName: 'test-plugin',
|
|
64
|
+
appName: 'test-plugin',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = (service as any).validateContext(context)
|
|
68
|
+
expect(result).toBe('plugin')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('should return "app-template" for workspace-app-template location', () => {
|
|
72
|
+
const context: Launch77Context = {
|
|
73
|
+
isValid: true,
|
|
74
|
+
locationType: 'workspace-app-template',
|
|
75
|
+
workspaceRoot: tempDir,
|
|
76
|
+
workspaceName: 'test-workspace',
|
|
77
|
+
workspaceVersion: '1.0.0',
|
|
78
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
79
|
+
packageName: 'test-template',
|
|
80
|
+
appName: 'test-template',
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = (service as any).validateContext(context)
|
|
84
|
+
expect(result).toBe('app-template')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Invalid contexts - test exact error messages
|
|
88
|
+
test('should throw InvalidPluginContextError for workspace-root', () => {
|
|
89
|
+
const context: Launch77Context = {
|
|
90
|
+
isValid: true,
|
|
91
|
+
locationType: 'workspace-root',
|
|
92
|
+
workspaceRoot: tempDir,
|
|
93
|
+
workspaceName: 'test-workspace',
|
|
94
|
+
workspaceVersion: '1.0.0',
|
|
95
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
96
|
+
packageName: 'workspace',
|
|
97
|
+
appName: undefined,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
expect(() => (service as any).validateContext(context)).toThrow('plugin:install must be run from within a package directory.')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('should throw InvalidPluginContextError for non-workspace', () => {
|
|
104
|
+
const context = {
|
|
105
|
+
isValid: false,
|
|
106
|
+
locationType: 'non-workspace' as const,
|
|
107
|
+
workspaceRoot: tempDir,
|
|
108
|
+
workspaceName: 'test-workspace',
|
|
109
|
+
workspaceVersion: '1.0.0',
|
|
110
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
111
|
+
packageName: 'unknown',
|
|
112
|
+
appName: undefined,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
expect(() => (service as any).validateContext(context)).toThrow('plugin:install must be run from within a package directory.')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('should throw InvalidPluginContextError when appName is missing', () => {
|
|
119
|
+
const context: Launch77Context = {
|
|
120
|
+
isValid: true,
|
|
121
|
+
locationType: 'workspace-app',
|
|
122
|
+
workspaceRoot: tempDir,
|
|
123
|
+
workspaceName: 'test-workspace',
|
|
124
|
+
workspaceVersion: '1.0.0',
|
|
125
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
126
|
+
packageName: 'test-app',
|
|
127
|
+
appName: undefined,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
expect(() => (service as any).validateContext(context)).toThrow('Could not determine package name. This is a bug. Please report it.')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('should throw InvalidPluginContextError when appName is empty string', () => {
|
|
134
|
+
const context: Launch77Context = {
|
|
135
|
+
isValid: true,
|
|
136
|
+
locationType: 'workspace-app',
|
|
137
|
+
workspaceRoot: tempDir,
|
|
138
|
+
workspaceName: 'test-workspace',
|
|
139
|
+
workspaceVersion: '1.0.0',
|
|
140
|
+
appsDir: path.join(tempDir, 'apps'),
|
|
141
|
+
packageName: 'test-app',
|
|
142
|
+
appName: '',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
expect(() => (service as any).validateContext(context)).toThrow('Could not determine package name. This is a bug. Please report it.')
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('validatePluginTargets', () => {
|
|
150
|
+
// Valid scenarios - verify metadata returned
|
|
151
|
+
test('should return metadata when plugin targets current package type', async () => {
|
|
152
|
+
const pluginDir = path.join(tempDir, 'test-plugin')
|
|
153
|
+
await fs.ensureDir(pluginDir)
|
|
154
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
155
|
+
name: 'test-plugin',
|
|
156
|
+
version: '1.0.0',
|
|
157
|
+
targets: ['app', 'library'],
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const result = await (service as any).validatePluginTargets(pluginDir, 'test-plugin', 'app')
|
|
161
|
+
expect(result).toEqual({
|
|
162
|
+
name: 'test-plugin',
|
|
163
|
+
version: '1.0.0',
|
|
164
|
+
targets: ['app', 'library'],
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('should accept plugin with multiple targets including current', async () => {
|
|
169
|
+
const pluginDir = path.join(tempDir, 'multi-target-plugin')
|
|
170
|
+
await fs.ensureDir(pluginDir)
|
|
171
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
172
|
+
name: 'multi-target-plugin',
|
|
173
|
+
version: '2.0.0',
|
|
174
|
+
targets: ['app', 'library', 'plugin', 'app-template'],
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const result = await (service as any).validatePluginTargets(pluginDir, 'multi-target-plugin', 'library')
|
|
178
|
+
expect(result.targets).toContain('library')
|
|
179
|
+
expect(result.targets).toHaveLength(4)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('should handle plugin.json with optional fields (pluginDependencies, libraryDependencies)', async () => {
|
|
183
|
+
const pluginDir = path.join(tempDir, 'full-plugin')
|
|
184
|
+
await fs.ensureDir(pluginDir)
|
|
185
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
186
|
+
name: 'full-plugin',
|
|
187
|
+
version: '1.5.0',
|
|
188
|
+
targets: ['app'],
|
|
189
|
+
pluginDependencies: { 'other-plugin': '^1.0.0' },
|
|
190
|
+
libraryDependencies: { react: '^18.0.0' },
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const result = await (service as any).validatePluginTargets(pluginDir, 'full-plugin', 'app')
|
|
194
|
+
expect(result.pluginDependencies).toEqual({ 'other-plugin': '^1.0.0' })
|
|
195
|
+
expect(result.libraryDependencies).toEqual({ react: '^18.0.0' })
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// Error scenarios - test exact error messages
|
|
199
|
+
test('should throw MissingPluginTargetsError when targets is undefined', async () => {
|
|
200
|
+
const pluginDir = path.join(tempDir, 'no-targets-plugin')
|
|
201
|
+
await fs.ensureDir(pluginDir)
|
|
202
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
203
|
+
name: 'no-targets-plugin',
|
|
204
|
+
version: '1.0.0',
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
await expect((service as any).validatePluginTargets(pluginDir, 'no-targets-plugin', 'app')).rejects.toThrow("Plugin 'no-targets-plugin' is missing the required 'targets' field in plugin.json.")
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('should throw MissingPluginTargetsError when targets is empty array', async () => {
|
|
211
|
+
const pluginDir = path.join(tempDir, 'empty-targets-plugin')
|
|
212
|
+
await fs.ensureDir(pluginDir)
|
|
213
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
214
|
+
name: 'empty-targets-plugin',
|
|
215
|
+
version: '1.0.0',
|
|
216
|
+
targets: [],
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
await expect((service as any).validatePluginTargets(pluginDir, 'empty-targets-plugin', 'app')).rejects.toThrow("Plugin 'empty-targets-plugin' is missing the required 'targets' field in plugin.json.")
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('should throw error when targets does not include current target', async () => {
|
|
223
|
+
const pluginDir = path.join(tempDir, 'incompatible-plugin')
|
|
224
|
+
await fs.ensureDir(pluginDir)
|
|
225
|
+
await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
|
|
226
|
+
name: 'incompatible-plugin',
|
|
227
|
+
version: '1.0.0',
|
|
228
|
+
targets: ['library', 'plugin'],
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
await expect((service as any).validatePluginTargets(pluginDir, 'incompatible-plugin', 'app')).rejects.toThrow("Plugin 'incompatible-plugin' cannot be installed in a 'app' package.")
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('should handle missing plugin.json file gracefully', async () => {
|
|
235
|
+
const pluginDir = path.join(tempDir, 'no-plugin-json')
|
|
236
|
+
await fs.ensureDir(pluginDir)
|
|
237
|
+
|
|
238
|
+
await expect((service as any).validatePluginTargets(pluginDir, 'no-plugin-json', 'app')).rejects.toThrow()
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('checkExistingInstallation', () => {
|
|
243
|
+
const mockLogger = (message: string) => {
|
|
244
|
+
/* capture logs */
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Not installed - return null
|
|
248
|
+
test('should return null when plugin is not installed', async () => {
|
|
249
|
+
const packageDir = path.join(tempDir, 'app1')
|
|
250
|
+
await fs.ensureDir(packageDir)
|
|
251
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
252
|
+
name: 'app1',
|
|
253
|
+
version: '1.0.0',
|
|
254
|
+
launch77: {
|
|
255
|
+
installedPlugins: {},
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
|
|
260
|
+
expect(result).toBeNull()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('should return null when package.json does not exist', async () => {
|
|
264
|
+
const packageDir = path.join(tempDir, 'nonexistent')
|
|
265
|
+
await fs.ensureDir(packageDir)
|
|
266
|
+
|
|
267
|
+
const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
|
|
268
|
+
expect(result).toBeNull()
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test('should return null when launch77 field is missing', async () => {
|
|
272
|
+
const packageDir = path.join(tempDir, 'app2')
|
|
273
|
+
await fs.ensureDir(packageDir)
|
|
274
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
275
|
+
name: 'app2',
|
|
276
|
+
version: '1.0.0',
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
|
|
280
|
+
expect(result).toBeNull()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('should return null when installedPlugins is missing', async () => {
|
|
284
|
+
const packageDir = path.join(tempDir, 'app3')
|
|
285
|
+
await fs.ensureDir(packageDir)
|
|
286
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
287
|
+
name: 'app3',
|
|
288
|
+
version: '1.0.0',
|
|
289
|
+
launch77: {},
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
|
|
293
|
+
expect(result).toBeNull()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('should return null when installedPlugins exists but plugin not in it', async () => {
|
|
297
|
+
const packageDir = path.join(tempDir, 'app4')
|
|
298
|
+
await fs.ensureDir(packageDir)
|
|
299
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
300
|
+
name: 'app4',
|
|
301
|
+
version: '1.0.0',
|
|
302
|
+
launch77: {
|
|
303
|
+
installedPlugins: {
|
|
304
|
+
'other-plugin': {
|
|
305
|
+
package: 'other-plugin',
|
|
306
|
+
version: '1.0.0',
|
|
307
|
+
installedAt: '2024-01-01T00:00:00.000Z',
|
|
308
|
+
source: 'local',
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
|
|
315
|
+
expect(result).toBeNull()
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
// Already installed - return result object
|
|
319
|
+
test('should return early-exit result with correct structure when plugin is installed', async () => {
|
|
320
|
+
const packageDir = path.join(tempDir, 'app5')
|
|
321
|
+
await fs.ensureDir(packageDir)
|
|
322
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
323
|
+
name: 'app5',
|
|
324
|
+
version: '1.0.0',
|
|
325
|
+
launch77: {
|
|
326
|
+
installedPlugins: {
|
|
327
|
+
'test-plugin': {
|
|
328
|
+
package: 'test-plugin',
|
|
329
|
+
version: '1.5.0',
|
|
330
|
+
installedAt: '2024-01-15T10:30:00.000Z',
|
|
331
|
+
source: 'local',
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
|
|
338
|
+
expect(result).toEqual({
|
|
339
|
+
pluginName: 'test-plugin',
|
|
340
|
+
filesInstalled: false,
|
|
341
|
+
packageJsonUpdated: false,
|
|
342
|
+
dependenciesInstalled: false,
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('should log correct message for local plugin (package name matches plugin name)', async () => {
|
|
347
|
+
const packageDir = path.join(tempDir, 'app6')
|
|
348
|
+
await fs.ensureDir(packageDir)
|
|
349
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
350
|
+
name: 'app6',
|
|
351
|
+
version: '1.0.0',
|
|
352
|
+
launch77: {
|
|
353
|
+
installedPlugins: {
|
|
354
|
+
release: {
|
|
355
|
+
package: 'release',
|
|
356
|
+
version: '2.0.0',
|
|
357
|
+
installedAt: '2024-02-01T12:00:00.000Z',
|
|
358
|
+
source: 'local',
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
const logs: string[] = []
|
|
365
|
+
const captureLogger = (message: string) => logs.push(message)
|
|
366
|
+
|
|
367
|
+
const result = await (service as any).checkExistingInstallation('release', packageDir, captureLogger)
|
|
368
|
+
expect(result).not.toBeNull()
|
|
369
|
+
expect(logs.some((log) => log.includes("Plugin 'release' is already installed"))).toBe(true)
|
|
370
|
+
expect(logs.some((log) => log.includes('release') && log.includes('local'))).toBe(true)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test('should log correct message for npm plugin (package name is scoped)', async () => {
|
|
374
|
+
const packageDir = path.join(tempDir, 'app7')
|
|
375
|
+
await fs.ensureDir(packageDir)
|
|
376
|
+
await fs.writeJson(path.join(packageDir, 'package.json'), {
|
|
377
|
+
name: 'app7',
|
|
378
|
+
version: '1.0.0',
|
|
379
|
+
launch77: {
|
|
380
|
+
installedPlugins: {
|
|
381
|
+
analytics: {
|
|
382
|
+
package: '@myorg/analytics-plugin',
|
|
383
|
+
version: '3.1.0',
|
|
384
|
+
installedAt: '2024-03-01T15:45:00.000Z',
|
|
385
|
+
source: 'npm',
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
const logs: string[] = []
|
|
392
|
+
const captureLogger = (message: string) => logs.push(message)
|
|
393
|
+
|
|
394
|
+
const result = await (service as any).checkExistingInstallation('analytics', packageDir, captureLogger)
|
|
395
|
+
expect(result).not.toBeNull()
|
|
396
|
+
expect(logs.some((log) => log.includes("Plugin 'analytics' is already installed"))).toBe(true)
|
|
397
|
+
expect(logs.some((log) => log.includes('@myorg/analytics-plugin') && log.includes('npm'))).toBe(true)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// Edge cases
|
|
401
|
+
test('should handle malformed package.json gracefully (invalid JSON)', async () => {
|
|
402
|
+
const packageDir = path.join(tempDir, 'app8')
|
|
403
|
+
await fs.ensureDir(packageDir)
|
|
404
|
+
await fs.writeFile(path.join(packageDir, 'package.json'), '{ invalid json }')
|
|
405
|
+
|
|
406
|
+
const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
|
|
407
|
+
expect(result).toBeNull()
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
test('should handle package.json read errors (permissions, etc.)', async () => {
|
|
411
|
+
const packageDir = path.join(tempDir, 'app9')
|
|
412
|
+
// Don't create the directory - simulate permission/access error
|
|
413
|
+
|
|
414
|
+
const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
|
|
415
|
+
expect(result).toBeNull()
|
|
416
|
+
})
|
|
417
|
+
})
|
|
418
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from 'path'
|
|
2
|
+
import * as fs from 'fs/promises'
|
|
2
3
|
|
|
3
4
|
import chalk from 'chalk'
|
|
4
5
|
import { execa } from 'execa'
|
|
@@ -7,7 +8,7 @@ import { readPluginMetadata } from '@launch77/plugin-runtime'
|
|
|
7
8
|
import { PluginInstallationError, InvalidPluginContextError, createInvalidContextError, MissingPluginTargetsError, createInvalidTargetError, NpmInstallationError, PluginResolutionError } from '../errors/plugin-errors.js'
|
|
8
9
|
import { validatePluginInput, resolvePluginLocation } from '../lib/plugin-resolver.js'
|
|
9
10
|
|
|
10
|
-
import type { Launch77Context, Launch77LocationType } from '@launch77/plugin-runtime'
|
|
11
|
+
import type { Launch77Context, Launch77LocationType, Launch77PackageManifest, InstalledPluginMetadata, PluginMetadata } from '@launch77/plugin-runtime'
|
|
11
12
|
import type { InstallPluginRequest, InstallPluginResult } from '../types/plugin-types.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -30,17 +31,19 @@ function locationTypeToTarget(locationType: Launch77LocationType): string | null
|
|
|
30
31
|
|
|
31
32
|
export class PluginService {
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
+
* Validate that we're in a valid package directory and return the target type
|
|
34
35
|
*/
|
|
35
|
-
|
|
36
|
-
const { pluginName } = request
|
|
37
|
-
|
|
38
|
-
// Must be in a package directory (app, library, plugin, or app-template)
|
|
36
|
+
private validateContext(context: Launch77Context): string {
|
|
39
37
|
const currentTarget = locationTypeToTarget(context.locationType)
|
|
40
38
|
if (!currentTarget) throw createInvalidContextError(context.locationType)
|
|
41
39
|
if (!context.appName) throw new InvalidPluginContextError('Could not determine package name. This is a bug. Please report it.')
|
|
40
|
+
return currentTarget
|
|
41
|
+
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Validate plugin name, resolve its location, and download if needed
|
|
45
|
+
*/
|
|
46
|
+
private async validateAndResolvePlugin(pluginName: string, workspaceRoot: string, logger: (message: string) => void): Promise<{ pluginPath: string; source: 'local' | 'npm'; npmPackage?: string }> {
|
|
44
47
|
logger(chalk.blue(`\nš Resolving plugin "${pluginName}"...`))
|
|
45
48
|
logger(` āā Validating plugin name...`)
|
|
46
49
|
|
|
@@ -50,9 +53,8 @@ export class PluginService {
|
|
|
50
53
|
}
|
|
51
54
|
logger(` ā āā ${chalk.green('ā')} Valid plugin name`)
|
|
52
55
|
|
|
53
|
-
// Step 2: Resolve plugin location
|
|
54
56
|
logger(` āā Checking local workspace: ${chalk.dim(`plugins/${pluginName}`)}`)
|
|
55
|
-
const resolution = await resolvePluginLocation(pluginName,
|
|
57
|
+
const resolution = await resolvePluginLocation(pluginName, workspaceRoot)
|
|
56
58
|
|
|
57
59
|
let pluginPath: string
|
|
58
60
|
|
|
@@ -63,13 +65,22 @@ export class PluginService {
|
|
|
63
65
|
logger(` ā āā ${chalk.dim('Not found locally')}`)
|
|
64
66
|
logger(` āā Resolving to npm package: ${chalk.cyan(resolution.npmPackage)}`)
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
pluginPath = await this.downloadNpmPlugin(resolution.npmPackage!, context.workspaceRoot, logger)
|
|
68
|
+
pluginPath = await this.downloadNpmPlugin(resolution.npmPackage!, workspaceRoot, logger)
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
logger(` āā ${chalk.green('ā')} Plugin resolved\n`)
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
return {
|
|
74
|
+
pluginPath,
|
|
75
|
+
source: resolution.source,
|
|
76
|
+
npmPackage: resolution.npmPackage,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read plugin metadata and validate it supports the current target
|
|
82
|
+
*/
|
|
83
|
+
private async validatePluginTargets(pluginPath: string, pluginName: string, currentTarget: string): Promise<PluginMetadata> {
|
|
73
84
|
const metadata = await readPluginMetadata(pluginPath)
|
|
74
85
|
|
|
75
86
|
if (!metadata.targets || metadata.targets.length === 0) {
|
|
@@ -80,12 +91,55 @@ export class PluginService {
|
|
|
80
91
|
throw createInvalidTargetError(pluginName, currentTarget, metadata.targets)
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
|
|
94
|
+
return metadata
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if plugin is already installed and return early-exit result if so
|
|
99
|
+
*/
|
|
100
|
+
private async checkExistingInstallation(pluginName: string, packagePath: string, logger: (message: string) => void): Promise<InstallPluginResult | null> {
|
|
101
|
+
const existingInstallation = await this.isPluginInstalled(pluginName, packagePath)
|
|
102
|
+
if (existingInstallation) {
|
|
103
|
+
logger(chalk.yellow(`\nā¹ļø Plugin '${pluginName}' is already installed in this package.\n`))
|
|
104
|
+
logger(`Package: ${chalk.cyan(existingInstallation.package)} (${existingInstallation.source})`)
|
|
105
|
+
logger(`Version: ${existingInstallation.version}`)
|
|
106
|
+
logger(`Installed: ${existingInstallation.installedAt}\n`)
|
|
107
|
+
logger(chalk.gray('To reinstall: Remove from package.json launch77.installedPlugins'))
|
|
108
|
+
logger(chalk.gray('(plugin:remove command coming soon)\n'))
|
|
109
|
+
return {
|
|
110
|
+
pluginName,
|
|
111
|
+
filesInstalled: false,
|
|
112
|
+
packageJsonUpdated: false,
|
|
113
|
+
dependenciesInstalled: false,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Install a plugin to the current package
|
|
121
|
+
*/
|
|
122
|
+
async installPlugin(request: InstallPluginRequest, context: Launch77Context, logger: (message: string) => void = console.log): Promise<InstallPluginResult> {
|
|
123
|
+
const { pluginName } = request
|
|
124
|
+
|
|
125
|
+
const currentTarget = this.validateContext(context)
|
|
126
|
+
const { pluginPath, source, npmPackage } = await this.validateAndResolvePlugin(pluginName, context.workspaceRoot, logger)
|
|
127
|
+
const metadata = await this.validatePluginTargets(pluginPath, pluginName, currentTarget)
|
|
128
|
+
|
|
84
129
|
const packagePath = this.getPackagePath(context)
|
|
130
|
+
const earlyExit = await this.checkExistingInstallation(pluginName, packagePath, logger)
|
|
131
|
+
if (earlyExit) return earlyExit
|
|
85
132
|
|
|
86
|
-
// Step 5: Run generator
|
|
87
133
|
await this.runGenerator(pluginPath, packagePath, context)
|
|
88
134
|
|
|
135
|
+
const packageName = source === 'npm' ? npmPackage! : pluginName
|
|
136
|
+
await this.writePluginManifest(packagePath, {
|
|
137
|
+
pluginName,
|
|
138
|
+
packageName,
|
|
139
|
+
version: metadata.version,
|
|
140
|
+
source,
|
|
141
|
+
})
|
|
142
|
+
|
|
89
143
|
return {
|
|
90
144
|
pluginName,
|
|
91
145
|
filesInstalled: true,
|
|
@@ -135,7 +189,9 @@ export class PluginService {
|
|
|
135
189
|
try {
|
|
136
190
|
const generatorPath = path.join(pluginPath, 'dist/generator.js')
|
|
137
191
|
|
|
138
|
-
|
|
192
|
+
const args = [generatorPath, `--appPath=${appPath}`, `--appName=${context.appName}`, `--workspaceName=${context.workspaceName}`, `--pluginPath=${pluginPath}`]
|
|
193
|
+
|
|
194
|
+
await execa('node', args, {
|
|
139
195
|
cwd: pluginPath,
|
|
140
196
|
stdio: 'inherit',
|
|
141
197
|
})
|
|
@@ -143,4 +199,55 @@ export class PluginService {
|
|
|
143
199
|
throw new PluginInstallationError(`Generator failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined)
|
|
144
200
|
}
|
|
145
201
|
}
|
|
202
|
+
|
|
203
|
+
private async isPluginInstalled(pluginName: string, packagePath: string): Promise<InstalledPluginMetadata | null> {
|
|
204
|
+
try {
|
|
205
|
+
const packageJsonPath = path.join(packagePath, 'package.json')
|
|
206
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
|
|
207
|
+
const packageJson = JSON.parse(packageJsonContent)
|
|
208
|
+
|
|
209
|
+
const manifest = packageJson.launch77 as Launch77PackageManifest | undefined
|
|
210
|
+
|
|
211
|
+
if (manifest?.installedPlugins?.[pluginName]) {
|
|
212
|
+
return manifest.installedPlugins[pluginName]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return null
|
|
216
|
+
} catch (error) {
|
|
217
|
+
// If package.json doesn't exist or can't be read, assume not installed
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Write plugin installation metadata to the target package's package.json
|
|
224
|
+
*/
|
|
225
|
+
private async writePluginManifest(packagePath: string, installationInfo: { pluginName: string; packageName: string; version: string; source: 'local' | 'npm' }): Promise<void> {
|
|
226
|
+
try {
|
|
227
|
+
const packageJsonPath = path.join(packagePath, 'package.json')
|
|
228
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
|
|
229
|
+
const packageJson = JSON.parse(packageJsonContent)
|
|
230
|
+
|
|
231
|
+
if (!packageJson.launch77) {
|
|
232
|
+
packageJson.launch77 = {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!packageJson.launch77.installedPlugins) {
|
|
236
|
+
packageJson.launch77.installedPlugins = {}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const manifest = packageJson.launch77 as Launch77PackageManifest
|
|
240
|
+
|
|
241
|
+
manifest.installedPlugins![installationInfo.pluginName] = {
|
|
242
|
+
package: installationInfo.packageName,
|
|
243
|
+
version: installationInfo.version,
|
|
244
|
+
installedAt: new Date().toISOString(),
|
|
245
|
+
source: installationInfo.source,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8')
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw new PluginInstallationError(`Failed to write plugin manifest: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
146
253
|
}
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"changeset": "changeset",
|
|
20
20
|
"version-packages": "changeset version",
|
|
21
21
|
"release": "turbo run build lint typecheck test && changeset publish",
|
|
22
|
-
"prepare": "husky"
|
|
22
|
+
"prepare": "husky",
|
|
23
|
+
"clean": "rm -rf node_modules apps/*/node_modules libraries/*/node_modules plugins/*/node_modules app-templates/*/node_modules package-lock.json apps/*/package-lock.json libraries/*/package-lock.json plugins/*/package-lock.json app-templates/*/package-lock.json dist apps/*/dist libraries/*/dist plugins/*/dist app-templates/*/dist .next apps/*/.next .turbo apps/*/.turbo libraries/*/.turbo plugins/*/.turbo app-templates/*/.turbo build apps/*/build out apps/*/out coverage apps/*/coverage .cache *.log .eslintcache"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@changesets/cli": "^2.29.8",
|