wattpm 2.19.0 → 2.20.0-alpha.2
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/index.js +8 -1
- package/lib/commands/build.js +130 -1
- package/lib/commands/external.js +231 -156
- package/lib/commands/init.js +17 -11
- package/lib/gitignore.js +3 -0
- package/lib/utils.js +57 -6
- package/package.json +8 -7
- package/schema.json +7 -1
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bold } from 'colorette'
|
|
2
|
-
import { buildCommand } from './lib/commands/build.js'
|
|
2
|
+
import { buildCommand, installCommand } from './lib/commands/build.js'
|
|
3
3
|
import { devCommand, reloadCommand, restartCommand, startCommand, stopCommand } from './lib/commands/execution.js'
|
|
4
4
|
import { importCommand, resolveCommand } from './lib/commands/external.js'
|
|
5
5
|
import { helpCommand } from './lib/commands/help.js'
|
|
@@ -11,6 +11,8 @@ import { version } from './lib/schema.js'
|
|
|
11
11
|
import { createLogger, overrideFatal, parseArgs, setVerbose } from './lib/utils.js'
|
|
12
12
|
|
|
13
13
|
export async function main () {
|
|
14
|
+
globalThis.platformatic = { executable: 'watt' }
|
|
15
|
+
|
|
14
16
|
const logger = createLogger('info')
|
|
15
17
|
|
|
16
18
|
overrideFatal(logger)
|
|
@@ -92,6 +94,9 @@ export async function main () {
|
|
|
92
94
|
case 'resolve':
|
|
93
95
|
command = resolveCommand
|
|
94
96
|
break
|
|
97
|
+
case 'install':
|
|
98
|
+
command = installCommand
|
|
99
|
+
break
|
|
95
100
|
case 'help':
|
|
96
101
|
command = helpCommand
|
|
97
102
|
break
|
|
@@ -106,3 +111,5 @@ export async function main () {
|
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
export * from './lib/schema.js'
|
|
114
|
+
|
|
115
|
+
export { resolveServices } from './lib/commands/external.js'
|
package/lib/commands/build.js
CHANGED
|
@@ -1,7 +1,88 @@
|
|
|
1
1
|
import { ensureLoggableError } from '@platformatic/utils'
|
|
2
2
|
import { bold } from 'colorette'
|
|
3
|
+
import { parse } from 'dotenv'
|
|
4
|
+
import { execa } from 'execa'
|
|
5
|
+
import { existsSync } from 'node:fs'
|
|
6
|
+
import { readFile } from 'node:fs/promises'
|
|
3
7
|
import { resolve } from 'node:path'
|
|
4
|
-
import { buildRuntime, findConfigurationFile, overrideFatal, parseArgs } from '../utils.js'
|
|
8
|
+
import { buildRuntime, findConfigurationFile, loadConfigurationFile, overrideFatal, parseArgs } from '../utils.js'
|
|
9
|
+
|
|
10
|
+
// This function will not perform the command if the .npmrc file contains the 'dry-run' flag - This is useful in tests
|
|
11
|
+
async function executeCommand (root, ...args) {
|
|
12
|
+
const npmrc = resolve(root, '.npmrc')
|
|
13
|
+
if (existsSync(npmrc)) {
|
|
14
|
+
try {
|
|
15
|
+
const env = parse(await readFile(npmrc, 'utf8'))
|
|
16
|
+
|
|
17
|
+
if (env['dry-run'] === 'true') {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
/* c8 ignore next 5 */
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// No-op
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* c8 ignore next */
|
|
27
|
+
return execa(...args)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function installDependencies (logger, root, services, production, packageManager) {
|
|
31
|
+
if (typeof services === 'string') {
|
|
32
|
+
const config = await loadConfigurationFile(logger, services)
|
|
33
|
+
services = config.services
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* c8 ignore next 8 */
|
|
37
|
+
if (!packageManager) {
|
|
38
|
+
if (existsSync(resolve(root, 'pnpm-lock.yaml'))) {
|
|
39
|
+
packageManager = 'pnpm'
|
|
40
|
+
} else {
|
|
41
|
+
packageManager = 'npm'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const args = ['install']
|
|
46
|
+
|
|
47
|
+
if (production) {
|
|
48
|
+
switch (packageManager) {
|
|
49
|
+
case 'pnpm':
|
|
50
|
+
args.push('--prod')
|
|
51
|
+
break
|
|
52
|
+
case 'npm':
|
|
53
|
+
args.push('--omit=dev')
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Install dependencies of the application
|
|
59
|
+
try {
|
|
60
|
+
logger.info(
|
|
61
|
+
`Installing ${production ? 'production ' : ''}dependencies for the application using ${packageManager} ...`
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
await executeCommand(root, packageManager, args, { cwd: root, stdio: 'inherit' })
|
|
65
|
+
/* c8 ignore next 3 */
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger.fatal({ error: ensureLoggableError(error) }, 'Unable to install dependencies of the application.')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const service of services) {
|
|
71
|
+
try {
|
|
72
|
+
logger.info(
|
|
73
|
+
`Installing ${production ? 'production ' : ''}dependencies for the service ${bold(service.id)} using ${packageManager} ...`
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
await executeCommand(root, packageManager, args, { cwd: resolve(root, service.path), stdio: 'inherit' })
|
|
77
|
+
/* c8 ignore next 6 */
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.fatal(
|
|
80
|
+
{ error: ensureLoggableError(error) },
|
|
81
|
+
`Unable to install dependencies of the service ${bold(service.id)}.`
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
5
86
|
|
|
6
87
|
export async function buildCommand (logger, args) {
|
|
7
88
|
const { positionals } = parseArgs(args, {}, false)
|
|
@@ -40,6 +121,34 @@ export async function buildCommand (logger, args) {
|
|
|
40
121
|
await runtime.close(false, true)
|
|
41
122
|
}
|
|
42
123
|
|
|
124
|
+
export async function installCommand (logger, args) {
|
|
125
|
+
const {
|
|
126
|
+
values: { production, 'package-manager': packageManager },
|
|
127
|
+
positionals
|
|
128
|
+
} = parseArgs(
|
|
129
|
+
args,
|
|
130
|
+
{
|
|
131
|
+
production: {
|
|
132
|
+
type: 'boolean',
|
|
133
|
+
short: 'p',
|
|
134
|
+
default: false
|
|
135
|
+
},
|
|
136
|
+
'package-manager': {
|
|
137
|
+
type: 'string',
|
|
138
|
+
short: 'P'
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
false
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
/* c8 ignore next */
|
|
145
|
+
const root = resolve(process.cwd(), positionals[0] ?? '')
|
|
146
|
+
const configurationFile = await findConfigurationFile(logger, root)
|
|
147
|
+
|
|
148
|
+
await installDependencies(logger, root, configurationFile, production, packageManager)
|
|
149
|
+
logger.done('All services have been resolved.')
|
|
150
|
+
}
|
|
151
|
+
|
|
43
152
|
export const help = {
|
|
44
153
|
build: {
|
|
45
154
|
usage: 'build [root]',
|
|
@@ -50,5 +159,25 @@ export const help = {
|
|
|
50
159
|
description: 'The directory containing the application (the default is the current directory)'
|
|
51
160
|
}
|
|
52
161
|
]
|
|
162
|
+
},
|
|
163
|
+
install: {
|
|
164
|
+
usage: 'install [root]',
|
|
165
|
+
description: 'Install all dependencies of an application and its services',
|
|
166
|
+
args: [
|
|
167
|
+
{
|
|
168
|
+
name: 'root',
|
|
169
|
+
description: 'The directory containing the application (the default is the current directory)'
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
options: [
|
|
173
|
+
{
|
|
174
|
+
usage: '-p --production',
|
|
175
|
+
description: 'Only install production dependencies'
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
usage: '-P, --package-manager <executable>',
|
|
179
|
+
description: 'Use an alternative package manager (the default is to autodetect it)'
|
|
180
|
+
}
|
|
181
|
+
]
|
|
53
182
|
}
|
|
54
183
|
}
|
package/lib/commands/external.js
CHANGED
|
@@ -1,56 +1,26 @@
|
|
|
1
1
|
import { configCandidates } from '@platformatic/basic'
|
|
2
|
-
import { Store } from '@platformatic/config'
|
|
3
|
-
import { platformaticRuntime } from '@platformatic/runtime'
|
|
4
2
|
import { ensureLoggableError } from '@platformatic/utils'
|
|
5
3
|
import { bold } from 'colorette'
|
|
4
|
+
import { parse } from 'dotenv'
|
|
6
5
|
import { execa } from 'execa'
|
|
7
|
-
import { existsSync
|
|
8
|
-
import {
|
|
9
|
-
import { basename, dirname, isAbsolute, join, relative, resolve
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
8
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
10
9
|
import { defaultServiceJson } from '../defaults.js'
|
|
11
10
|
import { version } from '../schema.js'
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
findConfigurationFile,
|
|
13
|
+
loadConfigurationFile,
|
|
14
|
+
loadRawConfigurationFile,
|
|
15
|
+
overrideFatal,
|
|
16
|
+
parseArgs,
|
|
17
|
+
saveConfigurationFile,
|
|
18
|
+
serviceToEnvVariable
|
|
19
|
+
} from '../utils.js'
|
|
20
|
+
import { installDependencies } from './build.js'
|
|
13
21
|
|
|
14
22
|
const originCandidates = ['origin', 'upstream']
|
|
15
23
|
|
|
16
|
-
export async function checkEmptyDirectory (logger, path, relativePath) {
|
|
17
|
-
if (existsSync(path)) {
|
|
18
|
-
const statObject = await stat(path)
|
|
19
|
-
|
|
20
|
-
if (!statObject.isDirectory()) {
|
|
21
|
-
logger.fatal(`Path ${bold(relativePath)} exists but it is not a directory.`)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const entries = await readdir(path)
|
|
25
|
-
|
|
26
|
-
if (entries.filter(e => !e.startsWith('.')).length) {
|
|
27
|
-
logger.fatal(`Directory ${bold(relativePath)} is not empty.`)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function parseConfiguration (logger, configurationFile) {
|
|
33
|
-
const store = new Store({
|
|
34
|
-
cwd: process.cwd(),
|
|
35
|
-
logger
|
|
36
|
-
})
|
|
37
|
-
store.add(platformaticRuntime)
|
|
38
|
-
|
|
39
|
-
const { configManager } = await store.loadConfig({
|
|
40
|
-
config: configurationFile,
|
|
41
|
-
overrides: {
|
|
42
|
-
/* c8 ignore next 3 */
|
|
43
|
-
onMissingEnv (key) {
|
|
44
|
-
return ''
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
await configManager.parse()
|
|
50
|
-
|
|
51
|
-
return configManager.current
|
|
52
|
-
}
|
|
53
|
-
|
|
54
24
|
async function parseLocalFolder (path) {
|
|
55
25
|
// Read the package.json, if any
|
|
56
26
|
const packageJsonPath = resolve(path, 'package.json')
|
|
@@ -103,54 +73,36 @@ async function findExistingConfiguration (root, path) {
|
|
|
103
73
|
}
|
|
104
74
|
}
|
|
105
75
|
|
|
106
|
-
async function
|
|
107
|
-
|
|
108
|
-
const root = dirname(configurationFile)
|
|
76
|
+
export async function appendEnvVariable (envFile, key, value) {
|
|
77
|
+
let contents = ''
|
|
109
78
|
|
|
110
|
-
|
|
79
|
+
if (existsSync(envFile)) {
|
|
80
|
+
contents = await readFile(envFile, 'utf-8')
|
|
111
81
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (path.startsWith(autoloadPath)) {
|
|
115
|
-
return
|
|
82
|
+
if (contents.length && !contents.endsWith('\n')) {
|
|
83
|
+
contents += '\n'
|
|
116
84
|
}
|
|
117
85
|
}
|
|
118
86
|
|
|
119
|
-
|
|
120
|
-
config.web ??= []
|
|
121
|
-
config.web.push({ id, path, url })
|
|
87
|
+
contents += `${key}=${value}\n`
|
|
122
88
|
|
|
123
|
-
|
|
89
|
+
return writeFile(envFile, contents, 'utf-8')
|
|
124
90
|
}
|
|
125
91
|
|
|
126
92
|
async function fixConfiguration (logger, root) {
|
|
127
93
|
const configurationFile = await findConfigurationFile(logger, root)
|
|
128
|
-
const config =
|
|
129
|
-
|
|
130
|
-
// Load all services in the autoload and the one manually specified
|
|
131
|
-
const services = []
|
|
132
|
-
const autoLoadPath = config.autoload?.path
|
|
133
|
-
if (autoLoadPath) {
|
|
134
|
-
for (const path of await readdir(resolve(root, autoLoadPath))) {
|
|
135
|
-
services.push(join(autoLoadPath, path))
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/* c8 ignore next */
|
|
140
|
-
for (const service of config.services ?? []) {
|
|
141
|
-
services.push(service.path)
|
|
142
|
-
}
|
|
94
|
+
const config = await loadConfigurationFile(logger, configurationFile)
|
|
143
95
|
|
|
144
96
|
// For each service, if there is no watt.json, create one and fix package dependencies
|
|
145
|
-
for (const
|
|
146
|
-
const wattConfiguration = await findExistingConfiguration(root,
|
|
97
|
+
for (const { path } of config.services) {
|
|
98
|
+
const wattConfiguration = await findExistingConfiguration(root, path)
|
|
147
99
|
|
|
148
100
|
/* c8 ignore next 3 */
|
|
149
101
|
if (wattConfiguration) {
|
|
150
102
|
continue
|
|
151
103
|
}
|
|
152
104
|
|
|
153
|
-
const { id, packageJson, stackable } = await parseLocalFolder(resolve(root,
|
|
105
|
+
const { id, packageJson, stackable } = await parseLocalFolder(resolve(root, path))
|
|
154
106
|
|
|
155
107
|
packageJson.dependencies ??= {}
|
|
156
108
|
packageJson.dependencies[stackable] = `^${version}`
|
|
@@ -161,28 +113,102 @@ async function fixConfiguration (logger, root) {
|
|
|
161
113
|
}
|
|
162
114
|
|
|
163
115
|
logger.info(`Detected stackable ${bold(stackable)} for service ${bold(id)}, adding to the service dependencies.`)
|
|
164
|
-
|
|
165
|
-
await
|
|
116
|
+
|
|
117
|
+
await saveConfigurationFile(logger, resolve(path, 'package.json'), packageJson)
|
|
118
|
+
await saveConfigurationFile(logger, resolve(path, 'watt.json'), wattJson)
|
|
166
119
|
}
|
|
167
120
|
}
|
|
168
121
|
|
|
169
|
-
async function
|
|
170
|
-
const
|
|
122
|
+
async function importService (logger, configurationFile, id, path, url) {
|
|
123
|
+
const config = await loadConfigurationFile(logger, configurationFile)
|
|
124
|
+
const rawConfig = await loadRawConfigurationFile(logger, configurationFile)
|
|
125
|
+
const root = dirname(configurationFile)
|
|
126
|
+
const envFile = resolve(root, '.env')
|
|
127
|
+
const envSampleFile = resolve(root, '.env.sample')
|
|
128
|
+
const envVariable = serviceToEnvVariable(id)
|
|
129
|
+
|
|
130
|
+
let useEnv = true
|
|
131
|
+
|
|
132
|
+
// If there is a locale path
|
|
133
|
+
if (path) {
|
|
134
|
+
let autoloadPath = config.autoload?.path
|
|
135
|
+
|
|
136
|
+
// If we already autoload this path, there is nothing to do
|
|
137
|
+
if (autoloadPath) {
|
|
138
|
+
autoloadPath = resolve(root, autoloadPath)
|
|
139
|
+
if (path.startsWith(autoloadPath)) {
|
|
140
|
+
logger.warn('The path is already autoloaded as a service.')
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
}
|
|
171
144
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
145
|
+
// If the path is within the application repository
|
|
146
|
+
if (path.startsWith(root)) {
|
|
147
|
+
// If the path is already defined as a service, there is nothing to do
|
|
148
|
+
if (config.services.some(s => s.path === path)) {
|
|
149
|
+
logger.warn('The path is already defined as a service.')
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Do not use env variables
|
|
154
|
+
useEnv = false
|
|
155
|
+
}
|
|
178
156
|
|
|
179
|
-
|
|
157
|
+
if (!url) {
|
|
158
|
+
logger.warn(`The service ${bold(id)} does not define a Git repository.`)
|
|
159
|
+
}
|
|
180
160
|
}
|
|
181
161
|
|
|
182
|
-
|
|
183
|
-
|
|
162
|
+
// Make sure the service is not already defined
|
|
163
|
+
if (config.serviceMap.has(id)) {
|
|
164
|
+
logger.fatal(`There is already a service ${bold(id)} defined, please choose a different service ID.`)
|
|
184
165
|
}
|
|
185
166
|
|
|
167
|
+
/* c8 ignore next */
|
|
168
|
+
rawConfig.web ??= []
|
|
169
|
+
|
|
170
|
+
if (useEnv) {
|
|
171
|
+
rawConfig.web.push({ id, path: `{${envVariable}}`, url })
|
|
172
|
+
|
|
173
|
+
// Make sure the environment variable is not already defined
|
|
174
|
+
if (existsSync(envFile)) {
|
|
175
|
+
const env = parse(await readFile(envFile, 'utf-8'))
|
|
176
|
+
|
|
177
|
+
if (env[envVariable]) {
|
|
178
|
+
logger.fatal(
|
|
179
|
+
`There is already an environment variable ${bold(envVariable)} defined, please choose a different service ID.`
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Copy the .env file to .env.sample if it does not exist
|
|
185
|
+
if (!existsSync(envSampleFile)) {
|
|
186
|
+
await writeFile(envSampleFile, '', 'utf-8')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await appendEnvVariable(envFile, envVariable, path ?? '')
|
|
190
|
+
await appendEnvVariable(envSampleFile, envVariable, '')
|
|
191
|
+
} else {
|
|
192
|
+
rawConfig.web.push({ id, path: relative(root, path) })
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await saveConfigurationFile(logger, configurationFile, rawConfig)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function importURL (logger, _, configurationFile, rawUrl, id, http) {
|
|
199
|
+
let url = rawUrl
|
|
200
|
+
if (rawUrl.match(/^[a-z0-9-]+\/[a-z0-9-]+$/i)) {
|
|
201
|
+
url = http ? `https://github.com/${rawUrl}.git` : `git@github.com:${rawUrl}.git`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await importService(logger, configurationFile, id ?? basename(rawUrl, '.git'), null, url)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function importLocal (logger, root, configurationFile, path, overridenId) {
|
|
208
|
+
const { id, url, packageJson, stackable } = await parseLocalFolder(path)
|
|
209
|
+
|
|
210
|
+
await importService(logger, configurationFile, overridenId ?? id, path, url)
|
|
211
|
+
|
|
186
212
|
// Check if there is any configuration file we recognize. If so, don't do anything
|
|
187
213
|
const wattConfiguration = await findExistingConfiguration(root, path)
|
|
188
214
|
if (wattConfiguration) {
|
|
@@ -204,34 +230,126 @@ async function importLocal (logger, root, configurationFile, path) {
|
|
|
204
230
|
}
|
|
205
231
|
|
|
206
232
|
logger.info(`Detected stackable ${bold(stackable)} for service ${bold(id)}, adding to the service dependencies.`)
|
|
207
|
-
|
|
208
|
-
await
|
|
233
|
+
|
|
234
|
+
await saveConfigurationFile(logger, resolve(path, 'package.json'), packageJson)
|
|
235
|
+
await saveConfigurationFile(logger, resolve(path, 'watt.json'), wattJson)
|
|
209
236
|
}
|
|
210
237
|
|
|
211
|
-
async function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
238
|
+
export async function resolveServices (
|
|
239
|
+
logger,
|
|
240
|
+
root,
|
|
241
|
+
configurationFile,
|
|
242
|
+
username,
|
|
243
|
+
password,
|
|
244
|
+
skipDependencies,
|
|
245
|
+
packageManager
|
|
246
|
+
) {
|
|
247
|
+
const config = await loadConfigurationFile(logger, configurationFile)
|
|
248
|
+
|
|
249
|
+
/* c8 ignore next 8 */
|
|
250
|
+
if (!packageManager) {
|
|
251
|
+
if (existsSync(resolve(root, 'pnpm-lock.yaml'))) {
|
|
252
|
+
packageManager = 'pnpm'
|
|
253
|
+
} else {
|
|
254
|
+
packageManager = 'npm'
|
|
255
|
+
}
|
|
215
256
|
}
|
|
216
257
|
|
|
217
|
-
|
|
218
|
-
|
|
258
|
+
// The services which might be to be resolved are the one that have a URL and either
|
|
259
|
+
// no path defined (which means no environment variable set) or a non-existing path (which means not resolved yet)
|
|
260
|
+
const resolvableServices = config.services.filter(service => {
|
|
261
|
+
if (!service.url) {
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (service.path && existsSync(service.path)) {
|
|
266
|
+
logger.warn(`Skipping service ${bold(service.id)} as the path already exists.`)
|
|
267
|
+
return false
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return true
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// Iterate the services a first time to verify the environment files configuration and which services must be resolved
|
|
274
|
+
const toResolve = []
|
|
275
|
+
|
|
276
|
+
// Simply use service.path here
|
|
277
|
+
for (const service of resolvableServices) {
|
|
278
|
+
if (!service.path) {
|
|
279
|
+
service.path = resolve(root, `${config.resolvedServicesBasePath}/${service.id}`)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const directory = resolve(root, service.path)
|
|
283
|
+
|
|
284
|
+
// If the directory already exists, it's either external or already resolved, nothing to do in both cases
|
|
285
|
+
if (!existsSync(directory)) {
|
|
286
|
+
if (!directory.startsWith(root)) {
|
|
287
|
+
logger.warn(
|
|
288
|
+
`Skipping service ${bold(service.id)} as the non existent directory ${bold(service.path)} is outside the project directory.`
|
|
289
|
+
)
|
|
290
|
+
} else {
|
|
291
|
+
// This repository must be resolved
|
|
292
|
+
toResolve.push(service)
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
logger.warn(
|
|
296
|
+
`Skipping service ${bold(service.id)} as the generated path ${bold(join(config.resolvedServicesBasePath, service.id))} already exists.`
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
219
300
|
|
|
220
|
-
|
|
301
|
+
// Resolve the services
|
|
302
|
+
for (const service of toResolve) {
|
|
303
|
+
const childLogger = logger.child({ name: service.id })
|
|
304
|
+
overrideFatal(childLogger)
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const absolutePath = service.path
|
|
308
|
+
const relativePath = relative(root, absolutePath)
|
|
309
|
+
|
|
310
|
+
// Clone and install dependencies
|
|
311
|
+
childLogger.info(`Resolving service ${bold(service.id)} ...`)
|
|
312
|
+
|
|
313
|
+
let url = service.url
|
|
314
|
+
if (url.startsWith('http') && username && password) {
|
|
315
|
+
const parsed = new URL(url)
|
|
316
|
+
parsed.username ||= username
|
|
317
|
+
parsed.password ||= password
|
|
318
|
+
url = parsed.toString()
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (username) {
|
|
322
|
+
childLogger.info(`Cloning ${bold(service.url)} as user ${bold(username)} into ${bold(relativePath)} ...`)
|
|
323
|
+
} else {
|
|
324
|
+
childLogger.info(`Cloning ${bold(service.url)} into ${bold(relativePath)} ...`)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await execa('git', ['clone', url, absolutePath])
|
|
328
|
+
} catch (error) {
|
|
329
|
+
childLogger.fatal(
|
|
330
|
+
{ error: ensureLoggableError(error) },
|
|
331
|
+
`Unable to clone repository of the service ${bold(service.id)}`
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Install dependencies
|
|
337
|
+
if (!skipDependencies) {
|
|
338
|
+
await installDependencies(logger, root, toResolve, false, packageManager)
|
|
339
|
+
}
|
|
221
340
|
}
|
|
222
341
|
|
|
223
342
|
export async function importCommand (logger, args) {
|
|
224
|
-
const {
|
|
343
|
+
const {
|
|
344
|
+
values: { id, http },
|
|
345
|
+
positionals
|
|
346
|
+
} = parseArgs(
|
|
225
347
|
args,
|
|
226
348
|
{
|
|
227
349
|
id: {
|
|
228
350
|
type: 'string',
|
|
229
351
|
short: 'i'
|
|
230
352
|
},
|
|
231
|
-
path: {
|
|
232
|
-
type: 'string',
|
|
233
|
-
short: 'p'
|
|
234
|
-
},
|
|
235
353
|
http: {
|
|
236
354
|
type: 'boolean',
|
|
237
355
|
short: 'h'
|
|
@@ -271,15 +389,15 @@ export async function importCommand (logger, args) {
|
|
|
271
389
|
})
|
|
272
390
|
|
|
273
391
|
if (local) {
|
|
274
|
-
return importLocal(logger, root, configurationFile, local)
|
|
392
|
+
return importLocal(logger, root, configurationFile, local, id)
|
|
275
393
|
}
|
|
276
394
|
|
|
277
|
-
return importURL(logger, root, configurationFile,
|
|
395
|
+
return importURL(logger, root, configurationFile, rawUrl, id, http)
|
|
278
396
|
}
|
|
279
397
|
|
|
280
398
|
export async function resolveCommand (logger, args) {
|
|
281
399
|
const {
|
|
282
|
-
values: { username, password, 'skip-dependencies': skipDependencies },
|
|
400
|
+
values: { username, password, 'skip-dependencies': skipDependencies, 'package-manager': packageManager },
|
|
283
401
|
positionals
|
|
284
402
|
} = parseArgs(
|
|
285
403
|
args,
|
|
@@ -298,6 +416,10 @@ export async function resolveCommand (logger, args) {
|
|
|
298
416
|
type: 'boolean',
|
|
299
417
|
short: 's',
|
|
300
418
|
default: false
|
|
419
|
+
},
|
|
420
|
+
'package-manager': {
|
|
421
|
+
type: 'string',
|
|
422
|
+
short: 'p'
|
|
301
423
|
}
|
|
302
424
|
},
|
|
303
425
|
false
|
|
@@ -305,56 +427,9 @@ export async function resolveCommand (logger, args) {
|
|
|
305
427
|
|
|
306
428
|
/* c8 ignore next */
|
|
307
429
|
const root = resolve(process.cwd(), positionals[0] ?? '')
|
|
308
|
-
|
|
309
430
|
const configurationFile = await findConfigurationFile(logger, root)
|
|
310
|
-
const config = await parseConfiguration(logger, configurationFile)
|
|
311
|
-
|
|
312
|
-
for (const service of config.services) {
|
|
313
|
-
let operation
|
|
314
|
-
const childLogger = logger.child({ name: service.id })
|
|
315
|
-
overrideFatal(childLogger)
|
|
316
|
-
|
|
317
|
-
try {
|
|
318
|
-
if (!service.url) {
|
|
319
|
-
continue
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
childLogger.info(`Resolving service ${bold(service.id)} ...`)
|
|
323
|
-
|
|
324
|
-
const relativePath = relative(root, service.path)
|
|
325
|
-
|
|
326
|
-
// Check that the target directory is empty, otherwise cloning will likely fail
|
|
327
|
-
await checkEmptyDirectory(childLogger, service.path, relativePath)
|
|
328
|
-
|
|
329
|
-
operation = 'clone repository'
|
|
330
|
-
|
|
331
|
-
let url = service.url
|
|
332
|
-
|
|
333
|
-
if (url.startsWith('http') && username && password) {
|
|
334
|
-
const parsed = new URL(url)
|
|
335
|
-
parsed.username ||= username
|
|
336
|
-
parsed.password ||= password
|
|
337
|
-
url = parsed.toString()
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (username) {
|
|
341
|
-
childLogger.info(`Cloning ${bold(service.url)} as user ${bold(username)} into ${bold(relativePath)} ...`)
|
|
342
|
-
} else {
|
|
343
|
-
childLogger.info(`Cloning ${bold(service.url)} into ${bold(relativePath)} ...`)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
await execa('git', ['clone', url, service.path])
|
|
347
|
-
|
|
348
|
-
if (!skipDependencies) {
|
|
349
|
-
operation = 'installing dependencies'
|
|
350
|
-
childLogger.info('Installing dependencies ...')
|
|
351
|
-
await execa('npm', ['i'], { cwd: service.path })
|
|
352
|
-
}
|
|
353
|
-
} catch (error) {
|
|
354
|
-
childLogger.fatal({ error: ensureLoggableError(error) }, `Unable to ${operation} of service ${bold(service.id)}`)
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
431
|
|
|
432
|
+
await resolveServices(logger, root, configurationFile, username, password, skipDependencies, packageManager)
|
|
358
433
|
logger.done('All services have been resolved.')
|
|
359
434
|
}
|
|
360
435
|
|
|
@@ -377,10 +452,6 @@ export const help = {
|
|
|
377
452
|
usage: '-i, --id <value>',
|
|
378
453
|
description: 'The id of the service (the default is the basename of the URL)'
|
|
379
454
|
},
|
|
380
|
-
{
|
|
381
|
-
usage: '-p, --path <value>',
|
|
382
|
-
description: 'The path where to import the service (the default is the service id)'
|
|
383
|
-
},
|
|
384
455
|
{
|
|
385
456
|
usage: '-h, --http',
|
|
386
457
|
description: 'Use HTTP URL when expanding GitHub repositories'
|
|
@@ -408,6 +479,10 @@ export const help = {
|
|
|
408
479
|
{
|
|
409
480
|
usage: '-s, --skip-dependencies',
|
|
410
481
|
description: 'Do not install services dependencies'
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
usage: 'P, --package-manager <executable>',
|
|
485
|
+
description: 'Use an alternative package manager (the default is to autodetect it)'
|
|
411
486
|
}
|
|
412
487
|
],
|
|
413
488
|
footer: `
|
package/lib/commands/init.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { ConfigManager } from '@platformatic/config'
|
|
2
2
|
import { ensureLoggableError } from '@platformatic/utils'
|
|
3
3
|
import { bold } from 'colorette'
|
|
4
|
-
import { existsSync
|
|
4
|
+
import { existsSync } from 'node:fs'
|
|
5
5
|
import { mkdir, stat, writeFile } from 'node:fs/promises'
|
|
6
6
|
import { basename, resolve } from 'node:path'
|
|
7
7
|
import { defaultConfiguration, defaultPackageJson } from '../defaults.js'
|
|
8
8
|
import { gitignore } from '../gitignore.js'
|
|
9
9
|
import { schema, version } from '../schema.js'
|
|
10
|
-
import { parseArgs, verbose } from '../utils.js'
|
|
10
|
+
import { parseArgs, saveConfigurationFile, verbose } from '../utils.js'
|
|
11
11
|
|
|
12
12
|
export async function initCommand (logger, args) {
|
|
13
13
|
const {
|
|
@@ -57,7 +57,7 @@ export async function initCommand (logger, args) {
|
|
|
57
57
|
|
|
58
58
|
// Create the web folder, will implicitly create the root
|
|
59
59
|
try {
|
|
60
|
-
await mkdir(web, { recursive: true
|
|
60
|
+
await mkdir(web, { recursive: true })
|
|
61
61
|
/* c8 ignore next 6 */
|
|
62
62
|
} catch (error) {
|
|
63
63
|
logger.fatal(
|
|
@@ -76,11 +76,11 @@ export async function initCommand (logger, args) {
|
|
|
76
76
|
|
|
77
77
|
await configManager.parse()
|
|
78
78
|
|
|
79
|
-
await
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
79
|
+
await saveConfigurationFile(logger, configurationFile, {
|
|
80
|
+
$schema: schema.$id,
|
|
81
|
+
...configManager.current,
|
|
82
|
+
entrypoint: positionals[1] ?? undefined
|
|
83
|
+
})
|
|
84
84
|
|
|
85
85
|
const packageJson = {
|
|
86
86
|
name: basename(root),
|
|
@@ -89,13 +89,13 @@ export async function initCommand (logger, args) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
if (packageManager === 'npm') {
|
|
92
|
-
packageJson.workspaces = ['web/*']
|
|
92
|
+
packageJson.workspaces = ['web/*', 'external/*']
|
|
93
93
|
} else if (packageManager === 'pnpm') {
|
|
94
|
-
await
|
|
94
|
+
await saveConfigurationFile(logger, resolve(root, 'pnpm-workspace.yaml'), { packages: ['web/*', 'external/*'] })
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// Write the package.json file
|
|
98
|
-
await
|
|
98
|
+
await saveConfigurationFile(logger, resolve(root, 'package.json'), packageJson)
|
|
99
99
|
|
|
100
100
|
// Write the .gitignore file
|
|
101
101
|
await writeFile(resolve(root, '.gitignore'), gitignore, 'utf-8')
|
|
@@ -116,6 +116,12 @@ export const help = {
|
|
|
116
116
|
name: 'entrypoint',
|
|
117
117
|
description: 'The name of the entrypoint service'
|
|
118
118
|
}
|
|
119
|
+
],
|
|
120
|
+
options: [
|
|
121
|
+
{
|
|
122
|
+
usage: 'p, --package-manager <executable>',
|
|
123
|
+
description: 'Use an alternative package manager'
|
|
124
|
+
}
|
|
119
125
|
]
|
|
120
126
|
}
|
|
121
127
|
}
|
package/lib/gitignore.js
CHANGED
package/lib/utils.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ConfigManager,
|
|
3
|
+
getParser,
|
|
4
|
+
getStringifier,
|
|
5
|
+
loadConfig as pltConfigLoadConfig,
|
|
6
|
+
Store
|
|
7
|
+
} from '@platformatic/config'
|
|
2
8
|
import { platformaticRuntime, buildRuntime as pltBuildRuntime } from '@platformatic/runtime'
|
|
3
9
|
import { bgGreen, black, bold } from 'colorette'
|
|
10
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
4
11
|
import { dirname, resolve } from 'node:path'
|
|
5
12
|
import { parseArgs as nodeParseArgs } from 'node:util'
|
|
6
|
-
import pino from 'pino'
|
|
13
|
+
import { pino } from 'pino'
|
|
7
14
|
import pinoPretty from 'pino-pretty'
|
|
8
15
|
|
|
9
16
|
export let verbose = false
|
|
@@ -70,7 +77,12 @@ export function parseArgs (args, options, stopAtFirstPositional = true) {
|
|
|
70
77
|
tokens: true
|
|
71
78
|
})
|
|
72
79
|
|
|
73
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
values,
|
|
82
|
+
positionals,
|
|
83
|
+
unparsed,
|
|
84
|
+
tokens
|
|
85
|
+
}
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
export function getMatchingRuntimeArgs (logger, positional) {
|
|
@@ -85,12 +97,16 @@ export function getMatchingRuntimeArgs (logger, positional) {
|
|
|
85
97
|
return args
|
|
86
98
|
}
|
|
87
99
|
|
|
100
|
+
export function serviceToEnvVariable (service) {
|
|
101
|
+
return `PLT_SERVICE_${service.toUpperCase().replaceAll(/[^A-Z0-9_]/g, '_')}_PATH`
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
export async function findConfigurationFile (logger, root) {
|
|
89
105
|
let current = root
|
|
90
106
|
let configurationFile
|
|
91
107
|
while (configurationFile === undefined) {
|
|
92
108
|
// Find a wattpm.json or watt.json file
|
|
93
|
-
configurationFile = await ConfigManager.findConfigFile(current,
|
|
109
|
+
configurationFile = await ConfigManager.findConfigFile(current, true)
|
|
94
110
|
if (!configurationFile) {
|
|
95
111
|
const newCurrent = dirname(current)
|
|
96
112
|
|
|
@@ -104,7 +120,9 @@ export async function findConfigurationFile (logger, root) {
|
|
|
104
120
|
|
|
105
121
|
if (typeof configurationFile !== 'string') {
|
|
106
122
|
logger.fatal(
|
|
107
|
-
`Cannot find a
|
|
123
|
+
`Cannot find a supported Watt configuration file (like ${bold(
|
|
124
|
+
'watt.json'
|
|
125
|
+
)}, a ${bold('wattpm.json')} or a ${bold('platformatic.json')}) in ${bold(root)}.`
|
|
108
126
|
)
|
|
109
127
|
}
|
|
110
128
|
|
|
@@ -112,11 +130,44 @@ export async function findConfigurationFile (logger, root) {
|
|
|
112
130
|
return resolved
|
|
113
131
|
}
|
|
114
132
|
|
|
133
|
+
export async function loadConfigurationFile (logger, configurationFile) {
|
|
134
|
+
const store = new Store({
|
|
135
|
+
cwd: process.cwd(),
|
|
136
|
+
logger
|
|
137
|
+
})
|
|
138
|
+
store.add(platformaticRuntime)
|
|
139
|
+
|
|
140
|
+
const { configManager } = await store.loadConfig({
|
|
141
|
+
config: configurationFile,
|
|
142
|
+
overrides: {
|
|
143
|
+
/* c8 ignore next 3 */
|
|
144
|
+
onMissingEnv (key) {
|
|
145
|
+
return ''
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
await configManager.parse(true, [])
|
|
151
|
+
return configManager.current
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function loadRawConfigurationFile (_, configurationFile) {
|
|
155
|
+
const parseConfig = getParser(configurationFile)
|
|
156
|
+
|
|
157
|
+
return parseConfig(await readFile(configurationFile, 'utf-8'))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function saveConfigurationFile (logger, configurationFile, config) {
|
|
161
|
+
const stringifyConfig = getStringifier(configurationFile)
|
|
162
|
+
|
|
163
|
+
return writeFile(configurationFile, stringifyConfig(config), 'utf-8')
|
|
164
|
+
}
|
|
165
|
+
|
|
115
166
|
export async function buildRuntime (logger, configurationFile) {
|
|
116
167
|
const store = new Store()
|
|
117
168
|
store.add(platformaticRuntime)
|
|
118
169
|
|
|
119
|
-
const config = await pltConfigLoadConfig({}, ['-c', configurationFile], store, {}, true)
|
|
170
|
+
const config = await pltConfigLoadConfig({}, ['-c', configurationFile], store, {}, true, logger)
|
|
120
171
|
config.configManager.args = config.args
|
|
121
172
|
|
|
122
173
|
const runtimeConfig = config.configManager
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wattpm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0-alpha.2",
|
|
4
4
|
"description": "The Node.js Application Server",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"colorette": "^2.0.20",
|
|
29
29
|
"commist": "^3.2.0",
|
|
30
|
+
"dotenv": "^16.4.5",
|
|
30
31
|
"execa": "^9.4.0",
|
|
31
32
|
"help-me": "^5.0.0",
|
|
32
33
|
"minimist": "^1.2.8",
|
|
@@ -34,11 +35,11 @@
|
|
|
34
35
|
"pino-pretty": "^13.0.0",
|
|
35
36
|
"split2": "^4.2.0",
|
|
36
37
|
"table": "^6.8.2",
|
|
37
|
-
"@platformatic/
|
|
38
|
-
"@platformatic/
|
|
39
|
-
"@platformatic/
|
|
40
|
-
"@platformatic/
|
|
41
|
-
"@platformatic/
|
|
38
|
+
"@platformatic/basic": "2.20.0-alpha.2",
|
|
39
|
+
"@platformatic/control": "2.20.0-alpha.2",
|
|
40
|
+
"@platformatic/config": "2.20.0-alpha.2",
|
|
41
|
+
"@platformatic/utils": "2.20.0-alpha.2",
|
|
42
|
+
"@platformatic/runtime": "2.20.0-alpha.2"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"borp": "^0.19.0",
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
"neostandard": "^0.11.1",
|
|
49
50
|
"typescript": "^5.5.4",
|
|
50
51
|
"undici": "^7.0.0",
|
|
51
|
-
"@platformatic/node": "2.
|
|
52
|
+
"@platformatic/node": "2.20.0-alpha.2"
|
|
52
53
|
},
|
|
53
54
|
"scripts": {
|
|
54
55
|
"test": "npm run lint && borp --concurrency=1 --timeout=300000",
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/wattpm/2.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/wattpm/2.20.0-alpha.2.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -183,6 +183,7 @@
|
|
|
183
183
|
},
|
|
184
184
|
"path": {
|
|
185
185
|
"type": "string",
|
|
186
|
+
"allowEmptyPaths": true,
|
|
186
187
|
"resolvePath": true
|
|
187
188
|
},
|
|
188
189
|
"config": {
|
|
@@ -335,6 +336,7 @@
|
|
|
335
336
|
},
|
|
336
337
|
"path": {
|
|
337
338
|
"type": "string",
|
|
339
|
+
"allowEmptyPaths": true,
|
|
338
340
|
"resolvePath": true
|
|
339
341
|
},
|
|
340
342
|
"config": {
|
|
@@ -1107,6 +1109,10 @@
|
|
|
1107
1109
|
}
|
|
1108
1110
|
],
|
|
1109
1111
|
"default": 300000
|
|
1112
|
+
},
|
|
1113
|
+
"resolvedServicesBasePath": {
|
|
1114
|
+
"type": "string",
|
|
1115
|
+
"default": "external"
|
|
1110
1116
|
}
|
|
1111
1117
|
},
|
|
1112
1118
|
"anyOf": [
|