diff --git a/.changeset/big-months-lose.md b/.changeset/big-months-lose.md new file mode 100644 index 0000000..e493549 --- /dev/null +++ b/.changeset/big-months-lose.md @@ -0,0 +1,5 @@ +--- +"@webiny/data-transfer": patch +--- + +Fix `writeEnv` to support plain `KEY=value` format in `.env.example` — older scaffolded projects no longer crash during the wizard. Both `{{TOKEN}}` placeholders and plain env lines are handled; falls back to the built-in template when no `.env.example` exists. diff --git a/__tests__/commands/transfer/wizard/envWriter.test.ts b/__tests__/commands/transfer/wizard/envWriter.test.ts index a14d95c..d24c3f0 100644 --- a/__tests__/commands/transfer/wizard/envWriter.test.ts +++ b/__tests__/commands/transfer/wizard/envWriter.test.ts @@ -80,10 +80,103 @@ describe("writeEnv", () => { expect(content).toContain("SEGMENTS=8"); }); - it("throws when .env.example exists but has no {{tokens}}", async () => { - await writeFile(join(tmpDir, ".env.example"), "# no tokens here\n"); + it("substitutes values in a plain KEY=value .env.example (no {{tokens}})", async () => { + const plain = [ + "# Source", + "SOURCE_REGION=eu-central-1", + "SOURCE_DDB_TABLE=", + "SOURCE_S3_BUCKET=old-bucket", + "# comment preserved", + "", + "SEGMENTS=4", + "CUSTOM_KEY=untouched" + ].join("\n"); + await writeFile(join(tmpDir, ".env.example"), plain); + + await writeEnv(tmpDir, SAMPLE_VALUES); - await expect(writeEnv(tmpDir, SAMPLE_VALUES)).rejects.toThrow(/{{TOKEN}}/); + const content = await readFile(join(tmpDir, ".env"), "utf8"); + expect(content).toContain("SOURCE_REGION=eu-central-1"); + expect(content).toContain("SOURCE_DDB_TABLE=wby-source-primary"); + expect(content).toContain("SOURCE_S3_BUCKET=wby-source-bucket"); + expect(content).toContain("SEGMENTS=8"); + expect(content).toContain("# comment preserved"); + expect(content).toContain("CUSTOM_KEY=untouched"); + expect(content).not.toContain("old-bucket"); + }); + + it("replaces all known keys in plain format", async () => { + const plain = [ + "SOURCE_REGION=eu-central-1", + "SOURCE_DDB_TABLE=", + "SOURCE_S3_BUCKET=", + "SOURCE_AUDIT_LOGS_TABLE=", + "SOURCE_OS_TABLE=", + "SOURCE_ACCOUNT_ID=", + "TARGET_REGION=eu-central-1", + "TARGET_DDB_TABLE=", + "TARGET_S3_BUCKET=", + "TARGET_AUDIT_LOGS_TABLE=", + "TARGET_OS_TABLE=", + "TARGET_OS_ENDPOINT=", + "TARGET_OS_INDEX_PREFIX=", + "TARGET_ACCOUNT_ID=", + "SEGMENTS=4" + ].join("\n"); + await writeFile(join(tmpDir, ".env.example"), plain); + + await writeEnv(tmpDir, SAMPLE_VALUES); + + const content = await readFile(join(tmpDir, ".env"), "utf8"); + expect(content).toContain("SOURCE_REGION=eu-central-1"); + expect(content).toContain("SOURCE_DDB_TABLE=wby-source-primary"); + expect(content).toContain("SOURCE_S3_BUCKET=wby-source-bucket"); + expect(content).toContain("SOURCE_OS_TABLE=wby-source-es"); + expect(content).toContain("SOURCE_ACCOUNT_ID=111111111111"); + expect(content).toContain("TARGET_REGION=us-east-1"); + expect(content).toContain("TARGET_DDB_TABLE=wby-target-primary"); + expect(content).toContain("TARGET_S3_BUCKET=wby-target-bucket"); + expect(content).toContain("TARGET_AUDIT_LOGS_TABLE=wby-target-audit-logs"); + expect(content).toContain("TARGET_OS_TABLE=wby-target-os"); + expect(content).toContain("TARGET_OS_ENDPOINT=search-target.us-east-1.es.amazonaws.com"); + expect(content).toContain("TARGET_OS_INDEX_PREFIX=my-prefix"); + expect(content).toContain("TARGET_ACCOUNT_ID=222222222222"); + expect(content).toContain("SEGMENTS=8"); + }); + + it("plain format preserves commented-out lines and blank lines", async () => { + const plain = [ + "# --- Source ---", + "SOURCE_REGION=eu-central-1", + "", + "# SOURCE_PROFILE=my-source-profile", + "SOURCE_DDB_TABLE=", + "# --- Target ---", + "TARGET_REGION=us-east-1", + "# TARGET_PROFILE=my-target-profile", + "TARGET_DDB_TABLE=" + ].join("\n"); + await writeFile(join(tmpDir, ".env.example"), plain); + + await writeEnv(tmpDir, SAMPLE_VALUES); + + const content = await readFile(join(tmpDir, ".env"), "utf8"); + expect(content).toContain("# --- Source ---"); + expect(content).toContain("# SOURCE_PROFILE=my-source-profile"); + expect(content).toContain("# --- Target ---"); + expect(content).toContain("# TARGET_PROFILE=my-target-profile"); + expect(content).toContain("\n\n"); + }); + + it("plain format writes empty string for empty values", async () => { + const values: EnvValues = { ...SAMPLE_VALUES, targetOsIndexPrefix: "" }; + const plain = "TARGET_OS_INDEX_PREFIX=old-prefix\n"; + await writeFile(join(tmpDir, ".env.example"), plain); + + await writeEnv(tmpDir, values); + + const content = await readFile(join(tmpDir, ".env"), "utf8"); + expect(content).toBe("TARGET_OS_INDEX_PREFIX=\n"); }); it("uses built-in template when .env.example is absent", async () => { diff --git a/src/commands/transfer/wizard/envWriter.ts b/src/commands/transfer/wizard/envWriter.ts index a058263..3bb0d1c 100644 --- a/src/commands/transfer/wizard/envWriter.ts +++ b/src/commands/transfer/wizard/envWriter.ts @@ -78,27 +78,42 @@ function substituteTokens(template: string, values: EnvValues): string { return result; } -export async function writeEnv(projectDir: string, values: EnvValues): Promise { - let template = BUILT_IN_TEMPLATE; +function substituteEnvLines(content: string, values: EnvValues): string { + return content + .split("\n") + .map(line => { + const trimmed = line.trimStart(); + if (!trimmed || trimmed.startsWith("#")) { + return line; + } + const eqIndex = trimmed.indexOf("="); + if (eqIndex === -1) { + return line; + } + const key = trimmed.slice(0, eqIndex).trim(); + const envKey = TOKEN_MAP[key]; + if (envKey) { + return `${key}=${String(values[envKey])}`; + } + return line; + }) + .join("\n"); +} +export async function writeEnv(projectDir: string, values: EnvValues): Promise { const examplePath = join(projectDir, ".env.example"); + let content: string; + try { const candidate = await readFile(examplePath, "utf8"); - if (!candidate.includes("{{")) { - throw new Error( - `.env.example at ${examplePath} contains no {{TOKEN}} placeholders. ` + - `Add placeholders or remove the file to use the built-in template.` - ); - } - template = candidate; - } catch (err) { - if ((err as NodeJS.ErrnoException).code === "ENOENT") { - // no .env.example — use built-in + if (candidate.includes("{{")) { + content = substituteTokens(candidate, values); } else { - throw err; + content = substituteEnvLines(candidate, values); } + } catch { + content = substituteTokens(BUILT_IN_TEMPLATE, values); } - const content = substituteTokens(template, values); await writeFile(join(projectDir, ".env"), content, "utf8"); } diff --git a/templates/projects/example/.env.example b/templates/projects/example/.env.example index ebdc968..3bc1c78 100644 --- a/templates/projects/example/.env.example +++ b/templates/projects/example/.env.example @@ -6,18 +6,20 @@ SOURCE_REGION=eu-central-1 SOURCE_PROFILE=default SOURCE_DDB_TABLE= SOURCE_S3_BUCKET= +SOURCE_ACCOUNT_ID= +SOURCE_AUDIT_LOGS_TABLE= +SOURCE_OS_TABLE= # --- Target environment --- TARGET_REGION=eu-central-1 TARGET_PROFILE=default TARGET_DDB_TABLE= TARGET_S3_BUCKET= - -# --- OpenSearch (uncomment if used) --- -# SOURCE_OS_TABLE= -# TARGET_OS_TABLE= -# TARGET_OS_ENDPOINT= -# TARGET_OS_INDEX_PREFIX= +TARGET_ACCOUNT_ID= +TARGET_AUDIT_LOGS_TABLE= +TARGET_OS_TABLE= +TARGET_OS_ENDPOINT= +TARGET_OS_INDEX_PREFIX= # --- Tuning --- SEGMENTS=4