@launch77/cli 1.4.0 ā 1.4.3
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 +21 -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 +3 -3
- 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
|
@@ -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",
|