wattpm 2.14.0 → 2.16.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -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)
@@ -106,3 +108,5 @@ export async function main () {
106
108
  }
107
109
 
108
110
  export * from './lib/schema.js'
111
+
112
+ export { resolveServices } from './lib/commands/external.js'
@@ -1,56 +1,25 @@
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, } from 'node:fs'
8
- import { readdir, readFile, stat, writeFile } from 'node:fs/promises'
9
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
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 { findConfigurationFile, overrideFatal, parseArgs } from '../utils.js'
11
+ import {
12
+ findConfigurationFile,
13
+ loadConfigurationFile,
14
+ loadRawConfigurationFile,
15
+ overrideFatal,
16
+ parseArgs,
17
+ saveConfigurationFile,
18
+ serviceToEnvVariable
19
+ } from '../utils.js'
13
20
 
14
21
  const originCandidates = ['origin', 'upstream']
15
22
 
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
23
  async function parseLocalFolder (path) {
55
24
  // Read the package.json, if any
56
25
  const packageJsonPath = resolve(path, 'package.json')
@@ -103,54 +72,36 @@ async function findExistingConfiguration (root, path) {
103
72
  }
104
73
  }
105
74
 
106
- async function addService (configurationFile, id, path, url) {
107
- const config = JSON.parse(await readFile(configurationFile, 'utf-8'))
108
- const root = dirname(configurationFile)
75
+ export async function appendEnvVariable (envFile, key, value) {
76
+ let contents = ''
109
77
 
110
- let autoloadPath = config.autoload?.path
78
+ if (existsSync(envFile)) {
79
+ contents = await readFile(envFile, 'utf-8')
111
80
 
112
- if (autoloadPath) {
113
- autoloadPath = join(root, autoloadPath)
114
- if (path.startsWith(autoloadPath)) {
115
- return
81
+ if (contents.length && !contents.endsWith('\n')) {
82
+ contents += '\n'
116
83
  }
117
84
  }
118
85
 
119
- /* c8 ignore next */
120
- config.web ??= []
121
- config.web.push({ id, path, url })
86
+ contents += `${key}=${value}\n`
122
87
 
123
- await writeFile(configurationFile, JSON.stringify(config, null, 2), 'utf-8')
88
+ return writeFile(envFile, contents, 'utf-8')
124
89
  }
125
90
 
126
91
  async function fixConfiguration (logger, root) {
127
92
  const configurationFile = await findConfigurationFile(logger, root)
128
- const config = JSON.parse(await readFile(configurationFile, 'utf-8'))
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
- }
93
+ const config = await loadConfigurationFile(logger, configurationFile)
143
94
 
144
95
  // For each service, if there is no watt.json, create one and fix package dependencies
145
- for (const service of services) {
146
- const wattConfiguration = await findExistingConfiguration(root, service)
96
+ for (const { path } of config.services) {
97
+ const wattConfiguration = await findExistingConfiguration(root, path)
147
98
 
148
99
  /* c8 ignore next 3 */
149
100
  if (wattConfiguration) {
150
101
  continue
151
102
  }
152
103
 
153
- const { id, packageJson, stackable } = await parseLocalFolder(resolve(root, service))
104
+ const { id, packageJson, stackable } = await parseLocalFolder(resolve(root, path))
154
105
 
155
106
  packageJson.dependencies ??= {}
156
107
  packageJson.dependencies[stackable] = `^${version}`
@@ -161,28 +112,102 @@ async function fixConfiguration (logger, root) {
161
112
  }
162
113
 
163
114
  logger.info(`Detected stackable ${bold(stackable)} for service ${bold(id)}, adding to the service dependencies.`)
164
- await writeFile(resolve(root, service, 'package.json'), JSON.stringify(packageJson, null, 2), 'utf-8')
165
- await writeFile(resolve(root, service, 'watt.json'), JSON.stringify(wattJson, null, 2), 'utf-8')
115
+
116
+ await saveConfigurationFile(logger, resolve(path, 'package.json'), packageJson)
117
+ await saveConfigurationFile(logger, resolve(path, 'watt.json'), wattJson)
166
118
  }
167
119
  }
168
120
 
169
- async function importLocal (logger, root, configurationFile, path) {
170
- const { id, url, packageJson, stackable } = await parseLocalFolder(path)
121
+ async function importService (logger, configurationFile, id, path, url) {
122
+ const config = await loadConfigurationFile(logger, configurationFile)
123
+ const rawConfig = await loadRawConfigurationFile(logger, configurationFile)
124
+ const root = dirname(configurationFile)
125
+ const envFile = resolve(root, '.env')
126
+ const envSampleFile = resolve(root, '.env.sample')
127
+ const envVariable = serviceToEnvVariable(id)
128
+
129
+ let useEnv = true
130
+
131
+ // If there is a locale path
132
+ if (path) {
133
+ let autoloadPath = config.autoload?.path
134
+
135
+ // If we already autoload this path, there is nothing to do
136
+ if (autoloadPath) {
137
+ autoloadPath = resolve(root, autoloadPath)
138
+ if (path.startsWith(autoloadPath)) {
139
+ logger.warn('The path is already autoloaded as a service.')
140
+ return
141
+ }
142
+ }
143
+
144
+ // If the path is within the application repository
145
+ if (path.startsWith(root)) {
146
+ // If the path is already defined as a service, there is nothing to do
147
+ if (config.services.some(s => s.path === path)) {
148
+ logger.warn('The path is already defined as a service.')
149
+ return
150
+ }
151
+
152
+ // Do not use env variables
153
+ useEnv = false
154
+ }
155
+
156
+ if (!url) {
157
+ logger.warn(`The service ${bold(id)} does not define a Git repository.`)
158
+ }
159
+ }
160
+
161
+ // Make sure the service is not already defined
162
+ if (config.serviceMap.has(id)) {
163
+ logger.fatal(`There is already a service ${bold(id)} defined, please choose a different service ID.`)
164
+ }
165
+
166
+ /* c8 ignore next */
167
+ rawConfig.web ??= []
171
168
 
172
- // Modify the configuration
173
- const config = JSON.parse(await readFile(configurationFile, 'utf-8'))
174
- let isAutoloaded = false
175
- const autoLoadPath = config.autoload?.path
176
- if (autoLoadPath) {
177
- const relativePath = relative(root, path)
169
+ if (useEnv) {
170
+ rawConfig.web.push({ id, path: `{${envVariable}}`, url })
178
171
 
179
- isAutoloaded = relativePath.startsWith(`${autoLoadPath}${sep}`)
172
+ // Make sure the environment variable is not already defined
173
+ if (existsSync(envFile)) {
174
+ const env = parse(await readFile(envFile, 'utf-8'))
175
+
176
+ if (env[envVariable]) {
177
+ logger.fatal(
178
+ `There is already an environment variable ${bold(envVariable)} defined, please choose a different service ID.`
179
+ )
180
+ }
181
+ }
182
+
183
+ // Copy the .env file to .env.sample if it does not exist
184
+ if (!existsSync(envSampleFile)) {
185
+ await writeFile(envSampleFile, '', 'utf-8')
186
+ }
187
+
188
+ await appendEnvVariable(envFile, envVariable, path ?? '')
189
+ await appendEnvVariable(envSampleFile, envVariable, '')
190
+ } else {
191
+ rawConfig.web.push({ id, path: relative(root, path) })
180
192
  }
181
193
 
182
- if (!isAutoloaded) {
183
- await addService(configurationFile, id, path, url)
194
+ await saveConfigurationFile(logger, configurationFile, rawConfig)
195
+ }
196
+
197
+ async function importURL (logger, _, configurationFile, rawUrl, id, http) {
198
+ let url = rawUrl
199
+ if (rawUrl.match(/^[a-z0-9-]+\/[a-z0-9-]+$/i)) {
200
+ url = http ? `https://github.com/${rawUrl}.git` : `git@github.com:${rawUrl}.git`
184
201
  }
185
202
 
203
+ await importService(logger, configurationFile, id ?? basename(rawUrl, '.git'), null, url)
204
+ }
205
+
206
+ async function importLocal (logger, root, configurationFile, path, overridenId) {
207
+ const { id, url, packageJson, stackable } = await parseLocalFolder(path)
208
+
209
+ await importService(logger, configurationFile, overridenId ?? id, path, url)
210
+
186
211
  // Check if there is any configuration file we recognize. If so, don't do anything
187
212
  const wattConfiguration = await findExistingConfiguration(root, path)
188
213
  if (wattConfiguration) {
@@ -204,34 +229,126 @@ async function importLocal (logger, root, configurationFile, path) {
204
229
  }
205
230
 
206
231
  logger.info(`Detected stackable ${bold(stackable)} for service ${bold(id)}, adding to the service dependencies.`)
207
- await writeFile(resolve(path, 'package.json'), JSON.stringify(packageJson, null, 2), 'utf-8')
208
- await writeFile(resolve(path, 'watt.json'), JSON.stringify(wattJson, null, 2), 'utf-8')
232
+
233
+ await saveConfigurationFile(logger, resolve(path, 'package.json'), packageJson)
234
+ await saveConfigurationFile(logger, resolve(path, 'watt.json'), wattJson)
209
235
  }
210
236
 
211
- async function importURL (logger, root, configurationFile, values, rawUrl) {
212
- let url = rawUrl
213
- if (rawUrl.match(/^[a-z0-9-]+\/[a-z0-9-]+$/i)) {
214
- url = values.http ? `https://github.com/${rawUrl}.git` : `git@github.com:${rawUrl}.git`
237
+ export async function resolveServices (
238
+ logger,
239
+ root,
240
+ configurationFile,
241
+ username,
242
+ password,
243
+ skipDependencies,
244
+ packageManager
245
+ ) {
246
+ const config = await loadConfigurationFile(logger, configurationFile)
247
+
248
+ /* c8 ignore next 8 */
249
+ if (!packageManager) {
250
+ if (existsSync(resolve(root, 'pnpm-lock.yaml'))) {
251
+ packageManager = 'pnpm'
252
+ } else {
253
+ packageManager = 'npm'
254
+ }
215
255
  }
216
256
 
217
- const service = values.id ?? basename(rawUrl, '.git')
218
- const path = values.path ?? `web/${service}`
257
+ // The services which might be to be resolved are the one that have a URL and either
258
+ // no path defined (which means no environment variable set) or a non-existing path (which means not resolved yet)
259
+ const resolvableServices = config.services.filter(service => {
260
+ if (!service.url) {
261
+ return false
262
+ }
263
+
264
+ if (service.path && existsSync(service.path)) {
265
+ logger.warn(`Skipping service ${bold(service.id)} as the path already exists.`)
266
+ return false
267
+ }
268
+
269
+ return true
270
+ })
219
271
 
220
- await addService(configurationFile, service, path, url)
272
+ // Iterate the services a first time to verify the environment files configuration and which services must be resolved
273
+ const toResolve = []
274
+
275
+ // Simply use service.path here
276
+ for (const service of resolvableServices) {
277
+ if (!service.path) {
278
+ service.path = resolve(root, `${config.resolvedServicesBasePath}/${service.id}`)
279
+ }
280
+
281
+ const directory = resolve(root, service.path)
282
+
283
+ // If the directory already exists, it's either external or already resolved, nothing to do in both cases
284
+ if (!existsSync(directory)) {
285
+ if (!directory.startsWith(root)) {
286
+ logger.warn(
287
+ `Skipping service ${bold(service.id)} as the non existent directory ${bold(service.path)} is outside the project directory.`
288
+ )
289
+ } else {
290
+ // This repository must be resolved
291
+ toResolve.push(service)
292
+ }
293
+ } else {
294
+ logger.warn(
295
+ `Skipping service ${bold(service.id)} as the generated path ${bold(join(config.resolvedServicesBasePath, service.id))} already exists.`
296
+ )
297
+ }
298
+ }
299
+
300
+ // Resolve the services
301
+ for (const service of toResolve) {
302
+ let operation
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
+ operation = 'clone repository'
312
+ childLogger.info(`Resolving service ${bold(service.id)} ...`)
313
+
314
+ let url = service.url
315
+ if (url.startsWith('http') && username && password) {
316
+ const parsed = new URL(url)
317
+ parsed.username ||= username
318
+ parsed.password ||= password
319
+ url = parsed.toString()
320
+ }
321
+
322
+ if (username) {
323
+ childLogger.info(`Cloning ${bold(service.url)} as user ${bold(username)} into ${bold(relativePath)} ...`)
324
+ } else {
325
+ childLogger.info(`Cloning ${bold(service.url)} into ${bold(relativePath)} ...`)
326
+ }
327
+
328
+ await execa('git', ['clone', url, absolutePath])
329
+
330
+ if (!skipDependencies) {
331
+ operation = 'installing dependencies'
332
+ childLogger.info(`Installing dependencies for service ${bold(service.id)} ...`)
333
+ await execa(packageManager, ['install'], { cwd: absolutePath })
334
+ }
335
+ } catch (error) {
336
+ childLogger.fatal({ error: ensureLoggableError(error) }, `Unable to ${operation} of service ${bold(service.id)}`)
337
+ }
338
+ }
221
339
  }
222
340
 
223
341
  export async function importCommand (logger, args) {
224
- const { values, positionals } = parseArgs(
342
+ const {
343
+ values: { id, http },
344
+ positionals
345
+ } = parseArgs(
225
346
  args,
226
347
  {
227
348
  id: {
228
349
  type: 'string',
229
350
  short: 'i'
230
351
  },
231
- path: {
232
- type: 'string',
233
- short: 'p'
234
- },
235
352
  http: {
236
353
  type: 'boolean',
237
354
  short: 'h'
@@ -271,15 +388,15 @@ export async function importCommand (logger, args) {
271
388
  })
272
389
 
273
390
  if (local) {
274
- return importLocal(logger, root, configurationFile, local)
391
+ return importLocal(logger, root, configurationFile, local, id)
275
392
  }
276
393
 
277
- return importURL(logger, root, configurationFile, values, rawUrl)
394
+ return importURL(logger, root, configurationFile, rawUrl, id, http)
278
395
  }
279
396
 
280
397
  export async function resolveCommand (logger, args) {
281
398
  const {
282
- values: { username, password, 'skip-dependencies': skipDependencies },
399
+ values: { username, password, 'skip-dependencies': skipDependencies, 'package-manager': packageManager },
283
400
  positionals
284
401
  } = parseArgs(
285
402
  args,
@@ -298,6 +415,10 @@ export async function resolveCommand (logger, args) {
298
415
  type: 'boolean',
299
416
  short: 's',
300
417
  default: false
418
+ },
419
+ 'package-manager': {
420
+ type: 'string',
421
+ short: 'p'
301
422
  }
302
423
  },
303
424
  false
@@ -305,56 +426,9 @@ export async function resolveCommand (logger, args) {
305
426
 
306
427
  /* c8 ignore next */
307
428
  const root = resolve(process.cwd(), positionals[0] ?? '')
308
-
309
429
  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
430
 
431
+ await resolveServices(logger, root, configurationFile, username, password, skipDependencies, packageManager)
358
432
  logger.done('All services have been resolved.')
359
433
  }
360
434
 
@@ -377,10 +451,6 @@ export const help = {
377
451
  usage: '-i, --id <value>',
378
452
  description: 'The id of the service (the default is the basename of the URL)'
379
453
  },
380
- {
381
- usage: '-p, --path <value>',
382
- description: 'The path where to import the service (the default is the service id)'
383
- },
384
454
  {
385
455
  usage: '-h, --http',
386
456
  description: 'Use HTTP URL when expanding GitHub repositories'
@@ -408,6 +478,10 @@ export const help = {
408
478
  {
409
479
  usage: '-s, --skip-dependencies',
410
480
  description: 'Do not install services dependencies'
481
+ },
482
+ {
483
+ usage: 'P, --package-manager',
484
+ description: 'Use an alternative package manager (the default is to autodetect it)'
411
485
  }
412
486
  ],
413
487
  footer: `
@@ -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, } from 'node:fs'
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 writeFile(
80
- configurationFile,
81
- JSON.stringify({ $schema: schema.$id, ...configManager.current, entrypoint: positionals[1] ?? undefined }, null, 2),
82
- 'utf-8'
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 writeFile(resolve(root, 'pnpm-workspace.yaml'), "packages:\n - 'web/*'", 'utf-8')
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 writeFile(resolve(root, 'package.json'), JSON.stringify(packageJson, null, 2), 'utf-8')
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',
123
+ description: 'Use an alternative package manager'
124
+ }
119
125
  ]
120
126
  }
121
127
  }
package/lib/gitignore.js CHANGED
@@ -129,4 +129,7 @@ dist
129
129
  .yarn/build-state.yml
130
130
  .yarn/install-state.gz
131
131
  .pnp.*
132
+
133
+ # Watt resolved folder
134
+ external/
132
135
  `
package/lib/utils.js CHANGED
@@ -1,9 +1,16 @@
1
- import { ConfigManager, loadConfig as pltConfigLoadConfig, Store } from '@platformatic/config'
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 { values, positionals, unparsed, tokens }
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, ['watt.json', 'wattpm.json', 'platformatic.json'])
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 ${bold('watt.json')}, a ${bold('wattpm.json')} or a ${bold('platformatic.json')} file in ${bold(root)}.`
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.14.0",
3
+ "version": "2.16.0-alpha.1",
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/basic": "2.14.0",
38
- "@platformatic/config": "2.14.0",
39
- "@platformatic/control": "2.14.0",
40
- "@platformatic/runtime": "2.14.0",
41
- "@platformatic/utils": "2.14.0"
38
+ "@platformatic/basic": "2.16.0-alpha.1",
39
+ "@platformatic/control": "2.16.0-alpha.1",
40
+ "@platformatic/config": "2.16.0-alpha.1",
41
+ "@platformatic/runtime": "2.16.0-alpha.1",
42
+ "@platformatic/utils": "2.16.0-alpha.1"
42
43
  },
43
44
  "devDependencies": {
44
45
  "borp": "^0.18.0",
@@ -48,7 +49,7 @@
48
49
  "neostandard": "^0.11.1",
49
50
  "typescript": "^5.5.4",
50
51
  "undici": "^6.19.8",
51
- "@platformatic/node": "2.14.0"
52
+ "@platformatic/node": "2.16.0-alpha.1"
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.14.0.json",
2
+ "$id": "https://schemas.platformatic.dev/wattpm/2.16.0-alpha.1.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": {
@@ -1078,6 +1080,10 @@
1078
1080
  }
1079
1081
  ],
1080
1082
  "default": 300000
1083
+ },
1084
+ "resolvedServicesBasePath": {
1085
+ "type": "string",
1086
+ "default": "external"
1081
1087
  }
1082
1088
  },
1083
1089
  "anyOf": [