Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/big-months-lose.md
Original file line number Diff line number Diff line change
@@ -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.
99 changes: 96 additions & 3 deletions __tests__/commands/transfer/wizard/envWriter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
43 changes: 29 additions & 14 deletions src/commands/transfer/wizard/envWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,27 +78,42 @@ function substituteTokens(template: string, values: EnvValues): string {
return result;
}

export async function writeEnv(projectDir: string, values: EnvValues): Promise<void> {
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<void> {
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");
}
14 changes: 8 additions & 6 deletions templates/projects/example/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading