( preloaded: LoadedComponent[], )
| 166 | // Each component's <name>.env is resolved as it is added, so the operator answers |
| 167 | // a component's env right after picking it — never a second batch pass. |
| 168 | export async function promptCustomComponents( |
| 169 | preloaded: LoadedComponent[], |
| 170 | ): Promise<{ components: DeployComponent[]; env: Record<string, string> }> { |
| 171 | const components: DeployComponent[] = []; |
| 172 | const env: Record<string, string> = {}; |
| 173 | const seen = new Set<string>(); |
| 174 | |
| 175 | // Take in a component: record it, then turn its env.example into env text. |
| 176 | const add = async (loaded: LoadedComponent): Promise<void> => { |
| 177 | seen.add(loaded.component.name); |
| 178 | components.push(loaded.component); |
| 179 | const text = await resolveComponentEnv(loaded.dir, loaded.component.name, { interactive: true }); |
| 180 | if (text !== null) env[loaded.component.name] = text; |
| 181 | }; |
| 182 | |
| 183 | if (preloaded.length > 0) { |
| 184 | process.stdout.write(` Already added: ${preloaded.map((c) => c.component.name).join(", ")}\n`); |
| 185 | } |
| 186 | for (const loaded of preloaded) await add(loaded); |
| 187 | |
| 188 | while ( |
| 189 | await confirm({ |
| 190 | message: components.length === 0 ? "Add a custom component?" : "Add another custom component?", |
| 191 | default: false, |
| 192 | }) |
| 193 | ) { |
| 194 | const dir = ( |
| 195 | await input({ |
| 196 | message: "Custom component folder", |
| 197 | // Full inline validation: path, component.json presence, contract shape, |
| 198 | // and a name not already taken — the same checks the batch path runs, so |
| 199 | // a bad folder is rejected here instead of failing late in the build. |
| 200 | validate: async (v) => { |
| 201 | const t = v.trim(); |
| 202 | if (!t) return "enter a folder path"; |
| 203 | if (!existsSync(t)) return `folder not found: ${t}`; |
| 204 | if (!existsSync(path.join(t, "component.json"))) return `no component.json in ${t}`; |
| 205 | let data: unknown; |
| 206 | try { |
| 207 | data = await readComponentJson(t); |
| 208 | } catch (err) { |
| 209 | return err instanceof Error ? err.message : String(err); |
| 210 | } |
| 211 | const errors = validateComponent(data); |
| 212 | if (errors.length > 0) return errors.join("; "); |
| 213 | const name = (data as DeployComponent).name; |
| 214 | return seen.has(name) ? `"${name}" already added` : true; |
| 215 | }, |
| 216 | }) |
| 217 | ).trim(); |
| 218 | |
| 219 | // The prompt's validator proved the shape; re-read for the typed value. |
| 220 | await add({ component: (await readComponentJson(dir)) as DeployComponent, dir }); |
| 221 | } |
| 222 | |
| 223 | return { components, env }; |
| 224 | } |
no test coverage detected