@platformatic/generators 4.0.0-new-config.2 → 4.0.0-new-config.4
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 +1 -1
- package/lib/base-generator.js +286 -48
- package/lib/import-generator.js +46 -44
- package/lib/utils.js +182 -0
- package/package.json +4 -3
package/index.js
CHANGED
package/lib/base-generator.js
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
import { generateDashedName } from '@platformatic/foundation'
|
|
2
|
+
import {
|
|
3
|
+
capabilityFactories,
|
|
4
|
+
evaluateConfigurationFile,
|
|
5
|
+
configurationFileNameFor,
|
|
6
|
+
listDirectoryEntries,
|
|
7
|
+
raw,
|
|
8
|
+
selectConfigurationFileNames,
|
|
9
|
+
selectLegacyConfigurationFileNames,
|
|
10
|
+
serializeConfiguration
|
|
11
|
+
} from '@platformatic/foundation/lib/v4/index.js'
|
|
12
|
+
import { builders, generateCode, parseModule } from 'magicast'
|
|
2
13
|
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { generateGitignore } from './create-gitignore.js'
|
|
5
|
-
import { MissingEnvVariable, ModuleNeeded, PrepareError } from './errors.js'
|
|
6
|
-
import { FileGenerator } from './file-generator.js'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
7
15
|
import {
|
|
8
16
|
convertApplicationNameToPrefix,
|
|
17
|
+
equivalentSource,
|
|
18
|
+
findAnyConfigurationFile,
|
|
19
|
+
readEnvironmentReferences,
|
|
20
|
+
readEnvFile,
|
|
9
21
|
envStringToObject,
|
|
10
22
|
extractEnvVariablesFromText,
|
|
11
|
-
flattenObject,
|
|
12
23
|
getApplicationTemplateFromSchemaUrl,
|
|
13
24
|
getLatestNpmVersion,
|
|
14
25
|
getPackageConfigurationObject,
|
|
15
26
|
PLT_ROOT,
|
|
16
27
|
stripVersion
|
|
17
28
|
} from './utils.js'
|
|
29
|
+
import { generateGitignore } from './create-gitignore.js'
|
|
30
|
+
import { MissingEnvVariable, ModuleNeeded, PrepareError } from './errors.js'
|
|
31
|
+
import { FileGenerator } from './file-generator.js'
|
|
18
32
|
|
|
19
33
|
/* c8 ignore start */
|
|
20
34
|
const fakeLogger = {
|
|
@@ -183,23 +197,7 @@ class BaseGenerator extends FileGenerator {
|
|
|
183
197
|
if (this.config.isUpdating) {
|
|
184
198
|
// only the packages options may have changed, let's update those
|
|
185
199
|
await this.generateConfigFile()
|
|
186
|
-
|
|
187
|
-
const fileFromDisk = await this.loadFile({ file: this.runtimeConfig, path: '' })
|
|
188
|
-
const currentConfigFile = JSON.parse(fileFromDisk.contents)
|
|
189
|
-
if (currentConfigFile.plugins) {
|
|
190
|
-
if (generatedConfigFile.plugins && generatedConfigFile.plugins.packages) {
|
|
191
|
-
currentConfigFile.plugins.packages = generatedConfigFile.plugins.packages
|
|
192
|
-
} else {
|
|
193
|
-
// remove packages because new configuration does not have them
|
|
194
|
-
currentConfigFile.plugins.packages = []
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
this.reset()
|
|
198
|
-
this.addFile({
|
|
199
|
-
path: '',
|
|
200
|
-
file: this.runtimeConfig,
|
|
201
|
-
contents: JSON.stringify(currentConfigFile, null, 2)
|
|
202
|
-
})
|
|
200
|
+
await this.#updatePackagesInPlace()
|
|
203
201
|
} else {
|
|
204
202
|
await this.getFastifyVersion()
|
|
205
203
|
await this.getPlatformaticVersion()
|
|
@@ -241,9 +239,14 @@ class BaseGenerator extends FileGenerator {
|
|
|
241
239
|
|
|
242
240
|
checkEnvVariablesInConfigFile () {
|
|
243
241
|
const excludedEnvs = [PLT_ROOT]
|
|
244
|
-
const configFileName = this.
|
|
245
|
-
|
|
246
|
-
|
|
242
|
+
const configFileName = this.configurationFileName()
|
|
243
|
+
/*
|
|
244
|
+
Read from the configuration the generator built rather than from the file it wrote: the file
|
|
245
|
+
resolves each placeholder into an expression, so the text no longer carries the `{VAR}` this
|
|
246
|
+
is looking for. What is being checked is whether the generator declared every variable it
|
|
247
|
+
used, which is a question about the object.
|
|
248
|
+
*/
|
|
249
|
+
const envVars = extractEnvVariablesFromText(JSON.stringify(this.generatedConfig ?? {}))
|
|
247
250
|
const envKeys = Object.keys(this.config.env)
|
|
248
251
|
if (envVars.length > 0) {
|
|
249
252
|
for (const ev of envVars) {
|
|
@@ -282,7 +285,7 @@ class BaseGenerator extends FileGenerator {
|
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
async generateConfigFile () {
|
|
285
|
-
const configFileName = this.
|
|
288
|
+
const configFileName = this.configurationFileName()
|
|
286
289
|
const contents = await this._getConfigFileContents()
|
|
287
290
|
// handle packages
|
|
288
291
|
if (this.packages.length > 0) {
|
|
@@ -308,15 +311,203 @@ class BaseGenerator extends FileGenerator {
|
|
|
308
311
|
})
|
|
309
312
|
}
|
|
310
313
|
|
|
314
|
+
/*
|
|
315
|
+
Kept so that what reads this configuration back reads the object rather than re-parsing the
|
|
316
|
+
file: the file is a module now, and its values are expressions rather than the literals the
|
|
317
|
+
generator put in.
|
|
318
|
+
*/
|
|
319
|
+
this.generatedConfig = contents
|
|
320
|
+
|
|
311
321
|
this.addFile({
|
|
312
322
|
path: '',
|
|
313
323
|
file: configFileName,
|
|
314
|
-
contents:
|
|
324
|
+
contents: this.serializeConfigFile(contents)
|
|
315
325
|
})
|
|
316
326
|
|
|
317
327
|
return contents
|
|
318
328
|
}
|
|
319
329
|
|
|
330
|
+
/*
|
|
331
|
+
The v4 per-app form. A capability with a factory is spelled by calling it; one without keeps the
|
|
332
|
+
stamped plain-object form, which is what the `$schema` marker exists for.
|
|
333
|
+
|
|
334
|
+
The placeholders the generator writes are resolved here rather than left as text. v3 substituted
|
|
335
|
+
`{PLT_API_PORT}` before anything read it, and v4 has no interpolation -- so the scaffolded value
|
|
336
|
+
becomes the expression it stood for, with the default the generator was going to write into
|
|
337
|
+
`.env` anyway.
|
|
338
|
+
*/
|
|
339
|
+
/*
|
|
340
|
+
An update touches one thing -- which packages the application loads -- and has to leave
|
|
341
|
+
everything else exactly as the user left it.
|
|
342
|
+
|
|
343
|
+
For a v4 configuration that means editing the module rather than rewriting it: its values are
|
|
344
|
+
expressions, so reading it back and re-emitting would bake `Number(process.env.PORT || 3042)`
|
|
345
|
+
into whatever the port happens to be on this machine, and would drop every comment with it.
|
|
346
|
+
*/
|
|
347
|
+
async #updatePackagesInPlace () {
|
|
348
|
+
const generated = this.generatedConfig ?? {}
|
|
349
|
+
const packages = generated.plugins?.packages ?? []
|
|
350
|
+
const existing = await this.#findExistingConfiguration()
|
|
351
|
+
|
|
352
|
+
if (!existing) {
|
|
353
|
+
/*
|
|
354
|
+
Nothing on disk to update, which is what an "update" of a directory that does not exist yet
|
|
355
|
+
looks like -- adding an application to a project is one. The generated files stand as they
|
|
356
|
+
are; resetting here would throw away everything this pass just produced.
|
|
357
|
+
*/
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/*
|
|
362
|
+
Only once there is something to rewrite. An update replaces the configuration and nothing
|
|
363
|
+
else, so the staged files go -- but a pass that found no configuration is adding an
|
|
364
|
+
application rather than updating one, and its files are the whole output.
|
|
365
|
+
*/
|
|
366
|
+
this.reset()
|
|
367
|
+
|
|
368
|
+
if (existing.file.endsWith('.json')) {
|
|
369
|
+
// A v3 project being updated: it is data, and rewriting it loses nothing it carries.
|
|
370
|
+
const current = JSON.parse(existing.contents)
|
|
371
|
+
|
|
372
|
+
if (current.plugins) {
|
|
373
|
+
current.plugins.packages = packages
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.addFile({ path: '', file: existing.file, contents: JSON.stringify(current, null, 2) })
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const module = parseModule(existing.contents)
|
|
381
|
+
const target = module.exports.default
|
|
382
|
+
// The factory form is a call whose first argument is the configuration; the plain object form
|
|
383
|
+
// is the configuration itself.
|
|
384
|
+
const configuration = target?.$type === 'function-call' ? target.$args[0] : target
|
|
385
|
+
|
|
386
|
+
if (configuration) {
|
|
387
|
+
configuration.plugins ??= {}
|
|
388
|
+
/*
|
|
389
|
+
Resolved the same way the file itself spells them -- the generator's model still holds
|
|
390
|
+
`{PLT_X}`, and writing that into a module would put the placeholder's own text where the
|
|
391
|
+
expression belongs. magicast has its own way of saying "this is source".
|
|
392
|
+
*/
|
|
393
|
+
configuration.plugins.packages = this.resolveScaffoldedPlaceholders(packages, builders.raw)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const updated = generateCode(module).code
|
|
397
|
+
|
|
398
|
+
/*
|
|
399
|
+
An update that changes nothing about the configuration leaves the file alone. Plugin option
|
|
400
|
+
*values* live in `.env`, so changing one moves nothing here -- and rewriting the file anyway
|
|
401
|
+
would reprint the block, producing a diff that says something changed when nothing did.
|
|
402
|
+
*/
|
|
403
|
+
if (equivalentSource(updated, existing.contents)) {
|
|
404
|
+
this.addFile({ path: '', file: existing.file, contents: existing.contents })
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.addFile({ path: '', file: existing.file, contents: updated })
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async #findExistingConfiguration () {
|
|
412
|
+
const entries = await listDirectoryEntries(this.targetDirectory)
|
|
413
|
+
const file = selectConfigurationFileNames(entries)[0] ?? selectLegacyConfigurationFileNames(entries)[0]
|
|
414
|
+
|
|
415
|
+
if (!file) {
|
|
416
|
+
return null
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const loaded = await this.loadFile({ file, path: '' })
|
|
420
|
+
|
|
421
|
+
return { contents: loaded.contents, file }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/*
|
|
425
|
+
The suffix follows the rule the format sets: `.js` in a "type": "commonjs" package is CommonJS,
|
|
426
|
+
where `export default` is a syntax error. The generator wrote that package.json a moment ago in
|
|
427
|
+
this same pass, so it reads its own answer rather than the filesystem's.
|
|
428
|
+
*/
|
|
429
|
+
configurationFileName () {
|
|
430
|
+
let module = false
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
module = JSON.parse(this.getFileObject('package.json', '')?.contents ?? '{}').type === 'module'
|
|
434
|
+
} catch {
|
|
435
|
+
// Not generated, or not JSON yet. The unambiguous suffix is the safe answer.
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return configurationFileNameFor({ module, typescript: this.config.typescript })
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
serializeConfigFile (config) {
|
|
442
|
+
const { $schema, module: declared, ...rest } = config
|
|
443
|
+
const module = declared ?? this.module
|
|
444
|
+
const factory = capabilityFactories[module]
|
|
445
|
+
const resolved = this.resolveScaffoldedPlaceholders(rest)
|
|
446
|
+
|
|
447
|
+
if (!factory) {
|
|
448
|
+
return `export default ${serializeConfiguration({ $schema, module, ...resolved })}\n`
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return `import { ${factory} } from '${module}'\n\nexport default ${factory}(${serializeConfiguration(resolved)})\n`
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
resolveScaffoldedPlaceholders (value, expression = raw) {
|
|
455
|
+
if (typeof value === 'string') {
|
|
456
|
+
const whole = value.match(/^\{([A-Za-z0-9_]+)\}$/)
|
|
457
|
+
|
|
458
|
+
if (!whole) {
|
|
459
|
+
return value
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const name = whole[1]
|
|
463
|
+
const fallback = this.config.env?.[name]
|
|
464
|
+
|
|
465
|
+
if (fallback === undefined) {
|
|
466
|
+
return expression(`process.env.${name}`)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/*
|
|
470
|
+
A numeric default is written into the expression, and `||` rather than `??`: an env file
|
|
471
|
+
carrying the ordinary empty assignment `PORT=` supplies `''`, which is present, so `??`
|
|
472
|
+
would not fall back and `Number('')` is an ephemeral port where the reader of that line
|
|
473
|
+
expects 3042.
|
|
474
|
+
|
|
475
|
+
Everything else is a bare reference. Its value lives in `.env` -- the wizard's answers among
|
|
476
|
+
them -- and copying it into the configuration would put the same fact in two places, where
|
|
477
|
+
editing one leaves the other saying something else.
|
|
478
|
+
*/
|
|
479
|
+
/*
|
|
480
|
+
A boolean default becomes a comparison, because v4 validates without coercion: a bare
|
|
481
|
+
reference hands the schema the string 'true', which is neither of the things a boolean
|
|
482
|
+
position accepts. The comparison reproduces the scaffolded default when the variable is
|
|
483
|
+
absent and lets its opposite flip it, which is what the value was there to express.
|
|
484
|
+
*/
|
|
485
|
+
if (fallback === 'true' || fallback === true) {
|
|
486
|
+
return expression(`process.env.${name} !== 'false'`)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (fallback === 'false' || fallback === false) {
|
|
490
|
+
return expression(`process.env.${name} === 'true'`)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return /^[0-9]+$/.test(String(fallback))
|
|
494
|
+
? expression(`Number(process.env.${name} || ${fallback})`)
|
|
495
|
+
: expression(`process.env.${name}`)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (value === null || typeof value !== 'object') {
|
|
499
|
+
return value
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (Array.isArray(value)) {
|
|
503
|
+
return value.map(entry => this.resolveScaffoldedPlaceholders(entry, expression))
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return Object.fromEntries(
|
|
507
|
+
Object.entries(value).map(([key, entry]) => [key, this.resolveScaffoldedPlaceholders(entry, expression)])
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
|
|
320
511
|
/**
|
|
321
512
|
* Reads the content of package.json and returns it as an object
|
|
322
513
|
* @returns Object
|
|
@@ -421,36 +612,77 @@ class BaseGenerator extends FileGenerator {
|
|
|
421
612
|
this.packages.push(pkg)
|
|
422
613
|
}
|
|
423
614
|
|
|
615
|
+
/*
|
|
616
|
+
Reads a configuration file whichever dialect it is in. A v3 file is data and parses; a v4 one is
|
|
617
|
+
a module whose values are expressions, so the only way to know what it says is to evaluate it --
|
|
618
|
+
which is what the loader does, in a worker with an explicit environment.
|
|
619
|
+
*/
|
|
620
|
+
async readConfigurationFile (path, role = 'root', envRoot = null) {
|
|
621
|
+
if (path.endsWith('.json')) {
|
|
622
|
+
return JSON.parse(await readFile(path, 'utf-8'))
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/*
|
|
626
|
+
The project's own environment, because the file's values are expressions that read it: a port
|
|
627
|
+
written as `Number(process.env.PORT || 3042)` evaluates to NaN without it, and the generator
|
|
628
|
+
would be reading a configuration the project never has.
|
|
629
|
+
*/
|
|
630
|
+
const env = { ...process.env, ...(await readEnvFile(envRoot ?? dirname(path))) }
|
|
631
|
+
|
|
632
|
+
// The role decides how the file is classified, and a root read as an application -- or the
|
|
633
|
+
// reverse -- is refused rather than silently accepted.
|
|
634
|
+
const { config } = await evaluateConfigurationFile({ path, env, command: 'start', production: false, role })
|
|
635
|
+
|
|
636
|
+
return config
|
|
637
|
+
}
|
|
638
|
+
|
|
424
639
|
async loadFromDir (applicationName, runtimeRootPath) {
|
|
425
|
-
const runtimePkgConfigFileData =
|
|
640
|
+
const runtimePkgConfigFileData = await this.readConfigurationFile(
|
|
641
|
+
join(runtimeRootPath, await findAnyConfigurationFile(runtimeRootPath)),
|
|
642
|
+
'root',
|
|
643
|
+
runtimeRootPath
|
|
644
|
+
)
|
|
426
645
|
const applicationsPath = runtimePkgConfigFileData.autoload?.path ?? DEFAULT_SERVICES_PATH
|
|
427
|
-
const
|
|
428
|
-
|
|
646
|
+
const applicationRoot = join(runtimeRootPath, applicationsPath, applicationName)
|
|
647
|
+
// The environment is the runtime's, not the application's: the .env sits at the root beside
|
|
648
|
+
// the configuration that autoloads it.
|
|
649
|
+
const applicationPkgJsonFileData = await this.readConfigurationFile(
|
|
650
|
+
join(applicationRoot, await findAnyConfigurationFile(applicationRoot)),
|
|
651
|
+
'application',
|
|
652
|
+
runtimeRootPath
|
|
429
653
|
)
|
|
430
654
|
const runtimeEnv = envStringToObject(await readFile(join(runtimeRootPath, '.env'), 'utf-8'))
|
|
431
655
|
const applicationNamePrefix = convertApplicationNameToPrefix(applicationName)
|
|
656
|
+
/*
|
|
657
|
+
Which options read the environment, taken from the source rather than from the loaded value.
|
|
658
|
+
A v3 configuration carried `"{PLT_X}"` in its data and survived being read as JSON; a v4 one
|
|
659
|
+
says `process.env.PLT_X` in its code, and reading it here gives whatever that produced in
|
|
660
|
+
*this* process -- `undefined`, since these are the scaffolded application's variables and not
|
|
661
|
+
ours. The name is in the file, so that is where it is read from.
|
|
662
|
+
*/
|
|
663
|
+
const applicationConfigurationFile = join(applicationRoot, await findAnyConfigurationFile(applicationRoot))
|
|
664
|
+
const environmentReferences = readEnvironmentReferences(await readFile(applicationConfigurationFile, 'utf-8'))
|
|
432
665
|
const plugins = []
|
|
433
666
|
if (applicationPkgJsonFileData.plugins && applicationPkgJsonFileData.plugins.packages) {
|
|
434
|
-
for (const pkg of applicationPkgJsonFileData.plugins.packages) {
|
|
435
|
-
const flattened = flattenObject(pkg)
|
|
667
|
+
for (const [index, pkg] of applicationPkgJsonFileData.plugins.packages.entries()) {
|
|
436
668
|
const output = {
|
|
437
|
-
name:
|
|
669
|
+
name: pkg.name,
|
|
438
670
|
options: []
|
|
439
671
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
672
|
+
|
|
673
|
+
const prefix = `plugins.packages.${index}.options.`
|
|
674
|
+
|
|
675
|
+
for (const [path, runtimeEnvVarKey] of environmentReferences) {
|
|
676
|
+
if (!path.startsWith(prefix)) {
|
|
677
|
+
continue
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
output.options.push({
|
|
681
|
+
name: runtimeEnvVarKey.replace(`PLT_${applicationNamePrefix}_`, ''),
|
|
682
|
+
path: path.slice(prefix.length),
|
|
683
|
+
type: 'string',
|
|
684
|
+
value: runtimeEnv[runtimeEnvVarKey]
|
|
685
|
+
})
|
|
454
686
|
}
|
|
455
687
|
|
|
456
688
|
plugins.push(output)
|
|
@@ -459,7 +691,13 @@ class BaseGenerator extends FileGenerator {
|
|
|
459
691
|
|
|
460
692
|
return {
|
|
461
693
|
name: applicationName,
|
|
462
|
-
|
|
694
|
+
/*
|
|
695
|
+
A v4 configuration says which capability it is outright -- the factory sets `module` -- and
|
|
696
|
+
a v3 one says it through the `$schema` URL, which is what that reader is for.
|
|
697
|
+
*/
|
|
698
|
+
template:
|
|
699
|
+
applicationPkgJsonFileData.module ??
|
|
700
|
+
getApplicationTemplateFromSchemaUrl(applicationPkgJsonFileData.$schema),
|
|
463
701
|
fields: [],
|
|
464
702
|
plugins
|
|
465
703
|
}
|
package/lib/import-generator.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { findConfigurationFileRecursive, safeRemove } from '@platformatic/foundation'
|
|
2
|
+
import { capabilityFactories } from '@platformatic/foundation/lib/v4/index.js'
|
|
2
3
|
import { spawnSync } from 'node:child_process'
|
|
3
4
|
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
4
|
-
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
5
6
|
import { BaseGenerator } from './base-generator.js'
|
|
6
7
|
|
|
8
|
+
export function importedConfiguration (pkg) {
|
|
9
|
+
const factory = capabilityFactories[pkg]
|
|
10
|
+
|
|
11
|
+
if (factory) {
|
|
12
|
+
return `import { ${factory} } from '${pkg}'\n\nexport default ${factory}({})\n`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `export default {\n module: '${pkg}'\n}\n`
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
export class ImportGenerator extends BaseGenerator {
|
|
8
19
|
constructor (options = {}) {
|
|
9
20
|
const { applicationName, module, version, parent: runtime, ...opts } = options
|
|
@@ -124,7 +135,7 @@ export class ImportGenerator extends BaseGenerator {
|
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
async #generateConfigFile (originalPath, updatedPath) {
|
|
127
|
-
// Determine if there is a
|
|
138
|
+
// Determine if there is a config file in the application path - If it's missing, insert one
|
|
128
139
|
// For import it means we don't update the file, for copy it means it was already copied in #copy.
|
|
129
140
|
const existingConfig = await findConfigurationFileRecursive(originalPath)
|
|
130
141
|
|
|
@@ -132,21 +143,18 @@ export class ImportGenerator extends BaseGenerator {
|
|
|
132
143
|
return
|
|
133
144
|
}
|
|
134
145
|
|
|
135
|
-
const { module: pkg
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
contents: JSON.stringify({ module: pkg }, null, 2)
|
|
148
|
-
})
|
|
149
|
-
}
|
|
146
|
+
const { module: pkg } = this.config
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
The v4 per-app form rather than a JSON stub. This is the path taken for a capability that
|
|
150
|
+
ships no generator of its own, so a stub here would leave the wizard writing the old dialect
|
|
151
|
+
for exactly the applications least likely to be exercised by anything else.
|
|
152
|
+
*/
|
|
153
|
+
this.addFile({
|
|
154
|
+
path: '',
|
|
155
|
+
file: join(updatedPath, 'watt.config.mjs'),
|
|
156
|
+
contents: importedConfiguration(pkg)
|
|
157
|
+
})
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
async #updatePackageJson (originalPath, updatedPath, pkg, version) {
|
|
@@ -170,43 +178,37 @@ export class ImportGenerator extends BaseGenerator {
|
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
async #updateRuntime (runtime) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
break
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/* c8 ignore next - else */
|
|
190
|
-
key ??= runtime.applicationsFolder ?? 'applications'
|
|
191
|
-
const applications = config[key] ?? []
|
|
181
|
+
/*
|
|
182
|
+
The configuration the runtime generator built, not a parse of the file it wrote: that file is
|
|
183
|
+
a module now, and its values are expressions rather than the data this is about to add an
|
|
184
|
+
entry to. It is written back through the same generator, which knows how to spell it.
|
|
185
|
+
|
|
186
|
+
One spelling: the list is `applications`, because the loader refuses the v3 aliases by name.
|
|
187
|
+
And a literal path rather than v3's `{PLT_APPLICATION_<ID>_PATH}` placeholder plus its `.env`
|
|
188
|
+
line -- the indirection bought nothing and left the entry pointing at the root itself in
|
|
189
|
+
every clone missing the gitignored file the value lived in.
|
|
190
|
+
*/
|
|
191
|
+
const config = runtime.generatedConfig ?? {}
|
|
192
|
+
const applications = Array.isArray(config.applications) ? config.applications : []
|
|
192
193
|
|
|
193
194
|
if (!applications.some(application => application.id === this.config.applicationName)) {
|
|
195
|
+
const base = this.runtime?.targetDirectory ?? this.targetDirectory
|
|
196
|
+
const absolute = isAbsolute(this.config.applicationPath)
|
|
197
|
+
? this.config.applicationPath
|
|
198
|
+
: resolve(base, this.config.applicationPath)
|
|
199
|
+
const relativePath = relative(base, absolute)
|
|
200
|
+
|
|
194
201
|
applications.push({
|
|
195
202
|
id: this.config.applicationName,
|
|
196
|
-
path
|
|
203
|
+
// A cross-drive path has no relative spelling; `relative` answers with the absolute one.
|
|
204
|
+
path: (isAbsolute(relativePath) ? absolute : relativePath).split(sep).join('/'),
|
|
197
205
|
url: this.config.gitUrl
|
|
198
206
|
})
|
|
199
207
|
}
|
|
200
208
|
|
|
201
|
-
config
|
|
202
|
-
|
|
203
|
-
if (env.length > 0) {
|
|
204
|
-
env += '\n'
|
|
205
|
-
}
|
|
206
|
-
env += `${this.config.applicationPathEnvName}=${this.config.applicationPath}`
|
|
209
|
+
config.applications = applications
|
|
207
210
|
|
|
208
211
|
runtime.updateRuntimeConfig(config)
|
|
209
|
-
runtime.updateRuntimeEnv(env)
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
async #copy (root) {
|
package/lib/utils.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listDirectoryEntries,
|
|
3
|
+
selectConfigurationFileNames,
|
|
4
|
+
selectLegacyConfigurationFileNames
|
|
5
|
+
} from '@platformatic/foundation/lib/v4/index.js'
|
|
6
|
+
import { builders, generateCode, parseModule } from 'magicast'
|
|
7
|
+
import { readFile } from 'node:fs/promises'
|
|
1
8
|
import { EOL } from 'node:os'
|
|
2
9
|
import { join } from 'node:path'
|
|
3
10
|
import { setTimeout } from 'timers/promises'
|
|
@@ -180,3 +187,178 @@ export function getApplicationTemplateFromSchemaUrl (schemaUrl) {
|
|
|
180
187
|
}
|
|
181
188
|
return `@platformatic/${splitted[splitted.length - 2]}`
|
|
182
189
|
}
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
The configuration file in a directory, whichever dialect it is in. A generator reading an existing
|
|
193
|
+
project meets both: one it scaffolded under v4, and one that predates the switch.
|
|
194
|
+
*/
|
|
195
|
+
export async function findAnyConfigurationFile (directory) {
|
|
196
|
+
const entries = await listDirectoryEntries(directory)
|
|
197
|
+
|
|
198
|
+
return selectConfigurationFileNames(entries)[0] ?? selectLegacyConfigurationFileNames(entries)[0] ?? null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// The environment a project supplies to its own configuration. Absent is the same as empty here:
|
|
202
|
+
// a project without one simply has nothing to layer.
|
|
203
|
+
export async function readEnvFile (directory) {
|
|
204
|
+
try {
|
|
205
|
+
return envStringToObject(await readFile(join(directory, '.env'), 'utf-8'))
|
|
206
|
+
} catch {
|
|
207
|
+
return {}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/*
|
|
212
|
+
Whether two spellings of a module say the same thing. Used to decide that an edit produced no
|
|
213
|
+
change: the printer reflows what it touches, so comparing the text byte for byte would report a
|
|
214
|
+
change on every update whether or not one happened.
|
|
215
|
+
*/
|
|
216
|
+
export function equivalentSource (left, right) {
|
|
217
|
+
/*
|
|
218
|
+
Whitespace removed rather than collapsed, because the printer's difference is exactly one space:
|
|
219
|
+
`packages: [{` against `packages: [\n {`. Two spellings that differ only inside a string
|
|
220
|
+
literal would compare equal here, and the consequence of that is leaving a file alone whose only
|
|
221
|
+
change was spaces inside a string -- which is the harmless direction to be wrong in.
|
|
222
|
+
*/
|
|
223
|
+
return left.replace(/\s/g, '') === right.replace(/\s/g, '')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/*
|
|
227
|
+
A string to be printed as source rather than as a string literal. Exported so that a caller
|
|
228
|
+
outside this package can hand one to `resolveScaffoldedPlaceholders` without taking on the AST
|
|
229
|
+
library itself.
|
|
230
|
+
*/
|
|
231
|
+
export const rawSource = builders.raw
|
|
232
|
+
|
|
233
|
+
/*
|
|
234
|
+
Add applications to an existing root configuration by editing the file the user has, rather than
|
|
235
|
+
by writing a new one from the configuration the loader returned.
|
|
236
|
+
|
|
237
|
+
The difference is everything the source says and the loaded configuration does not:
|
|
238
|
+
`process.env.PLT_SERVER_LOGGER_LEVEL` comes back as `'info'`, a factory call comes back as the
|
|
239
|
+
object it built, and comments come back not at all. Re-emitting replaces each of those with the
|
|
240
|
+
value it happened to have on the machine doing the writing.
|
|
241
|
+
|
|
242
|
+
Returns `null` when the file's shape cannot be edited in place -- a configuration built by a call,
|
|
243
|
+
or held in a variable, or an application list that is not a literal array. There is nothing to
|
|
244
|
+
edit there, and the caller is expected to say so rather than to write something else.
|
|
245
|
+
*/
|
|
246
|
+
export function appendApplications (source, entries, resolveEntry = entry => entry) {
|
|
247
|
+
const module = parseModule(source)
|
|
248
|
+
const target = module.exports.default
|
|
249
|
+
// The plain object form exports the configuration; the factory form passes it as the first argument.
|
|
250
|
+
const configuration = target?.$type === 'function-call' ? target.$args[0] : target
|
|
251
|
+
|
|
252
|
+
if (configuration?.$type !== 'object') {
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// One spelling: the loader refuses `services` and `web` by name, so a file this editor sees
|
|
257
|
+
// lists its applications under `applications` or not at all.
|
|
258
|
+
const key = 'applications'
|
|
259
|
+
const listed = configuration[key] ?? []
|
|
260
|
+
|
|
261
|
+
if (!Array.isArray(listed)) {
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const present = new Set(Array.from(listed, entry => entry.id))
|
|
266
|
+
const added = entries.filter(entry => entry.id && !present.has(entry.id))
|
|
267
|
+
|
|
268
|
+
if (added.length === 0) {
|
|
269
|
+
return source
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
configuration[key] = [...listed, ...added.map(entry => resolveEntry(entry))]
|
|
273
|
+
|
|
274
|
+
return generateCode(module).code
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// The applications a configuration lists. One spelling: the v3 aliases are refused by the loader.
|
|
278
|
+
export function listedApplications (config) {
|
|
279
|
+
return config?.applications ?? []
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/*
|
|
283
|
+
The environment variable an expression reads, if it reads one.
|
|
284
|
+
|
|
285
|
+
`process.env.NAME` is the bare form; `process.env.NAME ?? ''` and `process.env.NAME || 3042` are
|
|
286
|
+
what a writer emits when the position needs a fallback. All three name the same variable, and the
|
|
287
|
+
fallback is not part of the name.
|
|
288
|
+
*/
|
|
289
|
+
function environmentReference (node) {
|
|
290
|
+
if (!node) {
|
|
291
|
+
return null
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (node.type === 'LogicalExpression') {
|
|
295
|
+
return environmentReference(node.left)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (node.type !== 'MemberExpression' || node.property?.type !== 'Identifier') {
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const { object } = node
|
|
303
|
+
|
|
304
|
+
if (object?.type !== 'MemberExpression' || object.object?.name !== 'process' || object.property?.name !== 'env') {
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return node.property.name
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function collectEnvironmentReferences (node, path, found) {
|
|
312
|
+
if (!node) {
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (node.type === 'ObjectExpression') {
|
|
317
|
+
for (const property of node.properties) {
|
|
318
|
+
const key = property.key?.name ?? property.key?.value
|
|
319
|
+
|
|
320
|
+
if (key !== undefined) {
|
|
321
|
+
collectEnvironmentReferences(property.value, [...path, key], found)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (node.type === 'ArrayExpression') {
|
|
329
|
+
node.elements.forEach((element, index) => collectEnvironmentReferences(element, [...path, index], found))
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const name = environmentReference(node)
|
|
334
|
+
|
|
335
|
+
if (name) {
|
|
336
|
+
found.set(path.join('.'), name)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/*
|
|
341
|
+
Which values in a configuration come from the environment, by their path in it.
|
|
342
|
+
|
|
343
|
+
A v3 configuration said this in its data: `"port": "{PORT}"` survives being read as JSON. A v4
|
|
344
|
+
configuration says it in its code, and reading the file gives you the value the expression
|
|
345
|
+
produced -- `undefined`, for a variable that is not set in the process doing the reading. So a
|
|
346
|
+
tool that needs to know *which* variable a setting reads has to look at the source.
|
|
347
|
+
*/
|
|
348
|
+
export function readEnvironmentReferences (source) {
|
|
349
|
+
const { $ast: ast } = parseModule(source)
|
|
350
|
+
const found = new Map()
|
|
351
|
+
|
|
352
|
+
const declaration = ast.body.find(node => node.type === 'ExportDefaultDeclaration')?.declaration
|
|
353
|
+
|
|
354
|
+
if (!declaration) {
|
|
355
|
+
return found
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// The plain-object form exports the configuration; a factory or `defineConfig` passes it along.
|
|
359
|
+
const configuration = declaration.type === 'CallExpression' ? declaration.arguments[0] : declaration
|
|
360
|
+
|
|
361
|
+
collectEnvironmentReferences(configuration, [], found)
|
|
362
|
+
|
|
363
|
+
return found
|
|
364
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/generators",
|
|
3
|
-
"version": "4.0.0-new-config.
|
|
3
|
+
"version": "4.0.0-new-config.4",
|
|
4
4
|
"description": "Main classes and utils for generators.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
"change-case-all": "^2.1.0",
|
|
22
22
|
"execa": "^9.6.0",
|
|
23
23
|
"fastify": "^5.0.0",
|
|
24
|
+
"magicast": "^0.5.4",
|
|
24
25
|
"pino": "^9.9.0",
|
|
25
|
-
"undici": "^
|
|
26
|
-
"@platformatic/foundation": "4.0.0-new-config.
|
|
26
|
+
"undici": "^8.5.0",
|
|
27
|
+
"@platformatic/foundation": "4.0.0-new-config.4"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@types/inquirer": "^9.0.7",
|