@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
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import * as os from 'os'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
|
|
4
|
+
import fs from 'fs-extra'
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { PackageResolver } from './package-resolver.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Test implementation of PackageResolver for unit testing
|
|
11
|
+
*/
|
|
12
|
+
class TestPackageResolver extends PackageResolver {
|
|
13
|
+
protected getFolderName(): string {
|
|
14
|
+
return 'test-packages'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected getPackagePrefix(): string {
|
|
18
|
+
return '@test-org/test-'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected async verify(localPath: string): Promise<boolean> {
|
|
22
|
+
// Verify by checking for package.json
|
|
23
|
+
const packageJsonPath = path.join(localPath, 'package.json')
|
|
24
|
+
return await fs.pathExists(packageJsonPath)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('PackageResolver', () => {
|
|
29
|
+
let resolver: TestPackageResolver
|
|
30
|
+
let tempDir: string
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
resolver = new TestPackageResolver()
|
|
34
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'package-resolver-test-'))
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
await fs.remove(tempDir)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('validateInput', () => {
|
|
42
|
+
describe('valid inputs', () => {
|
|
43
|
+
it('should accept valid unscoped package names', () => {
|
|
44
|
+
const result = resolver.validateInput('release')
|
|
45
|
+
expect(result.isValid).toBe(true)
|
|
46
|
+
expect(result.error).toBeUndefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should accept unscoped names with hyphens', () => {
|
|
50
|
+
const result = resolver.validateInput('my-package')
|
|
51
|
+
expect(result.isValid).toBe(true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should accept unscoped names with numbers', () => {
|
|
55
|
+
const result = resolver.validateInput('package-123')
|
|
56
|
+
expect(result.isValid).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should accept valid scoped packages', () => {
|
|
60
|
+
const result = resolver.validateInput('@ibm/analytics')
|
|
61
|
+
expect(result.isValid).toBe(true)
|
|
62
|
+
expect(result.error).toBeUndefined()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should accept scoped packages with hyphens', () => {
|
|
66
|
+
const result = resolver.validateInput('@launch77-shared/plugin-release')
|
|
67
|
+
expect(result.isValid).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should trim whitespace from valid names', () => {
|
|
71
|
+
const result = resolver.validateInput(' release ')
|
|
72
|
+
expect(result.isValid).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should trim whitespace from scoped names', () => {
|
|
76
|
+
const result = resolver.validateInput(' @ibm/analytics ')
|
|
77
|
+
expect(result.isValid).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('invalid inputs', () => {
|
|
82
|
+
it('should reject empty string', () => {
|
|
83
|
+
const result = resolver.validateInput('')
|
|
84
|
+
expect(result.isValid).toBe(false)
|
|
85
|
+
expect(result.error).toBe('Package name cannot be empty')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should reject whitespace-only string', () => {
|
|
89
|
+
const result = resolver.validateInput(' ')
|
|
90
|
+
expect(result.isValid).toBe(false)
|
|
91
|
+
expect(result.error).toBe('Package name cannot be empty')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should reject invalid scoped package without name', () => {
|
|
95
|
+
const result = resolver.validateInput('@invalid')
|
|
96
|
+
expect(result.isValid).toBe(false)
|
|
97
|
+
expect(result.error).toContain('Scoped package must be in format @org/package')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should reject invalid scoped package without scope', () => {
|
|
101
|
+
const result = resolver.validateInput('@/package')
|
|
102
|
+
expect(result.isValid).toBe(false)
|
|
103
|
+
expect(result.error).toBeDefined()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should reject invalid scoped package with missing name after slash', () => {
|
|
107
|
+
const result = resolver.validateInput('@org/')
|
|
108
|
+
expect(result.isValid).toBe(false)
|
|
109
|
+
expect(result.error).toContain('Scoped package must be in format @org/package')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should reject names with uppercase letters', () => {
|
|
113
|
+
const result = resolver.validateInput('MyPackage')
|
|
114
|
+
expect(result.isValid).toBe(false)
|
|
115
|
+
expect(result.error).toBeDefined()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should reject names starting with numbers', () => {
|
|
119
|
+
const result = resolver.validateInput('123package')
|
|
120
|
+
expect(result.isValid).toBe(false)
|
|
121
|
+
expect(result.error).toBeDefined()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should reject names with underscores', () => {
|
|
125
|
+
const result = resolver.validateInput('my_package')
|
|
126
|
+
expect(result.isValid).toBe(false)
|
|
127
|
+
expect(result.error).toBeDefined()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should reject names with spaces', () => {
|
|
131
|
+
const result = resolver.validateInput('my package')
|
|
132
|
+
expect(result.isValid).toBe(false)
|
|
133
|
+
expect(result.error).toBeDefined()
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('toNpmPackageName', () => {
|
|
139
|
+
describe('unscoped names', () => {
|
|
140
|
+
it('should prefix unscoped package names', () => {
|
|
141
|
+
const result = resolver.toNpmPackageName('release')
|
|
142
|
+
expect(result).toBe('@test-org/test-release')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should prefix unscoped names with hyphens', () => {
|
|
146
|
+
const result = resolver.toNpmPackageName('my-package')
|
|
147
|
+
expect(result).toBe('@test-org/test-my-package')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should trim whitespace from unscoped names', () => {
|
|
151
|
+
const result = resolver.toNpmPackageName(' release ')
|
|
152
|
+
expect(result).toBe('@test-org/test-release')
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('scoped names', () => {
|
|
157
|
+
it('should return scoped packages as-is', () => {
|
|
158
|
+
const result = resolver.toNpmPackageName('@ibm/analytics')
|
|
159
|
+
expect(result).toBe('@ibm/analytics')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should return scoped packages with hyphens as-is', () => {
|
|
163
|
+
const result = resolver.toNpmPackageName('@launch77-shared/plugin-release')
|
|
164
|
+
expect(result).toBe('@launch77-shared/plugin-release')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should trim whitespace from scoped names', () => {
|
|
168
|
+
const result = resolver.toNpmPackageName(' @ibm/analytics ')
|
|
169
|
+
expect(result).toBe('@ibm/analytics')
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('resolveLocation', () => {
|
|
175
|
+
describe('local resolution (unscoped names)', () => {
|
|
176
|
+
it('should resolve to local when valid package exists', async () => {
|
|
177
|
+
const packageName = 'my-local-package'
|
|
178
|
+
const localPath = path.join(tempDir, 'test-packages', packageName)
|
|
179
|
+
await fs.ensureDir(localPath)
|
|
180
|
+
await fs.writeJSON(path.join(localPath, 'package.json'), { name: packageName, version: '0.0.1' })
|
|
181
|
+
|
|
182
|
+
const result = await resolver.resolveLocation(packageName, tempDir)
|
|
183
|
+
|
|
184
|
+
expect(result.source).toBe('local')
|
|
185
|
+
expect(result.resolvedName).toBe(packageName)
|
|
186
|
+
expect(result.localPath).toBe(localPath)
|
|
187
|
+
expect(result.npmPackage).toBeUndefined()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should fall back to npm when local directory exists but is invalid', async () => {
|
|
191
|
+
const packageName = 'invalid-local-package'
|
|
192
|
+
const localPath = path.join(tempDir, 'test-packages', packageName)
|
|
193
|
+
await fs.ensureDir(localPath)
|
|
194
|
+
// Don't create package.json, making it invalid
|
|
195
|
+
|
|
196
|
+
const result = await resolver.resolveLocation(packageName, tempDir)
|
|
197
|
+
|
|
198
|
+
expect(result.source).toBe('npm')
|
|
199
|
+
expect(result.resolvedName).toBe(packageName)
|
|
200
|
+
expect(result.npmPackage).toBe('@test-org/test-invalid-local-package')
|
|
201
|
+
expect(result.localPath).toBeUndefined()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should fall back to npm when local directory does not exist', async () => {
|
|
205
|
+
const packageName = 'nonexistent-package'
|
|
206
|
+
|
|
207
|
+
const result = await resolver.resolveLocation(packageName, tempDir)
|
|
208
|
+
|
|
209
|
+
expect(result.source).toBe('npm')
|
|
210
|
+
expect(result.resolvedName).toBe(packageName)
|
|
211
|
+
expect(result.npmPackage).toBe('@test-org/test-nonexistent-package')
|
|
212
|
+
expect(result.localPath).toBeUndefined()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should trim whitespace from package names', async () => {
|
|
216
|
+
const packageName = 'my-package'
|
|
217
|
+
const localPath = path.join(tempDir, 'test-packages', packageName)
|
|
218
|
+
await fs.ensureDir(localPath)
|
|
219
|
+
await fs.writeJSON(path.join(localPath, 'package.json'), { name: packageName, version: '0.0.1' })
|
|
220
|
+
|
|
221
|
+
const result = await resolver.resolveLocation(' my-package ', tempDir)
|
|
222
|
+
|
|
223
|
+
expect(result.source).toBe('local')
|
|
224
|
+
expect(result.resolvedName).toBe(packageName)
|
|
225
|
+
expect(result.localPath).toBe(localPath)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe('npm resolution (scoped names)', () => {
|
|
230
|
+
it('should always resolve scoped packages to npm', async () => {
|
|
231
|
+
const packageName = '@ibm/analytics'
|
|
232
|
+
|
|
233
|
+
const result = await resolver.resolveLocation(packageName, tempDir)
|
|
234
|
+
|
|
235
|
+
expect(result.source).toBe('npm')
|
|
236
|
+
expect(result.resolvedName).toBe(packageName)
|
|
237
|
+
expect(result.npmPackage).toBe(packageName)
|
|
238
|
+
expect(result.localPath).toBeUndefined()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('should resolve scoped packages to npm even if local directory exists', async () => {
|
|
242
|
+
const packageName = '@ibm/analytics'
|
|
243
|
+
// Create a local directory with the scoped package name (edge case)
|
|
244
|
+
const localPath = path.join(tempDir, 'test-packages', '@ibm/analytics')
|
|
245
|
+
await fs.ensureDir(localPath)
|
|
246
|
+
await fs.writeJSON(path.join(localPath, 'package.json'), { name: packageName })
|
|
247
|
+
|
|
248
|
+
const result = await resolver.resolveLocation(packageName, tempDir)
|
|
249
|
+
|
|
250
|
+
expect(result.source).toBe('npm')
|
|
251
|
+
expect(result.resolvedName).toBe(packageName)
|
|
252
|
+
expect(result.npmPackage).toBe(packageName)
|
|
253
|
+
expect(result.localPath).toBeUndefined()
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should handle scoped packages with hyphens', async () => {
|
|
257
|
+
const packageName = '@launch77-shared/plugin-release'
|
|
258
|
+
|
|
259
|
+
const result = await resolver.resolveLocation(packageName, tempDir)
|
|
260
|
+
|
|
261
|
+
expect(result.source).toBe('npm')
|
|
262
|
+
expect(result.resolvedName).toBe(packageName)
|
|
263
|
+
expect(result.npmPackage).toBe(packageName)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should trim whitespace from scoped package names', async () => {
|
|
267
|
+
const packageName = '@ibm/analytics'
|
|
268
|
+
|
|
269
|
+
const result = await resolver.resolveLocation(' @ibm/analytics ', tempDir)
|
|
270
|
+
|
|
271
|
+
expect(result.source).toBe('npm')
|
|
272
|
+
expect(result.resolvedName).toBe(packageName)
|
|
273
|
+
expect(result.npmPackage).toBe(packageName)
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
describe('edge cases', () => {
|
|
278
|
+
it('should handle empty workspace test-packages directory', async () => {
|
|
279
|
+
// Ensure the directory exists but is empty
|
|
280
|
+
await fs.ensureDir(path.join(tempDir, 'test-packages'))
|
|
281
|
+
|
|
282
|
+
const result = await resolver.resolveLocation('some-package', tempDir)
|
|
283
|
+
|
|
284
|
+
expect(result.source).toBe('npm')
|
|
285
|
+
expect(result.resolvedName).toBe('some-package')
|
|
286
|
+
expect(result.npmPackage).toBe('@test-org/test-some-package')
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('should handle missing workspace test-packages directory', async () => {
|
|
290
|
+
// Don't create the test-packages directory at all
|
|
291
|
+
|
|
292
|
+
const result = await resolver.resolveLocation('some-package', tempDir)
|
|
293
|
+
|
|
294
|
+
expect(result.source).toBe('npm')
|
|
295
|
+
expect(result.resolvedName).toBe('some-package')
|
|
296
|
+
expect(result.npmPackage).toBe('@test-org/test-some-package')
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('should handle different workspace root paths', async () => {
|
|
300
|
+
const customRoot = path.join(tempDir, 'custom', 'workspace')
|
|
301
|
+
const packageName = 'my-package'
|
|
302
|
+
const localPath = path.join(customRoot, 'test-packages', packageName)
|
|
303
|
+
await fs.ensureDir(localPath)
|
|
304
|
+
await fs.writeJSON(path.join(localPath, 'package.json'), { name: packageName, version: '0.0.1' })
|
|
305
|
+
|
|
306
|
+
const result = await resolver.resolveLocation(packageName, customRoot)
|
|
307
|
+
|
|
308
|
+
expect(result.source).toBe('local')
|
|
309
|
+
expect(result.localPath).toBe(localPath)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
})
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import * as path from 'path'
|
|
2
|
+
|
|
3
|
+
import { parsePluginName, isValidNpmPackageName } from '@launch77/plugin-runtime'
|
|
4
|
+
import fs from 'fs-extra'
|
|
5
|
+
|
|
6
|
+
import type { ValidationResult } from '@launch77/plugin-runtime'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base interface for package resolution results
|
|
10
|
+
*/
|
|
11
|
+
export interface PackageResolution {
|
|
12
|
+
/** The source of the package */
|
|
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
|
+
/** The version from package.json (required for local packages, undefined for npm until installed) */
|
|
21
|
+
version?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Abstract base class for resolving Launch77 packages
|
|
26
|
+
*
|
|
27
|
+
* Provides generic resolution logic for finding packages in:
|
|
28
|
+
* 1. Local workspace directory (e.g., plugins/, app-templates/)
|
|
29
|
+
* 2. npm packages with configured prefix (e.g., @launch77-shared/plugin-*, @launch77-shared/app-template-*)
|
|
30
|
+
*
|
|
31
|
+
* Concrete implementations must provide:
|
|
32
|
+
* - Folder name for local resolution
|
|
33
|
+
* - Package prefix for npm resolution
|
|
34
|
+
* - Verification logic to validate local packages
|
|
35
|
+
*/
|
|
36
|
+
export abstract class PackageResolver {
|
|
37
|
+
/**
|
|
38
|
+
* Get the local folder name where packages of this type are stored
|
|
39
|
+
* @example 'plugins' or 'app-templates'
|
|
40
|
+
*/
|
|
41
|
+
protected abstract getFolderName(): string
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the npm package prefix for unscoped packages
|
|
45
|
+
* @example '@launch77-shared/plugin-' or '@launch77-shared/app-template-'
|
|
46
|
+
*/
|
|
47
|
+
protected abstract getPackagePrefix(): string
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Verify that a local package is valid and complete
|
|
51
|
+
* @param localPath - The local directory path to verify
|
|
52
|
+
* @returns true if the package is valid, false otherwise
|
|
53
|
+
*/
|
|
54
|
+
protected abstract verify(localPath: string): Promise<boolean>
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate package input name
|
|
58
|
+
*
|
|
59
|
+
* Accepts:
|
|
60
|
+
* - Unscoped names (e.g., "release", "my-package")
|
|
61
|
+
* - Scoped npm packages (e.g., "@ibm/package-name")
|
|
62
|
+
*
|
|
63
|
+
* Rejects:
|
|
64
|
+
* - Invalid formats
|
|
65
|
+
* - Empty strings
|
|
66
|
+
* - Names with invalid characters
|
|
67
|
+
*
|
|
68
|
+
* @param name - The package name to validate
|
|
69
|
+
* @returns ValidationResult with isValid and optional error message
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* validateInput('release') // { isValid: true }
|
|
73
|
+
* validateInput('@ibm/analytics') // { isValid: true }
|
|
74
|
+
* validateInput('@invalid') // { isValid: false, error: '...' }
|
|
75
|
+
*/
|
|
76
|
+
validateInput(name: string): ValidationResult {
|
|
77
|
+
if (!name || name.trim().length === 0) {
|
|
78
|
+
return {
|
|
79
|
+
isValid: false,
|
|
80
|
+
error: 'Package name cannot be empty',
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const trimmedName = name.trim()
|
|
85
|
+
|
|
86
|
+
// Parse the name to determine type and validate
|
|
87
|
+
//TODO: move/rename parsePluginName() to parsePackageName()
|
|
88
|
+
const parsed = parsePluginName(trimmedName)
|
|
89
|
+
|
|
90
|
+
if (!parsed.isValid) {
|
|
91
|
+
return {
|
|
92
|
+
isValid: false,
|
|
93
|
+
error: parsed.error,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// If it's scoped, it must be a valid npm package name
|
|
98
|
+
if (parsed.type === 'scoped') {
|
|
99
|
+
return isValidNpmPackageName(trimmedName)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Unscoped names are valid for both local and npm
|
|
103
|
+
return { isValid: true }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert an unscoped package name to an npm package name
|
|
108
|
+
*
|
|
109
|
+
* Rules:
|
|
110
|
+
* - Unscoped names: prefix with configured package prefix
|
|
111
|
+
* - Scoped names: use as-is
|
|
112
|
+
*
|
|
113
|
+
* @param name - The package name (must be validated first)
|
|
114
|
+
* @returns The npm package name
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* toNpmPackageName('release') // '@launch77-shared/plugin-release' (for PluginResolver)
|
|
118
|
+
* toNpmPackageName('@ibm/analytics') // '@ibm/analytics'
|
|
119
|
+
*/
|
|
120
|
+
toNpmPackageName(name: string): string {
|
|
121
|
+
const trimmedName = name.trim()
|
|
122
|
+
|
|
123
|
+
// If already scoped, use as-is
|
|
124
|
+
if (trimmedName.startsWith('@')) {
|
|
125
|
+
return trimmedName
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Otherwise, convert using configured prefix
|
|
129
|
+
return `${this.getPackagePrefix()}${trimmedName}`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Read version from package.json
|
|
134
|
+
* @param packagePath - The path to the package directory
|
|
135
|
+
* @returns The version string from package.json
|
|
136
|
+
* @throws If package.json doesn't exist, can't be read, or is missing the version field
|
|
137
|
+
*/
|
|
138
|
+
private async readVersion(packagePath: string): Promise<string> {
|
|
139
|
+
const packageJsonPath = path.join(packagePath, 'package.json')
|
|
140
|
+
try {
|
|
141
|
+
const packageJson = await fs.readJson(packageJsonPath)
|
|
142
|
+
if (!packageJson.version) {
|
|
143
|
+
throw new Error(`Invalid package structure: package.json at ${packagePath} is missing required version field. ` + `All Launch77 packages must include a valid package.json with a version field.`)
|
|
144
|
+
}
|
|
145
|
+
return packageJson.version
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Re-throw our own error messages
|
|
148
|
+
if (error instanceof Error && error.message.includes('Invalid package structure')) {
|
|
149
|
+
throw error
|
|
150
|
+
}
|
|
151
|
+
// File not found or invalid JSON
|
|
152
|
+
throw new Error(`Invalid package structure: package.json not found or invalid at ${packagePath}. ` + `All Launch77 packages must include a valid package.json with a version field.`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolve package location from name
|
|
158
|
+
*
|
|
159
|
+
* Resolution order:
|
|
160
|
+
* 1. Check local workspace directory (configured by getFolderName())
|
|
161
|
+
* 2. Verify local package is valid (using verify())
|
|
162
|
+
* 3. Fall back to npm package name (with configured prefix)
|
|
163
|
+
* 4. Read version from package.json (if available)
|
|
164
|
+
*
|
|
165
|
+
* @param name - The package name to resolve
|
|
166
|
+
* @param workspaceRoot - The workspace root directory
|
|
167
|
+
* @returns PackageResolution with source, resolved location, and version
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* // Local package found
|
|
171
|
+
* await resolveLocation('my-package', '/workspace')
|
|
172
|
+
* // { source: 'local', resolvedName: 'my-package', localPath: '/workspace/plugins/my-package', version: '1.0.0' }
|
|
173
|
+
*
|
|
174
|
+
* // Not found locally, resolve to npm
|
|
175
|
+
* await resolveLocation('release', '/workspace')
|
|
176
|
+
* // { source: 'npm', resolvedName: 'release', npmPackage: '@launch77-shared/plugin-release' }
|
|
177
|
+
*
|
|
178
|
+
* // Scoped package always resolves to npm
|
|
179
|
+
* await resolveLocation('@ibm/analytics', '/workspace')
|
|
180
|
+
* // { source: 'npm', resolvedName: '@ibm/analytics', npmPackage: '@ibm/analytics' }
|
|
181
|
+
*/
|
|
182
|
+
async resolveLocation(name: string, workspaceRoot: string): Promise<PackageResolution> {
|
|
183
|
+
const trimmedName = name.trim()
|
|
184
|
+
const parsed = parsePluginName(trimmedName)
|
|
185
|
+
|
|
186
|
+
// If scoped, always use npm (local packages are never scoped)
|
|
187
|
+
if (parsed.type === 'scoped') {
|
|
188
|
+
return {
|
|
189
|
+
source: 'npm',
|
|
190
|
+
resolvedName: trimmedName,
|
|
191
|
+
npmPackage: trimmedName,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check local workspace directory
|
|
196
|
+
const localPath = path.join(workspaceRoot, this.getFolderName(), trimmedName)
|
|
197
|
+
const localExists = await fs.pathExists(localPath)
|
|
198
|
+
|
|
199
|
+
if (localExists) {
|
|
200
|
+
// Verify it's a valid package
|
|
201
|
+
const isValid = await this.verify(localPath)
|
|
202
|
+
|
|
203
|
+
if (isValid) {
|
|
204
|
+
const version = await this.readVersion(localPath)
|
|
205
|
+
return {
|
|
206
|
+
source: 'local',
|
|
207
|
+
resolvedName: trimmedName,
|
|
208
|
+
localPath,
|
|
209
|
+
version,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Not found locally or invalid, resolve to npm package
|
|
215
|
+
const npmPackage = this.toNpmPackageName(trimmedName)
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
source: 'npm',
|
|
219
|
+
resolvedName: trimmedName,
|
|
220
|
+
npmPackage,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -7,27 +7,19 @@ import ora from 'ora'
|
|
|
7
7
|
|
|
8
8
|
import { detectLaunch77Context } from '@launch77/plugin-runtime'
|
|
9
9
|
import { AppService } from '../services/app-svc.js'
|
|
10
|
-
import { APP_TYPES, APP_TYPES_LIST } from '../types/app-types.js'
|
|
11
|
-
|
|
12
|
-
import type { AppType } from '../types/app-types.js'
|
|
13
10
|
|
|
14
11
|
export function createAppCommand(): Command {
|
|
15
12
|
const command = new Command('app:create')
|
|
16
|
-
.argument('<
|
|
13
|
+
.argument('<template>', 'App template name (e.g., webapp, api, marketing-site)')
|
|
17
14
|
.argument('<app-name>', 'Name of the application')
|
|
18
|
-
.description('Create a new application')
|
|
19
|
-
.option('-p, --port <port>', 'Default port for the app')
|
|
20
|
-
.action(async (
|
|
15
|
+
.description('Create a new application from a template')
|
|
16
|
+
.option('-p, --port <port>', 'Default port for the app (default: 3000)')
|
|
17
|
+
.action(async (template: string, appName: string, options) => {
|
|
21
18
|
try {
|
|
22
|
-
|
|
23
|
-
if (!APP_TYPES.includes(type as AppType)) {
|
|
24
|
-
throw new Error(`Invalid app type: ${type}\n\nSupported types: ${APP_TYPES_LIST}`)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
console.log(chalk.blue(`\nš Creating ${type} app: ${appName}\n`))
|
|
19
|
+
console.log(chalk.blue(`\nš Creating app: ${appName} from template: ${template}\n`))
|
|
28
20
|
|
|
29
|
-
// Set default port
|
|
30
|
-
const port = options.port ||
|
|
21
|
+
// Set default port
|
|
22
|
+
const port = options.port || '3000'
|
|
31
23
|
|
|
32
24
|
// Detect context
|
|
33
25
|
const context = await detectLaunch77Context(process.cwd())
|
|
@@ -39,7 +31,7 @@ export function createAppCommand(): Command {
|
|
|
39
31
|
const templateSpinner = ora('Generating app structure...').start()
|
|
40
32
|
let result
|
|
41
33
|
try {
|
|
42
|
-
result = await appService.createApp({ type:
|
|
34
|
+
result = await appService.createApp({ type: template, appName, port }, context)
|
|
43
35
|
templateSpinner.succeed('App structure generated')
|
|
44
36
|
} catch (error) {
|
|
45
37
|
templateSpinner.fail('Failed to generate app structure')
|
|
@@ -47,8 +39,12 @@ export function createAppCommand(): Command {
|
|
|
47
39
|
}
|
|
48
40
|
|
|
49
41
|
// Success message
|
|
50
|
-
|
|
51
|
-
console.log(chalk.
|
|
42
|
+
const relativeAppPath = path.relative(process.cwd(), result.appPath)
|
|
43
|
+
console.log(chalk.green(`\nā
App created successfully at ${relativeAppPath}`))
|
|
44
|
+
console.log(chalk.gray(`\nNext steps:`))
|
|
45
|
+
console.log(chalk.gray(` cd ${relativeAppPath}`))
|
|
46
|
+
console.log(chalk.gray(` npm run dev`))
|
|
47
|
+
console.log(chalk.gray(`\nServer will start at http://localhost:${port}\n`))
|
|
52
48
|
} catch (error) {
|
|
53
49
|
const message = error instanceof Error ? error.message : String(error)
|
|
54
50
|
console.error(chalk.red('Error:'), message)
|
|
@@ -12,7 +12,7 @@ import { AppService } from '../services/app-svc.js'
|
|
|
12
12
|
export function deleteAppCommand(): Command {
|
|
13
13
|
const command = new Command('app:delete')
|
|
14
14
|
.argument('<app-name>', 'Name of the app to delete')
|
|
15
|
-
.description('Delete an app from the
|
|
15
|
+
.description('Delete an app from the workspace (requires confirmation)')
|
|
16
16
|
.action(async (appName: string) => {
|
|
17
17
|
try {
|
|
18
18
|
console.log(chalk.yellow(`\nā ļø WARNING: You are about to delete the app '${appName}'\n`))
|
|
@@ -25,7 +25,6 @@ export function validateManifestCommand(): Command {
|
|
|
25
25
|
console.log()
|
|
26
26
|
console.log(chalk.cyan('Manifest details:'))
|
|
27
27
|
console.log(chalk.gray(' Schema version:'), manifest.schemaVersion)
|
|
28
|
-
console.log(chalk.gray(' App type:'), manifest.type)
|
|
29
28
|
console.log(chalk.gray(' Package:'), manifest.package)
|
|
30
29
|
|
|
31
30
|
if ('name' in manifest && manifest.name) {
|
package/src/modules/app/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { AppService } from './services/app-svc.js'
|
|
|
3
3
|
export { ManifestService } from './services/manifest-svc.js'
|
|
4
4
|
|
|
5
5
|
// Types
|
|
6
|
-
export type { CreateAppRequest, CreateAppResult, DeleteAppRequest, DeleteAppResult
|
|
6
|
+
export type { CreateAppRequest, CreateAppResult, DeleteAppRequest, DeleteAppResult } from './types/app-types.js'
|
|
7
7
|
export type { AppManifest, DeploymentConfig } from './lib/manifest-schema.js'
|
|
8
8
|
|
|
9
9
|
// Errors
|
|
@@ -12,5 +12,4 @@ export { AppAlreadyExistsError, AppNotFoundError, InvalidAppNameError, TemplateN
|
|
|
12
12
|
// Commands
|
|
13
13
|
export { createAppCommand } from './commands/create-app.js'
|
|
14
14
|
export { deleteAppCommand } from './commands/delete-app.js'
|
|
15
|
-
export { generateManifestCommand } from './commands/generate-manifest.js'
|
|
16
15
|
export { validateManifestCommand } from './commands/validate-manifest.js'
|