@vertile-ai/iac 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +73 -37
  2. package/docs/README.md +3 -2
  3. package/docs/index.html +2 -2
  4. package/docs/manifest.md +38 -5
  5. package/examples/bun-hono-api/README.md +9 -0
  6. package/examples/bun-hono-api/infrastructure/iac/iac.do.json +87 -0
  7. package/examples/bun-hono-api/infrastructure/iac/iac.json +107 -0
  8. package/examples/bun-hono-api/package.json +15 -0
  9. package/examples/bun-hono-api/src/index.ts +7 -0
  10. package/examples/go-api/README.md +8 -0
  11. package/examples/go-api/cmd/server/main.go +16 -0
  12. package/examples/go-api/go.mod +3 -0
  13. package/examples/go-api/infrastructure/iac/iac.do.json +90 -0
  14. package/examples/go-api/infrastructure/iac/iac.json +106 -0
  15. package/examples/next-monorepo/README.md +22 -0
  16. package/examples/next-monorepo/apps/admin/package.json +7 -0
  17. package/examples/next-monorepo/apps/web/package.json +7 -0
  18. package/examples/next-monorepo/infrastructure/iac/iac.aws.json +172 -0
  19. package/examples/next-monorepo/infrastructure/iac/iac.do.json +129 -0
  20. package/examples/{dynomic → next-monorepo}/infrastructure/iac/iac.json +102 -23
  21. package/examples/next-monorepo/infrastructure/iac/iac.vercel.json +109 -0
  22. package/examples/{dynomic → next-monorepo}/package.json +2 -2
  23. package/examples/node-api/README.md +8 -0
  24. package/examples/node-api/infrastructure/iac/iac.aws.json +132 -0
  25. package/examples/node-api/infrastructure/iac/iac.json +111 -0
  26. package/examples/node-api/package.json +12 -0
  27. package/examples/node-api/src/server.js +8 -0
  28. package/examples/python-fastapi-api/README.md +8 -0
  29. package/examples/python-fastapi-api/app/main.py +8 -0
  30. package/examples/python-fastapi-api/infrastructure/iac/iac.aws.json +129 -0
  31. package/examples/python-fastapi-api/infrastructure/iac/iac.json +113 -0
  32. package/examples/python-fastapi-api/pyproject.toml +10 -0
  33. package/examples/react-spa/README.md +8 -0
  34. package/examples/react-spa/index.html +2 -0
  35. package/examples/react-spa/infrastructure/iac/iac.aws.json +115 -0
  36. package/examples/react-spa/infrastructure/iac/iac.json +111 -0
  37. package/examples/react-spa/infrastructure/iac/iac.vercel.json +76 -0
  38. package/examples/react-spa/package.json +19 -0
  39. package/examples/react-spa/src/main.jsx +8 -0
  40. package/examples/sveltekit-web/README.md +9 -0
  41. package/examples/sveltekit-web/infrastructure/iac/iac.json +76 -0
  42. package/examples/sveltekit-web/infrastructure/iac/iac.vercel.json +74 -0
  43. package/examples/sveltekit-web/package.json +19 -0
  44. package/examples/sveltekit-web/src/routes/+page.svelte +3 -0
  45. package/examples/sveltekit-web/svelte.config.js +7 -0
  46. package/package.json +1 -1
  47. package/schema/iac.schema.json +83 -2
  48. package/src/apply.mjs +3 -4
  49. package/src/cli.mjs +1 -0
  50. package/src/core/context.mjs +4 -2
  51. package/src/core/deployments.mjs +39 -0
  52. package/src/core/env-files.mjs +38 -0
  53. package/src/core/env-source.mjs +5 -0
  54. package/src/core/hcl.mjs +2 -1
  55. package/src/core/manifest.mjs +15 -1
  56. package/src/core/render.mjs +19 -8
  57. package/src/core/vercel-manifests.mjs +5 -11
  58. package/src/plan.mjs +3 -4
  59. package/src/providers/aws/index.mjs +43 -24
  60. package/src/provision-env.mjs +72 -24
  61. package/src/render.mjs +3 -4
  62. package/src/shared.mjs +0 -4
  63. package/src/sync-env.mjs +33 -38
  64. package/examples/dynomic/README.md +0 -22
  65. package/examples/dynomic/apps/admin/package.json +0 -7
  66. package/examples/dynomic/apps/web/package.json +0 -7
  67. /package/examples/{dynomic → next-monorepo}/apps/admin/vercel.json +0 -0
  68. /package/examples/{dynomic → next-monorepo}/apps/web/vercel.json +0 -0
