@launch77/cli 1.4.3 → 1.5.0
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 +19 -0
- package/dist/cli.js +1 -4
- package/dist/cli.js.map +1 -1
- package/dist/infrastructure/npm-package.d.ts +42 -0
- package/dist/infrastructure/npm-package.d.ts.map +1 -0
- package/dist/infrastructure/npm-package.js +46 -0
- package/dist/infrastructure/npm-package.js.map +1 -0
- package/dist/infrastructure/package-resolver.d.ts +117 -0
- package/dist/infrastructure/package-resolver.d.ts.map +1 -0
- package/dist/infrastructure/package-resolver.js +170 -0
- package/dist/infrastructure/package-resolver.js.map +1 -0
- package/dist/infrastructure/package-resolver.test.d.ts +2 -0
- package/dist/infrastructure/package-resolver.test.d.ts.map +1 -0
- package/dist/infrastructure/package-resolver.test.js +251 -0
- package/dist/infrastructure/package-resolver.test.js.map +1 -0
- package/dist/infrastructure/template.d.ts +0 -1
- package/dist/infrastructure/template.d.ts.map +1 -1
- package/dist/infrastructure/template.js.map +1 -1
- package/dist/modules/app/commands/create-app.d.ts.map +1 -1
- package/dist/modules/app/commands/create-app.js +14 -15
- package/dist/modules/app/commands/create-app.js.map +1 -1
- package/dist/modules/app/commands/delete-app.js +1 -1
- package/dist/modules/app/commands/delete-app.js.map +1 -1
- package/dist/modules/app/commands/validate-manifest.d.ts.map +1 -1
- package/dist/modules/app/commands/validate-manifest.js +0 -1
- package/dist/modules/app/commands/validate-manifest.js.map +1 -1
- package/dist/modules/app/index.d.ts +1 -2
- package/dist/modules/app/index.d.ts.map +1 -1
- package/dist/modules/app/index.js +0 -1
- package/dist/modules/app/index.js.map +1 -1
- package/dist/modules/app/lib/app-template-resolver.d.ts +14 -0
- package/dist/modules/app/lib/app-template-resolver.d.ts.map +1 -0
- package/dist/modules/app/lib/app-template-resolver.js +51 -0
- package/dist/modules/app/lib/app-template-resolver.js.map +1 -0
- package/dist/modules/app/lib/manifest-schema.d.ts +0 -7
- package/dist/modules/app/lib/manifest-schema.d.ts.map +1 -1
- package/dist/modules/app/lib/manifest-schema.js +0 -2
- package/dist/modules/app/lib/manifest-schema.js.map +1 -1
- package/dist/modules/app/services/app-svc.d.ts +2 -1
- package/dist/modules/app/services/app-svc.d.ts.map +1 -1
- package/dist/modules/app/services/app-svc.js +77 -12
- package/dist/modules/app/services/app-svc.js.map +1 -1
- package/dist/modules/app/services/manifest-svc.d.ts +2 -2
- package/dist/modules/app/services/manifest-svc.d.ts.map +1 -1
- package/dist/modules/app/services/manifest-svc.js +9 -50
- package/dist/modules/app/services/manifest-svc.js.map +1 -1
- package/dist/modules/app/types/app-types.d.ts +2 -5
- package/dist/modules/app/types/app-types.d.ts.map +1 -1
- package/dist/modules/app/types/app-types.js +1 -4
- package/dist/modules/app/types/app-types.js.map +1 -1
- package/dist/modules/catalog/commands/scan.js +3 -3
- package/dist/modules/catalog/commands/scan.js.map +1 -1
- package/dist/modules/catalog/services/catalog-svc.d.ts +2 -2
- package/dist/modules/catalog/services/catalog-svc.d.ts.map +1 -1
- package/dist/modules/catalog/services/catalog-svc.js +2 -2
- package/dist/modules/catalog/services/catalog-svc.js.map +1 -1
- package/dist/modules/deploy/commands/deploy-init-action.d.ts.map +1 -1
- package/dist/modules/deploy/commands/deploy-init-action.js +21 -20
- package/dist/modules/deploy/commands/deploy-init-action.js.map +1 -1
- package/dist/modules/deploy/commands/deploy-logs-action.d.ts.map +1 -1
- package/dist/modules/deploy/commands/deploy-logs-action.js +16 -13
- package/dist/modules/deploy/commands/deploy-logs-action.js.map +1 -1
- package/dist/modules/deploy/commands/deploy-status-action.d.ts.map +1 -1
- package/dist/modules/deploy/commands/deploy-status-action.js +16 -13
- package/dist/modules/deploy/commands/deploy-status-action.js.map +1 -1
- package/dist/modules/plugin/lib/plugin-resolver.d.ts +10 -72
- package/dist/modules/plugin/lib/plugin-resolver.d.ts.map +1 -1
- package/dist/modules/plugin/lib/plugin-resolver.js +23 -115
- package/dist/modules/plugin/lib/plugin-resolver.js.map +1 -1
- package/dist/modules/plugin/lib/plugin-resolver.test.js +45 -39
- package/dist/modules/plugin/lib/plugin-resolver.test.js.map +1 -1
- package/dist/modules/plugin/services/plugin-svc.d.ts +2 -0
- package/dist/modules/plugin/services/plugin-svc.d.ts.map +1 -1
- package/dist/modules/plugin/services/plugin-svc.js +24 -19
- package/dist/modules/plugin/services/plugin-svc.js.map +1 -1
- package/dist/modules/plugin/services/plugin-svc.test.js +12 -6
- package/dist/modules/plugin/services/plugin-svc.test.js.map +1 -1
- package/dist/templates/plugin/package.json.hbs +1 -1
- package/dist/templates/plugin/plugin.json.hbs +1 -3
- package/dist/utils/validation.d.ts +3 -7
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +7 -31
- package/dist/utils/validation.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +1 -4
- package/src/infrastructure/npm-package.ts +73 -0
- package/src/infrastructure/package-resolver.test.ts +313 -0
- package/src/infrastructure/package-resolver.ts +223 -0
- package/src/infrastructure/template.ts +0 -1
- package/src/modules/app/commands/create-app.ts +14 -18
- package/src/modules/app/commands/delete-app.ts +1 -1
- package/src/modules/app/commands/validate-manifest.ts +0 -1
- package/src/modules/app/index.ts +1 -2
- package/src/modules/app/lib/app-template-resolver.ts +56 -0
- package/src/modules/app/lib/manifest-schema.ts +0 -5
- package/src/modules/app/services/app-svc.ts +89 -13
- package/src/modules/app/services/manifest-svc.ts +8 -57
- package/src/modules/app/types/app-types.ts +2 -9
- package/src/modules/catalog/commands/scan.ts +3 -3
- package/src/modules/catalog/services/catalog-svc.ts +4 -4
- package/src/modules/deploy/commands/deploy-init-action.ts +23 -20
- package/src/modules/deploy/commands/deploy-logs-action.ts +19 -13
- package/src/modules/deploy/commands/deploy-status-action.ts +19 -13
- package/src/modules/plugin/lib/plugin-resolver.test.ts +48 -39
- package/src/modules/plugin/lib/plugin-resolver.ts +22 -141
- package/src/modules/plugin/services/plugin-svc.test.ts +12 -6
- package/src/modules/plugin/services/plugin-svc.ts +27 -18
- package/src/utils/validation.ts +10 -38
- package/templates/plugin/package.json.hbs +1 -1
- package/templates/plugin/plugin.json.hbs +1 -3
- package/dist/modules/app/commands/generate-manifest.d.ts +0 -3
- package/dist/modules/app/commands/generate-manifest.d.ts.map +0 -1
- package/dist/modules/app/commands/generate-manifest.js +0 -62
- package/dist/modules/app/commands/generate-manifest.js.map +0 -1
- package/dist/modules/startup/commands/create-startup.d.ts +0 -3
- package/dist/modules/startup/commands/create-startup.d.ts.map +0 -1
- package/dist/modules/startup/commands/create-startup.js +0 -43
- package/dist/modules/startup/commands/create-startup.js.map +0 -1
- package/dist/modules/startup/errors/startup-errors.d.ts +0 -13
- package/dist/modules/startup/errors/startup-errors.d.ts.map +0 -1
- package/dist/modules/startup/errors/startup-errors.js +0 -25
- package/dist/modules/startup/errors/startup-errors.js.map +0 -1
- package/dist/modules/startup/index.d.ts +0 -5
- package/dist/modules/startup/index.d.ts.map +0 -1
- package/dist/modules/startup/index.js +0 -7
- package/dist/modules/startup/index.js.map +0 -1
- package/dist/modules/startup/services/startup-service.d.ts +0 -7
- package/dist/modules/startup/services/startup-service.d.ts.map +0 -1
- package/dist/modules/startup/services/startup-service.js +0 -43
- package/dist/modules/startup/services/startup-service.js.map +0 -1
- package/dist/modules/startup/types/startup-types.d.ts +0 -8
- package/dist/modules/startup/types/startup-types.d.ts.map +0 -1
- package/dist/modules/startup/types/startup-types.js +0 -2
- package/dist/modules/startup/types/startup-types.js.map +0 -1
- package/dist/modules/startup/utils/startup-validators.d.ts +0 -8
- package/dist/modules/startup/utils/startup-validators.d.ts.map +0 -1
- package/dist/modules/startup/utils/startup-validators.js +0 -17
- package/dist/modules/startup/utils/startup-validators.js.map +0 -1
- package/dist/templates/startup/apps/.gitkeep +0 -8
- package/dist/utils/monorepo.d.ts +0 -19
- package/dist/utils/monorepo.d.ts.map +0 -1
- package/dist/utils/monorepo.js +0 -100
- package/dist/utils/monorepo.js.map +0 -1
- package/src/modules/app/commands/generate-manifest.ts +0 -75
- package/src/modules/startup/commands/create-startup.ts +0 -53
- package/src/modules/startup/errors/startup-errors.ts +0 -23
- package/src/modules/startup/index.ts +0 -11
- package/src/modules/startup/services/startup-service.ts +0 -57
- package/src/modules/startup/types/startup-types.ts +0 -8
- package/src/modules/startup/utils/startup-validators.ts +0 -19
- package/src/utils/monorepo.ts +0 -137
- package/templates/startup/apps/.gitkeep +0 -8
|
@@ -1,99 +1,106 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import * as path from 'path'
|
|
3
1
|
import * as os from 'os'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
|
|
4
4
|
import fs from 'fs-extra'
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { PluginResolver } from './plugin-resolver.js'
|
|
7
8
|
|
|
8
9
|
describe('Plugin Resolver', () => {
|
|
9
|
-
|
|
10
|
+
let resolver: PluginResolver
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
resolver = new PluginResolver()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('validateInput', () => {
|
|
10
17
|
it('should accept valid unscoped plugin names', () => {
|
|
11
|
-
expect(
|
|
12
|
-
expect(
|
|
13
|
-
expect(
|
|
18
|
+
expect(resolver.validateInput('release')).toEqual({ isValid: true })
|
|
19
|
+
expect(resolver.validateInput('my-plugin')).toEqual({ isValid: true })
|
|
20
|
+
expect(resolver.validateInput('analytics-v2')).toEqual({ isValid: true })
|
|
14
21
|
})
|
|
15
22
|
|
|
16
23
|
it('should accept valid scoped npm packages', () => {
|
|
17
|
-
expect(
|
|
18
|
-
expect(
|
|
19
|
-
expect(
|
|
24
|
+
expect(resolver.validateInput('@ibm/plugin-name')).toEqual({ isValid: true })
|
|
25
|
+
expect(resolver.validateInput('@launch77-shared/plugin-release')).toEqual({ isValid: true })
|
|
26
|
+
expect(resolver.validateInput('@org/analytics')).toEqual({ isValid: true })
|
|
20
27
|
})
|
|
21
28
|
|
|
22
29
|
it('should reject empty names', () => {
|
|
23
|
-
const result =
|
|
30
|
+
const result = resolver.validateInput('')
|
|
24
31
|
expect(result.isValid).toBe(false)
|
|
25
32
|
expect(result.error).toBeDefined()
|
|
26
33
|
})
|
|
27
34
|
|
|
28
35
|
it('should reject whitespace-only names', () => {
|
|
29
|
-
const result =
|
|
36
|
+
const result = resolver.validateInput(' ')
|
|
30
37
|
expect(result.isValid).toBe(false)
|
|
31
38
|
expect(result.error).toBeDefined()
|
|
32
39
|
})
|
|
33
40
|
|
|
34
41
|
it('should reject invalid scoped packages', () => {
|
|
35
|
-
const result1 =
|
|
42
|
+
const result1 = resolver.validateInput('@invalid')
|
|
36
43
|
expect(result1.isValid).toBe(false)
|
|
37
44
|
expect(result1.error).toBeDefined()
|
|
38
45
|
|
|
39
|
-
const result2 =
|
|
46
|
+
const result2 = resolver.validateInput('@/package')
|
|
40
47
|
expect(result2.isValid).toBe(false)
|
|
41
48
|
expect(result2.error).toBeDefined()
|
|
42
49
|
|
|
43
|
-
const result3 =
|
|
50
|
+
const result3 = resolver.validateInput('@org/')
|
|
44
51
|
expect(result3.isValid).toBe(false)
|
|
45
52
|
expect(result3.error).toBeDefined()
|
|
46
53
|
})
|
|
47
54
|
|
|
48
55
|
it('should reject names with uppercase letters', () => {
|
|
49
|
-
const result =
|
|
56
|
+
const result = resolver.validateInput('MyPlugin')
|
|
50
57
|
expect(result.isValid).toBe(false)
|
|
51
58
|
expect(result.error).toContain('lowercase')
|
|
52
59
|
})
|
|
53
60
|
|
|
54
61
|
it('should reject names starting with numbers', () => {
|
|
55
|
-
const result =
|
|
62
|
+
const result = resolver.validateInput('123plugin')
|
|
56
63
|
expect(result.isValid).toBe(false)
|
|
57
64
|
expect(result.error).toBeDefined()
|
|
58
65
|
})
|
|
59
66
|
|
|
60
67
|
it('should reject names with special characters', () => {
|
|
61
|
-
const result1 =
|
|
68
|
+
const result1 = resolver.validateInput('plugin_name')
|
|
62
69
|
expect(result1.isValid).toBe(false)
|
|
63
70
|
|
|
64
|
-
const result2 =
|
|
71
|
+
const result2 = resolver.validateInput('plugin.name')
|
|
65
72
|
expect(result2.isValid).toBe(false)
|
|
66
73
|
|
|
67
|
-
const result3 =
|
|
74
|
+
const result3 = resolver.validateInput('plugin name')
|
|
68
75
|
expect(result3.isValid).toBe(false)
|
|
69
76
|
})
|
|
70
77
|
|
|
71
78
|
it('should trim whitespace before validation', () => {
|
|
72
|
-
expect(
|
|
73
|
-
expect(
|
|
79
|
+
expect(resolver.validateInput(' release ')).toEqual({ isValid: true })
|
|
80
|
+
expect(resolver.validateInput(' @ibm/analytics ')).toEqual({ isValid: true })
|
|
74
81
|
})
|
|
75
82
|
})
|
|
76
83
|
|
|
77
84
|
describe('toNpmPackageName', () => {
|
|
78
85
|
it('should prefix unscoped names with @launch77-shared/plugin-', () => {
|
|
79
|
-
expect(toNpmPackageName('release')).toBe('@launch77-shared/plugin-release')
|
|
80
|
-
expect(toNpmPackageName('my-plugin')).toBe('@launch77-shared/plugin-my-plugin')
|
|
81
|
-
expect(toNpmPackageName('analytics-v2')).toBe('@launch77-shared/plugin-analytics-v2')
|
|
86
|
+
expect(resolver.toNpmPackageName('release')).toBe('@launch77-shared/plugin-release')
|
|
87
|
+
expect(resolver.toNpmPackageName('my-plugin')).toBe('@launch77-shared/plugin-my-plugin')
|
|
88
|
+
expect(resolver.toNpmPackageName('analytics-v2')).toBe('@launch77-shared/plugin-analytics-v2')
|
|
82
89
|
})
|
|
83
90
|
|
|
84
91
|
it('should return scoped packages as-is', () => {
|
|
85
|
-
expect(toNpmPackageName('@ibm/analytics')).toBe('@ibm/analytics')
|
|
86
|
-
expect(toNpmPackageName('@launch77-shared/plugin-release')).toBe('@launch77-shared/plugin-release')
|
|
87
|
-
expect(toNpmPackageName('@org/package')).toBe('@org/package')
|
|
92
|
+
expect(resolver.toNpmPackageName('@ibm/analytics')).toBe('@ibm/analytics')
|
|
93
|
+
expect(resolver.toNpmPackageName('@launch77-shared/plugin-release')).toBe('@launch77-shared/plugin-release')
|
|
94
|
+
expect(resolver.toNpmPackageName('@org/package')).toBe('@org/package')
|
|
88
95
|
})
|
|
89
96
|
|
|
90
97
|
it('should trim whitespace', () => {
|
|
91
|
-
expect(toNpmPackageName(' release ')).toBe('@launch77-shared/plugin-release')
|
|
92
|
-
expect(toNpmPackageName(' @ibm/analytics ')).toBe('@ibm/analytics')
|
|
98
|
+
expect(resolver.toNpmPackageName(' release ')).toBe('@launch77-shared/plugin-release')
|
|
99
|
+
expect(resolver.toNpmPackageName(' @ibm/analytics ')).toBe('@ibm/analytics')
|
|
93
100
|
})
|
|
94
101
|
})
|
|
95
102
|
|
|
96
|
-
describe('
|
|
103
|
+
describe('resolveLocation', () => {
|
|
97
104
|
let tempDir: string
|
|
98
105
|
|
|
99
106
|
beforeEach(async () => {
|
|
@@ -113,19 +120,21 @@ describe('Plugin Resolver', () => {
|
|
|
113
120
|
await fs.ensureDir(pluginPath)
|
|
114
121
|
await fs.ensureDir(path.join(pluginPath, 'dist'))
|
|
115
122
|
await fs.writeFile(path.join(pluginPath, 'plugin.json'), JSON.stringify({ name: 'my-plugin', version: '1.0.0' }))
|
|
123
|
+
await fs.writeFile(path.join(pluginPath, 'package.json'), JSON.stringify({ name: 'my-plugin', version: '0.0.1' }))
|
|
116
124
|
await fs.writeFile(path.join(pluginPath, 'dist/generator.js'), 'console.log("test")')
|
|
117
125
|
|
|
118
|
-
const result = await
|
|
126
|
+
const result = await resolver.resolveLocation('my-plugin', tempDir)
|
|
119
127
|
|
|
120
128
|
expect(result).toEqual({
|
|
121
129
|
source: 'local',
|
|
122
130
|
resolvedName: 'my-plugin',
|
|
123
131
|
localPath: pluginPath,
|
|
132
|
+
version: '0.0.1',
|
|
124
133
|
})
|
|
125
134
|
})
|
|
126
135
|
|
|
127
136
|
it('should resolve to npm if local plugin does not exist', async () => {
|
|
128
|
-
const result = await
|
|
137
|
+
const result = await resolver.resolveLocation('release', tempDir)
|
|
129
138
|
|
|
130
139
|
expect(result).toEqual({
|
|
131
140
|
source: 'npm',
|
|
@@ -141,7 +150,7 @@ describe('Plugin Resolver', () => {
|
|
|
141
150
|
await fs.ensureDir(path.join(pluginPath, 'dist'))
|
|
142
151
|
await fs.writeFile(path.join(pluginPath, 'dist/generator.js'), 'console.log("test")')
|
|
143
152
|
|
|
144
|
-
const result = await
|
|
153
|
+
const result = await resolver.resolveLocation('incomplete', tempDir)
|
|
145
154
|
|
|
146
155
|
expect(result).toEqual({
|
|
147
156
|
source: 'npm',
|
|
@@ -156,7 +165,7 @@ describe('Plugin Resolver', () => {
|
|
|
156
165
|
await fs.ensureDir(pluginPath)
|
|
157
166
|
await fs.writeFile(path.join(pluginPath, 'plugin.json'), JSON.stringify({ name: 'incomplete', version: '1.0.0' }))
|
|
158
167
|
|
|
159
|
-
const result = await
|
|
168
|
+
const result = await resolver.resolveLocation('incomplete', tempDir)
|
|
160
169
|
|
|
161
170
|
expect(result).toEqual({
|
|
162
171
|
source: 'npm',
|
|
@@ -170,7 +179,7 @@ describe('Plugin Resolver', () => {
|
|
|
170
179
|
const pluginPath = path.join(tempDir, 'plugins', '@ibm')
|
|
171
180
|
await fs.ensureDir(pluginPath)
|
|
172
181
|
|
|
173
|
-
const result = await
|
|
182
|
+
const result = await resolver.resolveLocation('@ibm/analytics', tempDir)
|
|
174
183
|
|
|
175
184
|
expect(result).toEqual({
|
|
176
185
|
source: 'npm',
|
|
@@ -180,7 +189,7 @@ describe('Plugin Resolver', () => {
|
|
|
180
189
|
})
|
|
181
190
|
|
|
182
191
|
it('should resolve scoped @launch77-shared packages to npm', async () => {
|
|
183
|
-
const result = await
|
|
192
|
+
const result = await resolver.resolveLocation('@launch77-shared/plugin-release', tempDir)
|
|
184
193
|
|
|
185
194
|
expect(result).toEqual({
|
|
186
195
|
source: 'npm',
|
|
@@ -193,7 +202,7 @@ describe('Plugin Resolver', () => {
|
|
|
193
202
|
// Remove plugins directory
|
|
194
203
|
await fs.remove(path.join(tempDir, 'plugins'))
|
|
195
204
|
|
|
196
|
-
const result = await
|
|
205
|
+
const result = await resolver.resolveLocation('release', tempDir)
|
|
197
206
|
|
|
198
207
|
expect(result).toEqual({
|
|
199
208
|
source: 'npm',
|
|
@@ -203,7 +212,7 @@ describe('Plugin Resolver', () => {
|
|
|
203
212
|
})
|
|
204
213
|
|
|
205
214
|
it('should trim whitespace from plugin names', async () => {
|
|
206
|
-
const result = await
|
|
215
|
+
const result = await resolver.resolveLocation(' release ', tempDir)
|
|
207
216
|
|
|
208
217
|
expect(result).toEqual({
|
|
209
218
|
source: 'npm',
|
|
@@ -1,160 +1,41 @@
|
|
|
1
1
|
import * as path from 'path'
|
|
2
2
|
|
|
3
3
|
import fs from 'fs-extra'
|
|
4
|
-
import { parsePluginName, isValidNpmPackageName } from '@launch77/plugin-runtime'
|
|
5
4
|
|
|
6
|
-
import
|
|
5
|
+
import { PackageResolver } from '../../../infrastructure/package-resolver.js'
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
|
-
* Plugin
|
|
10
|
-
*/
|
|
11
|
-
export interface PluginResolution {
|
|
12
|
-
/** The source of the plugin */
|
|
13
|
-
source: 'local' | 'npm'
|
|
14
|
-
/** The resolved name/package to use */
|
|
15
|
-
resolvedName: string
|
|
16
|
-
/** The local path if source is 'local' */
|
|
17
|
-
localPath?: string
|
|
18
|
-
/** The npm package name if source is 'npm' */
|
|
19
|
-
npmPackage?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Validate plugin input name
|
|
24
|
-
*
|
|
25
|
-
* Accepts:
|
|
26
|
-
* - Unscoped names (e.g., "release", "my-plugin")
|
|
27
|
-
* - Scoped npm packages (e.g., "@ibm/plugin-name")
|
|
28
|
-
*
|
|
29
|
-
* Rejects:
|
|
30
|
-
* - Invalid formats
|
|
31
|
-
* - Empty strings
|
|
32
|
-
* - Names with invalid characters
|
|
33
|
-
*
|
|
34
|
-
* @param name - The plugin name to validate
|
|
35
|
-
* @returns ValidationResult with isValid and optional error message
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* validatePluginInput('release') // { isValid: true }
|
|
39
|
-
* validatePluginInput('@ibm/analytics') // { isValid: true }
|
|
40
|
-
* validatePluginInput('@invalid') // { isValid: false, error: '...' }
|
|
41
|
-
*/
|
|
42
|
-
export function validatePluginInput(name: string): ValidationResult {
|
|
43
|
-
if (!name || name.trim().length === 0) {
|
|
44
|
-
return {
|
|
45
|
-
isValid: false,
|
|
46
|
-
error: 'Plugin name cannot be empty',
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const trimmedName = name.trim()
|
|
51
|
-
|
|
52
|
-
// Parse the name to determine type and validate
|
|
53
|
-
const parsed = parsePluginName(trimmedName)
|
|
54
|
-
|
|
55
|
-
if (!parsed.isValid) {
|
|
56
|
-
return {
|
|
57
|
-
isValid: false,
|
|
58
|
-
error: parsed.error,
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// If it's scoped, it must be a valid npm package name
|
|
63
|
-
if (parsed.type === 'scoped') {
|
|
64
|
-
return isValidNpmPackageName(trimmedName)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Unscoped names are valid for both local and npm
|
|
68
|
-
return { isValid: true }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Convert an unscoped plugin name to an npm package name
|
|
73
|
-
*
|
|
74
|
-
* Rules:
|
|
75
|
-
* - Unscoped names: prefix with @launch77-shared/plugin-
|
|
76
|
-
* - Scoped names: use as-is
|
|
8
|
+
* Plugin resolver implementation
|
|
77
9
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* @example
|
|
82
|
-
* toNpmPackageName('release') // '@launch77-shared/plugin-release'
|
|
83
|
-
* toNpmPackageName('@ibm/analytics') // '@ibm/analytics'
|
|
10
|
+
* Resolves plugins from:
|
|
11
|
+
* - Local workspace plugins/ directory
|
|
12
|
+
* - npm packages with @launch77-shared/plugin- prefix
|
|
84
13
|
*/
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// If already scoped, use as-is
|
|
89
|
-
if (trimmedName.startsWith('@')) {
|
|
90
|
-
return trimmedName
|
|
14
|
+
export class PluginResolver extends PackageResolver {
|
|
15
|
+
protected getFolderName(): string {
|
|
16
|
+
return 'plugins'
|
|
91
17
|
}
|
|
92
18
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Resolve plugin location from name
|
|
99
|
-
*
|
|
100
|
-
* Resolution order:
|
|
101
|
-
* 1. Check local workspace plugins directory
|
|
102
|
-
* 2. Resolve to npm package name
|
|
103
|
-
*
|
|
104
|
-
* @param name - The plugin name to resolve
|
|
105
|
-
* @param workspaceRoot - The workspace root directory
|
|
106
|
-
* @returns PluginResolution with source and resolved location
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* // Local plugin found
|
|
110
|
-
* await resolvePluginLocation('my-plugin', '/workspace')
|
|
111
|
-
* // { source: 'local', resolvedName: 'my-plugin', localPath: '/workspace/plugins/my-plugin' }
|
|
112
|
-
*
|
|
113
|
-
* // Not found locally, resolve to npm
|
|
114
|
-
* await resolvePluginLocation('release', '/workspace')
|
|
115
|
-
* // { source: 'npm', resolvedName: 'release', npmPackage: '@launch77-shared/plugin-release' }
|
|
116
|
-
*
|
|
117
|
-
* // Scoped package always resolves to npm
|
|
118
|
-
* await resolvePluginLocation('@ibm/analytics', '/workspace')
|
|
119
|
-
* // { source: 'npm', resolvedName: '@ibm/analytics', npmPackage: '@ibm/analytics' }
|
|
120
|
-
*/
|
|
121
|
-
export async function resolvePluginLocation(name: string, workspaceRoot: string): Promise<PluginResolution> {
|
|
122
|
-
const trimmedName = name.trim()
|
|
123
|
-
const parsed = parsePluginName(trimmedName)
|
|
124
|
-
|
|
125
|
-
// If scoped, always use npm (local plugins are never scoped)
|
|
126
|
-
if (parsed.type === 'scoped') {
|
|
127
|
-
return {
|
|
128
|
-
source: 'npm',
|
|
129
|
-
resolvedName: trimmedName,
|
|
130
|
-
npmPackage: trimmedName,
|
|
131
|
-
}
|
|
19
|
+
protected getPackagePrefix(): string {
|
|
20
|
+
return '@launch77-shared/plugin-'
|
|
132
21
|
}
|
|
133
22
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const localExists = await fs.pathExists(localPath)
|
|
137
|
-
|
|
138
|
-
if (localExists) {
|
|
139
|
-
// Verify it's a valid plugin (has plugin.json and dist/generator.js)
|
|
23
|
+
protected async verify(localPath: string): Promise<boolean> {
|
|
24
|
+
// Verify it's a valid plugin (has plugin.json, dist/generator.js, and package.json with version)
|
|
140
25
|
const hasPluginJson = await fs.pathExists(path.join(localPath, 'plugin.json'))
|
|
141
26
|
const hasGenerator = await fs.pathExists(path.join(localPath, 'dist/generator.js'))
|
|
27
|
+
const hasPackageJson = await fs.pathExists(path.join(localPath, 'package.json'))
|
|
142
28
|
|
|
143
|
-
if (hasPluginJson
|
|
144
|
-
return
|
|
145
|
-
source: 'local',
|
|
146
|
-
resolvedName: trimmedName,
|
|
147
|
-
localPath,
|
|
148
|
-
}
|
|
29
|
+
if (!hasPluginJson || !hasGenerator || !hasPackageJson) {
|
|
30
|
+
return false
|
|
149
31
|
}
|
|
150
|
-
}
|
|
151
32
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
33
|
+
// Verify package.json has a version field
|
|
34
|
+
try {
|
|
35
|
+
const packageJson = await fs.readJson(path.join(localPath, 'package.json'))
|
|
36
|
+
return !!packageJson.version
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
159
40
|
}
|
|
160
41
|
}
|
|
@@ -159,9 +159,9 @@ describe('PluginService', () => {
|
|
|
159
159
|
|
|
160
160
|
const result = await (service as any).validatePluginTargets(pluginDir, 'test-plugin', 'app')
|
|
161
161
|
expect(result).toEqual({
|
|
162
|
-
name: 'test-plugin',
|
|
163
|
-
version: '1.0.0',
|
|
164
162
|
targets: ['app', 'library'],
|
|
163
|
+
pluginDependencies: undefined,
|
|
164
|
+
libraryDependencies: undefined,
|
|
165
165
|
})
|
|
166
166
|
})
|
|
167
167
|
|
|
@@ -175,8 +175,11 @@ describe('PluginService', () => {
|
|
|
175
175
|
})
|
|
176
176
|
|
|
177
177
|
const result = await (service as any).validatePluginTargets(pluginDir, 'multi-target-plugin', 'library')
|
|
178
|
-
expect(result
|
|
179
|
-
|
|
178
|
+
expect(result).toEqual({
|
|
179
|
+
targets: ['app', 'library', 'plugin', 'app-template'],
|
|
180
|
+
pluginDependencies: undefined,
|
|
181
|
+
libraryDependencies: undefined,
|
|
182
|
+
})
|
|
180
183
|
})
|
|
181
184
|
|
|
182
185
|
test('should handle plugin.json with optional fields (pluginDependencies, libraryDependencies)', async () => {
|
|
@@ -191,8 +194,11 @@ describe('PluginService', () => {
|
|
|
191
194
|
})
|
|
192
195
|
|
|
193
196
|
const result = await (service as any).validatePluginTargets(pluginDir, 'full-plugin', 'app')
|
|
194
|
-
expect(result
|
|
195
|
-
|
|
197
|
+
expect(result).toEqual({
|
|
198
|
+
targets: ['app'],
|
|
199
|
+
pluginDependencies: { 'other-plugin': '^1.0.0' },
|
|
200
|
+
libraryDependencies: { react: '^18.0.0' },
|
|
201
|
+
})
|
|
196
202
|
})
|
|
197
203
|
|
|
198
204
|
// Error scenarios - test exact error messages
|
|
@@ -5,8 +5,9 @@ import chalk from 'chalk'
|
|
|
5
5
|
import { execa } from 'execa'
|
|
6
6
|
import { readPluginMetadata } from '@launch77/plugin-runtime'
|
|
7
7
|
|
|
8
|
+
import { downloadNpmPackage } from '../../../infrastructure/npm-package.js'
|
|
8
9
|
import { PluginInstallationError, InvalidPluginContextError, createInvalidContextError, MissingPluginTargetsError, createInvalidTargetError, NpmInstallationError, PluginResolutionError } from '../errors/plugin-errors.js'
|
|
9
|
-
import {
|
|
10
|
+
import { PluginResolver } from '../lib/plugin-resolver.js'
|
|
10
11
|
|
|
11
12
|
import type { Launch77Context, Launch77LocationType, Launch77PackageManifest, InstalledPluginMetadata, PluginMetadata } from '@launch77/plugin-runtime'
|
|
12
13
|
import type { InstallPluginRequest, InstallPluginResult } from '../types/plugin-types.js'
|
|
@@ -30,6 +31,12 @@ function locationTypeToTarget(locationType: Launch77LocationType): string | null
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export class PluginService {
|
|
34
|
+
private pluginResolver: PluginResolver
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
this.pluginResolver = new PluginResolver()
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
/**
|
|
34
41
|
* Validate that we're in a valid package directory and return the target type
|
|
35
42
|
*/
|
|
@@ -43,29 +50,37 @@ export class PluginService {
|
|
|
43
50
|
/**
|
|
44
51
|
* Validate plugin name, resolve its location, and download if needed
|
|
45
52
|
*/
|
|
46
|
-
private async validateAndResolvePlugin(pluginName: string, workspaceRoot: string, logger: (message: string) => void): Promise<{ pluginPath: string; source: 'local' | 'npm'; npmPackage?: string }> {
|
|
53
|
+
private async validateAndResolvePlugin(pluginName: string, workspaceRoot: string, logger: (message: string) => void): Promise<{ pluginPath: string; source: 'local' | 'npm'; npmPackage?: string; version: string }> {
|
|
47
54
|
logger(chalk.blue(`\n🔍 Resolving plugin "${pluginName}"...`))
|
|
48
55
|
logger(` ├─ Validating plugin name...`)
|
|
49
56
|
|
|
50
|
-
const validation =
|
|
57
|
+
const validation = this.pluginResolver.validateInput(pluginName)
|
|
51
58
|
if (!validation.isValid) {
|
|
52
59
|
throw new PluginResolutionError(pluginName, validation.error || 'Invalid plugin name')
|
|
53
60
|
}
|
|
54
61
|
logger(` │ └─ ${chalk.green('✓')} Valid plugin name`)
|
|
55
62
|
|
|
56
63
|
logger(` ├─ Checking local workspace: ${chalk.dim(`plugins/${pluginName}`)}`)
|
|
57
|
-
const resolution = await
|
|
64
|
+
const resolution = await this.pluginResolver.resolveLocation(pluginName, workspaceRoot)
|
|
58
65
|
|
|
59
66
|
let pluginPath: string
|
|
67
|
+
let version: string
|
|
60
68
|
|
|
61
69
|
if (resolution.source === 'local') {
|
|
62
70
|
logger(` │ └─ ${chalk.green('✓')} Found local plugin`)
|
|
63
71
|
pluginPath = resolution.localPath!
|
|
72
|
+
version = resolution.version! // Local plugins always have version after verification
|
|
64
73
|
} else {
|
|
65
74
|
logger(` │ └─ ${chalk.dim('Not found locally')}`)
|
|
66
75
|
logger(` ├─ Resolving to npm package: ${chalk.cyan(resolution.npmPackage)}`)
|
|
67
76
|
|
|
68
77
|
pluginPath = await this.downloadNpmPlugin(resolution.npmPackage!, workspaceRoot, logger)
|
|
78
|
+
|
|
79
|
+
// Read version from downloaded package
|
|
80
|
+
const packageJsonPath = path.join(pluginPath, 'package.json')
|
|
81
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
|
|
82
|
+
const packageJson = JSON.parse(packageJsonContent)
|
|
83
|
+
version = packageJson.version
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
logger(` └─ ${chalk.green('✓')} Plugin resolved\n`)
|
|
@@ -74,6 +89,7 @@ export class PluginService {
|
|
|
74
89
|
pluginPath,
|
|
75
90
|
source: resolution.source,
|
|
76
91
|
npmPackage: resolution.npmPackage,
|
|
92
|
+
version,
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
95
|
|
|
@@ -123,7 +139,7 @@ export class PluginService {
|
|
|
123
139
|
const { pluginName } = request
|
|
124
140
|
|
|
125
141
|
const currentTarget = this.validateContext(context)
|
|
126
|
-
const { pluginPath, source, npmPackage } = await this.validateAndResolvePlugin(pluginName, context.workspaceRoot, logger)
|
|
142
|
+
const { pluginPath, source, npmPackage, version } = await this.validateAndResolvePlugin(pluginName, context.workspaceRoot, logger)
|
|
127
143
|
const metadata = await this.validatePluginTargets(pluginPath, pluginName, currentTarget)
|
|
128
144
|
|
|
129
145
|
const packagePath = this.getPackagePath(context)
|
|
@@ -136,7 +152,7 @@ export class PluginService {
|
|
|
136
152
|
await this.writePluginManifest(packagePath, {
|
|
137
153
|
pluginName,
|
|
138
154
|
packageName,
|
|
139
|
-
version
|
|
155
|
+
version,
|
|
140
156
|
source,
|
|
141
157
|
})
|
|
142
158
|
|
|
@@ -154,19 +170,12 @@ export class PluginService {
|
|
|
154
170
|
private async downloadNpmPlugin(npmPackage: string, workspaceRoot: string, logger: (message: string) => void): Promise<string> {
|
|
155
171
|
logger(` └─ Installing from npm: ${chalk.cyan(npmPackage)}...`)
|
|
156
172
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
stdio: 'pipe', // Capture output for clean logging
|
|
162
|
-
})
|
|
173
|
+
const result = await downloadNpmPackage({
|
|
174
|
+
packageName: npmPackage,
|
|
175
|
+
workspaceRoot,
|
|
176
|
+
})
|
|
163
177
|
|
|
164
|
-
|
|
165
|
-
const pluginPath = path.join(workspaceRoot, 'node_modules', npmPackage)
|
|
166
|
-
return pluginPath
|
|
167
|
-
} catch (error) {
|
|
168
|
-
throw new NpmInstallationError(npmPackage, error instanceof Error ? error : undefined)
|
|
169
|
-
}
|
|
178
|
+
return result.packagePath
|
|
170
179
|
}
|
|
171
180
|
|
|
172
181
|
private getPackagePath(context: Launch77Context): string {
|
package/src/utils/validation.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as path from 'path'
|
|
|
2
2
|
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { Launch77Context } from '@launch77/plugin-runtime'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Validation result with helpful error messages
|
|
@@ -12,50 +12,22 @@ export interface ValidationResult {
|
|
|
12
12
|
errorMessage?: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* Validate that we're in a startup context (for create-app)
|
|
17
|
-
*/
|
|
18
|
-
export function validateStartupContext(context: MonorepoContext): ValidationResult {
|
|
19
|
-
if (!context.isValid) {
|
|
20
|
-
return {
|
|
21
|
-
valid: false,
|
|
22
|
-
errorMessage: 'Must be run from within a Launch77 monorepo (could not find .launch-monorepo-root.md)',
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (context.location !== 'startup-root' && context.location !== 'startup-app') {
|
|
27
|
-
return {
|
|
28
|
-
valid: false,
|
|
29
|
-
errorMessage: `Command must be run from a startup directory.\n\n` + `${chalk.gray('Current location:')} ${context.location}\n` + `${chalk.gray('Expected:')} Within startups/<startup-name>/\n\n` + `${chalk.yellow('Navigate to a startup first:')}\n` + ` cd startups/<startup-name>/\n\n` + `${chalk.yellow('Or create a startup:')}\n` + ` launch77 create-startup <startup-name>`,
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!context.appsDir) {
|
|
34
|
-
return {
|
|
35
|
-
valid: false,
|
|
36
|
-
errorMessage: `Could not determine apps directory for location: ${context.location}\n` + `This is a bug. Please report it.`,
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return { valid: true }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
15
|
/**
|
|
44
16
|
* Validate that we're inside an app directory (for plugin:install)
|
|
45
17
|
*/
|
|
46
|
-
export function validateAppContext(context:
|
|
18
|
+
export function validateAppContext(context: Launch77Context): ValidationResult {
|
|
47
19
|
if (!context.isValid) {
|
|
48
20
|
return {
|
|
49
21
|
valid: false,
|
|
50
|
-
errorMessage: 'Must be run from within a Launch77
|
|
22
|
+
errorMessage: 'Must be run from within a Launch77 workspace (could not find .launch77/workspace.json)',
|
|
51
23
|
}
|
|
52
24
|
}
|
|
53
25
|
|
|
54
|
-
// Must be inside an app
|
|
55
|
-
if (context.
|
|
26
|
+
// Must be inside an app
|
|
27
|
+
if (context.locationType !== 'workspace-app') {
|
|
56
28
|
return {
|
|
57
29
|
valid: false,
|
|
58
|
-
errorMessage: `plugin:install must be run from within an app directory.\n\n` + `${chalk.gray('Current location:')} ${context.
|
|
30
|
+
errorMessage: `plugin:install must be run from within an app directory.\n\n` + `${chalk.gray('Current location:')} ${context.locationType}\n` + `${chalk.gray('Expected:')} apps/<app-name>/\n\n` + `${chalk.yellow('Navigate to an app directory:')}\n` + ` cd apps/<app-name>/\n\n` + `${chalk.yellow('Or create an app first:')}\n` + ` launch77 app:create <template> <app-name>`,
|
|
59
31
|
}
|
|
60
32
|
}
|
|
61
33
|
|
|
@@ -73,16 +45,16 @@ export function validateAppContext(context: MonorepoContext): ValidationResult {
|
|
|
73
45
|
* Get the app directory path
|
|
74
46
|
* Returns null if not in an app context
|
|
75
47
|
*/
|
|
76
|
-
export function getAppDirectory(context:
|
|
77
|
-
if (context.
|
|
48
|
+
export function getAppDirectory(context: Launch77Context): string | null {
|
|
49
|
+
if (context.locationType !== 'workspace-app') {
|
|
78
50
|
return null
|
|
79
51
|
}
|
|
80
52
|
|
|
81
|
-
if (!context.
|
|
53
|
+
if (!context.workspaceRoot || !context.appName) {
|
|
82
54
|
return null
|
|
83
55
|
}
|
|
84
56
|
|
|
85
|
-
return path.join(context.
|
|
57
|
+
return path.join(context.workspaceRoot, 'apps', context.appName)
|
|
86
58
|
}
|
|
87
59
|
|
|
88
60
|
/**
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"generate-manifest.d.ts","sourceRoot":"","sources":["../../../../src/modules/app/commands/generate-manifest.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAQnC,wBAAgB,uBAAuB,IAAI,OAAO,CA8DjD"}
|