@@ -34,6 +34,8 @@ export function resolvePlatformContext(argv) {
34
34
  }
35
35
  }
36
36
 
37
- export function targetWorkspace(context, target) {
38
- return path.join(context.generatedRoot, target)
37
+ export function targetWorkspace(context, target, deploymentName = '') {
38
+ return deploymentName
39
+ ? path.join(context.generatedRoot, target, deploymentName)
40
+ : path.join(context.generatedRoot, target)
39
41
  }
@@ -0,0 +1,39 @@
1
+ function asObject(value) {
2
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
3
+ }
4
+
5
+ export function providerDeployments(manifest, target) {
6
+ return asObject(asObject(manifest.providers[target]).deployments)
7
+ }
8
+
9
+ export function resolveDeployment({ manifest, target, environment, deploymentName = '' }) {
10
+ const deployments = providerDeployments(manifest, target)
11
+ const name = deploymentName || (deployments[environment] ? environment : '')
12
+
13
+ if (!name) {
14
+ return {
15
+ name: '',
16
+ environment,
17
+ values: {},
18
+ }
19
+ }
20
+
21
+ if (!deployments[name] && Object.keys(deployments).length === 0) {
22
+ return {
23
+ name: '',
24
+ environment,
25
+ values: {},
26
+ }
27
+ }
28
+
29
+ if (!deployments[name]) {
30
+ throw new Error(`Unknown ${target} deployment "${name}". Use one of: ${Object.keys(deployments).join(', ')}`)
31
+ }
32
+
33
+ const values = asObject(deployments[name])
34
+ return {
35
+ name,
36
+ environment: values.environment || environment,
37
+ values,
38
+ }
39
+ }
@@ -0,0 +1,38 @@
1
+ const defaultEnvironmentFiles = {
2
+ development: ['.env.development'],
3
+ local: ['.env.local'],
4
+ preview: ['.env.staging'],
5
+ production: ['.env.production'],
6
+ staging: ['.env.staging'],
7
+ test: ['.env.test'],
8
+ }
9
+
10
+ function asObject(value) {
11
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
12
+ }
13
+
14
+ function asFiles(value) {
15
+ if (Array.isArray(value)) return value
16
+ if (typeof value === 'string' && value.trim()) return [value]
17
+ return []
18
+ }
19
+
20
+ export function environmentConfig(manifest, environment) {
21
+ const configured = asObject(asObject(manifest.env).environments)[environment]
22
+ if (typeof configured === 'string' || Array.isArray(configured)) {
23
+ return { files: asFiles(configured) }
24
+ }
25
+ return asObject(configured)
26
+ }
27
+
28
+ export function environmentFiles(manifest, environment) {
29
+ const config = environmentConfig(manifest, environment)
30
+ const files = asFiles(config.files || config.sources || config.file)
31
+ if (files.length > 0) return files
32
+ return defaultEnvironmentFiles[environment] || [`.env.${environment}`]
33
+ }
34
+
35
+ export function environmentOutputFile(manifest, environment) {
36
+ const config = environmentConfig(manifest, environment)
37
+ return config.output || config.outputFile || defaultEnvironmentFiles[environment]?.[0] || `.env.${environment}`
38
+ }
@@ -0,0 +1,5 @@
1
+ export const defaultEnvSourceDir = '.vertile-iac/env'
2
+
3
+ export function envSourceDir(manifest) {
4
+ return manifest.env.sourceDir || defaultEnvSourceDir
5
+ }
package/src/core/hcl.mjs CHANGED
@@ -81,10 +81,11 @@ export function renderGenericResources(resources = []) {
81
81
  .join('\n\n')
82
82
  }
83
83
 
84
- export function renderLocals(manifest, environment) {
84
+ export function renderLocals(manifest, environment, deployment = {}) {
85
85
  return block('locals', [], {
86
86
  project_name: manifest.project.name,
87
87
  environment,
88
+ deployment: deployment.name || undefined,
88
89
  })
89
90
  }
90
91
 
@@ -56,6 +56,20 @@ function validateProviderResources(providers) {
56
56
  }
57
57
  }
58
58
 
59
+ function validateProviderDeployments(manifest) {
60
+ for (const [provider, config] of Object.entries(manifest.providers)) {
61
+ const deployments = asObject(config.deployments)
62
+ for (const [name, deployment] of Object.entries(deployments)) {
63
+ const environment = asObject(deployment).environment
64
+ if (environment && !manifest.environments.includes(environment)) {
65
+ throw new Error(
66
+ `iac.json providers.${provider}.deployments.${name}.environment must be one of: ${manifest.environments.join(', ')}`,
67
+ )
68
+ }
69
+ }
70
+ }
71
+ }
72
+
59
73
  export function validateManifest(manifest) {
60
74
  if (manifest.version !== 1) {
61
75
  throw new Error(`Unsupported iac.json version "${manifest.version}". Expected version 1.`)
@@ -65,6 +79,7 @@ export function validateManifest(manifest) {
65
79
  }
66
80
  assertStringArray(manifest.environments, 'environments')
67
81
  validateProviderResources(manifest.providers)
82
+ validateProviderDeployments(manifest)
68
83
  }
69
84
 
70
85
  export function normalizeManifest(rawManifest) {
@@ -80,7 +95,6 @@ export function normalizeManifest(rawManifest) {
80
95
  providers: asObject(manifest.providers),
81
96
  apps: Array.isArray(manifest.apps) ? manifest.apps.map(normalizeApp) : [],
82
97
  domains: Array.isArray(manifest.domains) ? manifest.domains : [],
83
- infraDir: manifest.infraDir,
84
98
  objectStorage: normalizeKeyedList(manifest, 'objectStorage'),
85
99
  databases: normalizeKeyedList(manifest, 'databases'),
86
100
  queues: normalizeKeyedList(manifest, 'queues'),
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
  import { targetWorkspace } from './context.mjs'
4
+ import { resolveDeployment } from './deployments.mjs'
5
+ import { assertEnvironment } from './manifest.mjs'
4
6
  import { renderTerraform as renderAws } from '../providers/aws/index.mjs'
5
7
  import { renderTerraform as renderDigitalOcean } from '../providers/digitalocean/index.mjs'
6
8
  import { renderTerraform as renderVercel } from '../providers/vercel/index.mjs'
@@ -11,28 +13,37 @@ const renderers = {
11
13
  vercel: renderVercel,
12
14
  }
13
15
 
14
- export function renderTarget({ manifest, environment, target }) {
16
+ export function renderTarget({ manifest, environment, target, deploymentName = '' }) {
15
17
  const render = renderers[target]
16
18
  if (!render) throw new Error(`No renderer registered for target "${target}".`)
17
- return render({ manifest, environment })
19
+ const deployment = resolveDeployment({ manifest, target, environment, deploymentName })
20
+ assertEnvironment(manifest, deployment.environment)
21
+ return render({ manifest, environment: deployment.environment, deployment })
18
22
  }
19
23
 
20
- export async function writeTarget({ context, manifest, environment, target }) {
21
- const workspace = targetWorkspace(context, target)
22
- const files = renderTarget({ manifest, environment, target })
24
+ export async function writeTarget({ context, manifest, environment, target, deploymentName = '' }) {
25
+ const deployment = resolveDeployment({ manifest, target, environment, deploymentName })
26
+ assertEnvironment(manifest, deployment.environment)
27
+ const workspace = targetWorkspace(context, target, deployment.name)
28
+ const files = renderTarget({
29
+ manifest,
30
+ environment: deployment.environment,
31
+ target,
32
+ deploymentName: deployment.name,
33
+ })
23
34
  await fs.mkdir(workspace, { recursive: true })
24
35
 
25
36
  for (const [name, contents] of Object.entries(files)) {
26
37
  await fs.writeFile(path.join(workspace, name), contents)
27
38
  }
28
39
 
29
- return { workspace, files }
40
+ return { workspace, files, deployment }
30
41
  }
31
42
 
32
- export async function writeTargets({ context, manifest, environment, targets }) {
43
+ export async function writeTargets({ context, manifest, environment, targets, deploymentName = '' }) {
33
44
  const rendered = []
34
45
  for (const target of targets) {
35
- rendered.push(await writeTarget({ context, manifest, environment, target }))
46
+ rendered.push(await writeTarget({ context, manifest, environment, target, deploymentName }))
36
47
  }
37
48
  return rendered
38
49
  }
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs'
2
+ import { envSourceDir } from './env-source.mjs'
2
3
  import { readManifest } from './manifest.mjs'
3
4
 
4
5
  const projectSettingKeys = [
@@ -50,16 +51,6 @@ function teamSlugFromManifest(manifest) {
50
51
  return config.teamSlug || config.team || ''
51
52
  }
52
53
 
53
- function envSourceDir(manifest) {
54
- return (
55
- manifest.env.sourceDir ||
56
- manifest.env.dir ||
57
- manifest.env.infraDir ||
58
- manifest.infraDir ||
59
- 'infrastructure'
60
- )
61
- }
62
-
63
54
  function configuredProjects(manifest) {
64
55
  return manifest.apps.map((app) => {
65
56
  const values = appVercelValues(app)
@@ -130,9 +121,12 @@ function projectDomains(manifest) {
130
121
  }
131
122
 
132
123
  export function vercelEnvManifestFromIac(manifest, context = {}) {
124
+ const config = vercelConfig(manifest)
133
125
  return {
134
126
  teamSlug: teamSlugFromManifest(manifest),
135
- infraDir: context.infraDir || envSourceDir(manifest),
127
+ sourceDir: envSourceDir(manifest),
128
+ environmentFiles: manifest.env?.environments || {},
129
+ targets: config.env?.targets || {},
136
130
  projects: configuredProjects(manifest),
137
131
  }
138
132
  }
package/src/plan.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  import process from 'node:process'
4
4
  import { parseTargetOption, readOption } from './core/args.mjs'
5
5
  import { resolvePlatformContext } from './core/context.mjs'
6
- import { assertEnvironment, readManifest } from './core/manifest.mjs'
6
+ import { readManifest } from './core/manifest.mjs'
7
7
  import { writeTargets } from './core/render.mjs'
8
8
  import { terraformPlan } from './core/terraform.mjs'
9
9
 
@@ -12,11 +12,10 @@ async function main() {
12
12
  const context = resolvePlatformContext(argv)
13
13
  const manifest = readManifest(context.manifestPath)
14
14
  const environment = readOption(argv, '--env') || 'production'
15
+ const deploymentName = readOption(argv, '--deployment') || ''
15
16
  const targets = parseTargetOption(argv)
16
17
 
17
- assertEnvironment(manifest, environment)
18
-
19
- const rendered = await writeTargets({ context, manifest, environment, targets })
18
+ const rendered = await writeTargets({ context, manifest, environment, targets, deploymentName })
20
19
  for (const item of rendered) {
21
20
  console.log(`Planning ${item.workspace}`)
22
21
  terraformPlan({
@@ -16,38 +16,54 @@ import {
16
16
  terraformVariableName,
17
17
  } from '../../core/concepts.mjs'
18
18
 
19
- function providerBlock(manifest, environment) {
19
+ function deploymentLabel(environment, deployment = {}) {
20
+ return deployment.name || environment
21
+ }
22
+
23
+ function providerBlock(manifest, environment, deployment = {}) {
20
24
  const config = manifest.providers.aws || {}
25
+ const deploymentValues = deployment.values || {}
21
26
  const body = {
22
- region: config.region,
27
+ region: deploymentValues.region || config.region,
28
+ profile: deploymentValues.profile || config.profile,
23
29
  }
24
- const tags = {
30
+ const tags = compactBody({
25
31
  Project: manifest.project.name,
26
32
  Environment: environment,
33
+ Deployment: deployment.name || undefined,
27
34
  ...(config.defaultTags || {}),
28
- }
35
+ ...(deploymentValues.tags || {}),
36
+ })
29
37
 
30
38
  const provider = block('provider', ['aws'], body)
31
- if (Object.keys(tags).length === 0) return provider
39
+ const assumeRole = deploymentValues.assumeRole || deploymentValues.assume_role || config.assumeRole || config.assume_role
40
+ const assumeRoleBlock = assumeRole
41
+ ? `\n ${nestedBlock('assume_role', assumeRole).replace(/\n/g, '\n ')}`
42
+ : ''
43
+ const defaultTagsBlock = Object.keys(tags).length > 0
44
+ ? `\n ${nestedBlock('default_tags', { tags }).replace(/\n/g, '\n ')}`
45
+ : ''
32
46
 
33
- return `${provider.slice(0, -1)}\n ${nestedBlock('default_tags', { tags }).replace(/\n/g, '\n ')}\n}`
47
+ return `${provider.slice(0, -1)}${assumeRoleBlock}${defaultTagsBlock}\n}`
34
48
  }
35
49
 
36
- function objectStorageBlocks(manifest, environment) {
50
+ function objectStorageBlocks(manifest, environment, deployment = {}) {
51
+ const nameEnvironment = deploymentLabel(environment, deployment)
37
52
  return manifest.objectStorage.map((item) => {
38
53
  const values = providerValues(item, 'aws')
39
54
  return block('resource', ['aws_s3_bucket', resourceName('object_storage', item.key)], compactBody({
40
- bucket: values.bucket || providerResourceName(manifest, environment, item),
55
+ bucket: values.bucket || providerResourceName(manifest, nameEnvironment, item),
41
56
  force_destroy: values.forceDestroy,
42
57
  }))
43
58
  })
44
59
  }
45
60
 
46
- function queueBlocks(manifest, environment) {
61
+ function queueBlocks(manifest, environment, deployment = {}) {
62
+ const nameEnvironment = deploymentLabel(environment, deployment)
47
63
  return manifest.queues.map((item) => {
48
64
  const values = providerValues(item, 'aws')
49
65
  const fifo = values.fifo || item.kind === 'fifo'
50
- const baseName = values.name || providerResourceName(manifest, environment, item)
66
+ const baseName = values.name || providerResourceName(manifest, nameEnvironment, item)
51
67
  return block('resource', ['aws_sqs_queue', resourceName('queue', item.key)], compactBody({
52
68
  name: fifo && !baseName.endsWith('.fifo') ? `${baseName}.fifo` : baseName,
53
69
  fifo_queue: fifo || undefined,
@@ -57,12 +73,13 @@ function queueBlocks(manifest, environment) {
57
73
  })
58
74
  }
59
75
 
60
- function databaseBlocks(manifest, environment) {
76
+ function databaseBlocks(manifest, environment, deployment = {}) {
77
+ const nameEnvironment = deploymentLabel(environment, deployment)
61
78
  return manifest.databases.map((item) => {
62
79
  const values = providerValues(item, 'aws')
63
80
  const variable = terraformVariableName('database', item.key, 'password')
64
81
  return block('resource', ['aws_db_instance', resourceName('database', item.key)], compactBody({
65
- identifier: values.identifier || providerResourceName(manifest, environment, item),
82
+ identifier: values.identifier || providerResourceName(manifest, nameEnvironment, item),
66
83
  engine: values.engine || item.engine || 'postgres',
67
84
  engine_version: values.engineVersion,
68
85
  instance_class: values.instanceClass || 'db.t4g.micro',
@@ -101,19 +118,21 @@ function amazonLinuxDataSource() {
101
118
  ].join('\n')
102
119
  }
103
120
 
104
- function computeBlocks(manifest, environment, field, prefix) {
121
+ function computeBlocks(manifest, environment, deployment, field, prefix) {
122
+ const nameEnvironment = deploymentLabel(environment, deployment)
105
123
  return manifest[field].map((item) => {
106
124
  const values = providerValues(item, 'aws')
107
125
  return block('resource', ['aws_instance', resourceName(prefix, item.key)], compactBody({
108
126
  ami: values.ami ? values.ami : raw('data.aws_ami.amazon_linux.id'),
109
127
  instance_type: values.instanceType || 't3.micro',
110
128
  count: field === 'clusters' ? values.size || item.size || 1 : undefined,
111
- tags: {
112
- Name: values.name || providerResourceName(manifest, environment, item),
129
+ tags: compactBody({
130
+ Name: values.name || providerResourceName(manifest, nameEnvironment, item),
113
131
  Project: manifest.project.name,
114
132
  Environment: environment,
133
+ Deployment: deployment.name || undefined,
115
134
  Kind: prefix,
116
- },
135
+ }),
117
136
  }))
118
137
  })
119
138
  }
@@ -127,19 +146,19 @@ function outputBlocks(manifest) {
127
146
  ))
128
147
  }
129
148
 
130
- export function renderTerraform({ manifest, environment }) {
149
+ export function renderTerraform({ manifest, environment, deployment = {} }) {
131
150
  const config = manifest.providers.aws || {}
132
151
  const resources = renderGenericResources(config.resources)
133
152
  const needsAmazonLinux = manifest.sandboxes.length > 0 || manifest.clusters.length > 0
134
153
  const mainBlocks = [
135
- renderLocals(manifest, environment),
136
- providerBlock(manifest, environment),
154
+ renderLocals(manifest, environment, deployment),
155
+ providerBlock(manifest, environment, deployment),
137
156
  needsAmazonLinux ? amazonLinuxDataSource() : '',
138
- ...objectStorageBlocks(manifest, environment),
139
- ...queueBlocks(manifest, environment),
140
- ...databaseBlocks(manifest, environment),
141
- ...computeBlocks(manifest, environment, 'sandboxes', 'sandbox'),
142
- ...computeBlocks(manifest, environment, 'clusters', 'cluster'),
157
+ ...objectStorageBlocks(manifest, environment, deployment),
158
+ ...queueBlocks(manifest, environment, deployment),
159
+ ...databaseBlocks(manifest, environment, deployment),
160
+ ...computeBlocks(manifest, environment, deployment, 'sandboxes', 'sandbox'),
161
+ ...computeBlocks(manifest, environment, deployment, 'clusters', 'cluster'),
143
162
  resources,
144
163
  ].filter(Boolean)
145
164
  const variableBlocks = databaseVariables(manifest)
@@ -3,6 +3,7 @@
3
3
  import fs from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import process from 'node:process'
6
+ import { environmentFiles } from './core/env-files.mjs'
6
7
  import { readVercelEnvManifest } from './core/vercel-manifests.mjs'
7
8
  import { resolveIacContext } from './shared.mjs'
8
9
 
@@ -140,6 +141,20 @@ function parseEnvFile(filePath) {
140
141
  return entries
141
142
  }
142
143
 
144
+ function mergeEntries(layers) {
145
+ const order = []
146
+ const values = new Map()
147
+
148
+ for (const layer of layers) {
149
+ for (const { key, value } of layer) {
150
+ if (!values.has(key)) order.push(key)
151
+ values.set(key, value)
152
+ }
153
+ }
154
+
155
+ return order.map((key) => ({ key, value: values.get(key) }))
156
+ }
157
+
143
158
  function readTokenFromFile(filePath) {
144
159
  if (!fs.existsSync(filePath)) return ''
145
160
 
@@ -162,24 +177,50 @@ function readTokenFromFile(filePath) {
162
177
  return ''
163
178
  }
164
179
 
165
- // Maps a Vercel target name to the .env filename used in infrastructure folders.
166
- const targetToEnvFile = {
167
- development: '.env.development',
168
- staging: '.env.staging',
169
- production: '.env.production',
180
+ function targetEnvironment(manifest, target) {
181
+ const configured = manifest.targets?.[target]
182
+ if (typeof configured === 'string') return configured
183
+ if (configured && typeof configured === 'object' && configured.environment) {
184
+ return configured.environment
185
+ }
186
+ return target === 'preview' ? 'staging' : target
170
187
  }
171
188
 
172
- function readInfraScopedEntries(infraDir, target, projects) {
173
- const envFile = targetToEnvFile[target]
189
+ function targetEnvFiles(manifest, environment) {
190
+ return environmentFiles({ env: { environments: manifest.environmentFiles || {} } }, environment)
191
+ }
174
192
 
175
- const teamEntries = parseEnvFile(path.join(rootDir, infraDir, 'shared', envFile))
193
+ function readEnvFiles(baseDir, files, { requireFiles = false } = {}) {
194
+ const filePaths = files.map((file) => path.join(baseDir, file))
195
+ const missing = filePaths.filter((filePath) => !fs.existsSync(filePath))
176
196
 
177
- const projectEntries = Object.fromEntries(
178
- projects.map((project) => [
179
- project.key,
180
- parseEnvFile(path.join(rootDir, infraDir, project.key, envFile)),
181
- ]),
182
- )
197
+ if (requireFiles && missing.length > 0) {
198
+ throw new Error(
199
+ `Missing required env source file(s) for --reconcile-delete: ${missing.map((filePath) => path.relative(rootDir, filePath)).join(', ')}`,
200
+ )
201
+ }
202
+
203
+ return mergeEntries(filePaths.map(parseEnvFile))
204
+ }
205
+
206
+ function readSourceScopedEntries(sourceDir, environment, projects, files, options = {}) {
207
+ const {
208
+ readTeam = true,
209
+ readProjects = true,
210
+ ...readOptions
211
+ } = options
212
+ const teamEntries = readTeam
213
+ ? readEnvFiles(path.join(rootDir, sourceDir, 'shared'), files, readOptions)
214
+ : []
215
+
216
+ const projectEntries = readProjects
217
+ ? Object.fromEntries(
218
+ projects.map((project) => [
219
+ project.key,
220
+ readEnvFiles(path.join(rootDir, sourceDir, project.key), files, readOptions),
221
+ ]),
222
+ )
223
+ : {}
183
224
 
184
225
  return { teamEntries, projectEntries }
185
226
  }
@@ -631,13 +672,13 @@ async function main() {
631
672
  const teamSlug = manifest.teamSlug
632
673
  const projects = Array.isArray(manifest.projects) ? manifest.projects : []
633
674
  const configuredProjects = [...projects]
634
- const infraDir = iacContext.infraDir || manifest.infraDir
675
+ const sourceDir = manifest.sourceDir
635
676
 
636
677
  if (!teamSlug) {
637
678
  throw new Error('Missing "teamSlug" in env manifest')
638
679
  }
639
- if (!infraDir) {
640
- throw new Error('Missing "infraDir" in env manifest')
680
+ if (!sourceDir) {
681
+ throw new Error('Missing "sourceDir" in env manifest')
641
682
  }
642
683
 
643
684
  let teamId = ''
@@ -746,12 +787,16 @@ async function main() {
746
787
  `${c.bold('Mode:')} ${dryRun ? c.yellow('dry-run') : c.green('apply')} ${c.gray('|')} scope=${c.cyan(args.scope)} ${c.gray('|')} targets=${c.cyan(args.targets.join(','))} ${c.gray('|')} reconcile-delete=${c.cyan(args.reconcileDelete ? 'on' : 'off')}`,
747
788
  )
748
789
 
749
- // preview deployments read from the staging env files
750
- const resolvedTarget = (target) => (target === 'preview' ? 'staging' : target)
751
-
752
790
  if (args.scope === 'team' || args.scope === 'all') {
753
791
  for (const target of args.targets) {
754
- const parsed = readInfraScopedEntries(infraDir, resolvedTarget(target), projects)
792
+ const environment = targetEnvironment(manifest, target)
793
+ const parsed = readSourceScopedEntries(
794
+ sourceDir,
795
+ environment,
796
+ [],
797
+ targetEnvFiles(manifest, environment),
798
+ { requireFiles: args.reconcileDelete, readProjects: false },
799
+ )
755
800
  await upsertTeamShared({
756
801
  token,
757
802
  dryRun,
@@ -767,10 +812,13 @@ async function main() {
767
812
  if (args.scope === 'projects' || args.scope === 'all') {
768
813
  const parsedByTarget = {}
769
814
  for (const target of args.targets) {
770
- parsedByTarget[target] = readInfraScopedEntries(
771
- infraDir,
772
- resolvedTarget(target),
815
+ const environment = targetEnvironment(manifest, target)
816
+ parsedByTarget[target] = readSourceScopedEntries(
817
+ sourceDir,
818
+ environment,
773
819
  selectedProjects,
820
+ targetEnvFiles(manifest, environment),
821
+ { requireFiles: args.reconcileDelete, readTeam: false },
774
822
  )
775
823
  }
776
824
 
package/src/render.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  import process from 'node:process'
4
4
  import { parseTargetOption, readOption } from './core/args.mjs'
5
5
  import { resolvePlatformContext } from './core/context.mjs'
6
- import { assertEnvironment, readManifest } from './core/manifest.mjs'
6
+ import { readManifest } from './core/manifest.mjs'
7
7
  import { writeTargets } from './core/render.mjs'
8
8
 
9
9
  async function main() {
@@ -11,11 +11,10 @@ async function main() {
11
11
  const context = resolvePlatformContext(argv)
12
12
  const manifest = readManifest(context.manifestPath)
13
13
  const environment = readOption(argv, '--env') || 'production'
14
+ const deploymentName = readOption(argv, '--deployment') || ''
14
15
  const targets = parseTargetOption(argv)
15
16
 
16
- assertEnvironment(manifest, environment)
17
-
18
- const rendered = await writeTargets({ context, manifest, environment, targets })
17
+ const rendered = await writeTargets({ context, manifest, environment, targets, deploymentName })
19
18
  for (const item of rendered) {
20
19
  console.log(`Rendered ${item.workspace}`)
21
20
  }
package/src/shared.mjs CHANGED
@@ -55,8 +55,6 @@ export function resolveIacContext(argv, defaults = {}) {
55
55
  const projectSettingsArg = readOption(argv, '--project-settings')
56
56
  const projectDomainsArg = readOption(argv, '--project-domains')
57
57
  const iacManifestArg = readOption(argv, '--iac-manifest')
58
- const infraDir =
59
- readOption(argv, '--infra-dir') || defaults.infraDir || ''
60
58
 
61
59
  const autoCreateKeys = new Set([
62
60
  ...splitList(defaults.autoCreateKeys || ''),
@@ -70,7 +68,6 @@ export function resolveIacContext(argv, defaults = {}) {
70
68
  return {
71
69
  repoRoot,
72
70
  iacDir,
73
- infraDir,
74
71
  manifestPath: resolveFrom(
75
72
  repoRoot,
76
73
  manifestArg || path.relative(repoRoot, path.join(iacDir, 'env-manifest.json')),
@@ -109,7 +106,6 @@ export function sharedOptionsHelp() {
109
106
  --project-settings <path> Project settings manifest path.
110
107
  --project-domains <path> Project domains manifest path.
111
108
  --iac-manifest <path> Unified IaC manifest path. Defaults to <iac-dir>/iac.json.
112
- --infra-dir <path> Override manifest infraDir.
113
109
  --token-file <path> Token file. Defaults to <repo-root>/.vercel.token.
114
110
  --auto-create-keys <a,b> Project keys allowed to be created in apply mode.
115
111
  --auto-create-prefixes <a,b> Project key prefixes allowed to be created in apply mode.`