Skip to content

Commit c907fda

Browse files
committed
fix: use clean temporary metadata for the build
1 parent f1b479b commit c907fda

6 files changed

Lines changed: 126 additions & 17 deletions

File tree

cmp/compiler/src/metadata/manager.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ export function createEmptyMetadata(): MetadataSchema {
2727
/**
2828
* Get the path to the metadata file
2929
*/
30-
export function getMetadataPath(config: MetadataConfig): string {
31-
return getMetadataPathUtil(config);
30+
export function getMetadataPath(
31+
config: MetadataConfig,
32+
filename?: string,
33+
): string {
34+
return getMetadataPathUtil(config, filename);
3235
}
3336

3437
/**
@@ -38,8 +41,9 @@ export function getMetadataPath(config: MetadataConfig): string {
3841
*/
3942
export async function loadMetadata(
4043
config: MetadataConfig,
44+
filename?: string,
4145
): Promise<MetadataSchema> {
42-
const metadataPath = getMetadataPath(config);
46+
const metadataPath = getMetadataPath(config, filename);
4347

4448
try {
4549
const content = await withTimeout(
@@ -64,8 +68,9 @@ export async function loadMetadata(
6468
export async function saveMetadata(
6569
config: MetadataConfig,
6670
metadata: MetadataSchema,
71+
filename?: string,
6772
): Promise<void> {
68-
const metadataPath = getMetadataPath(config);
73+
const metadataPath = getMetadataPath(config, filename);
6974
await withTimeout(
7075
fs.mkdir(path.dirname(metadataPath), { recursive: true }),
7176
DEFAULT_TIMEOUTS.FILE_IO,
@@ -90,13 +95,15 @@ export async function saveMetadata(
9095
*
9196
* @param config - Metadata configuration
9297
* @param entries - Translation entries to add/update
98+
* @param filename - Optional custom metadata filename
9399
* @returns The updated metadata schema
94100
*/
95101
export async function saveMetadataWithEntries(
96102
config: MetadataConfig,
97103
entries: TranslationEntry[],
104+
filename?: string,
98105
): Promise<MetadataSchema> {
99-
const metadataPath = getMetadataPath(config);
106+
const metadataPath = getMetadataPath(config, filename);
100107
const lockDir = path.dirname(metadataPath);
101108

102109
// Ensure directory exists before locking
@@ -125,13 +132,13 @@ export async function saveMetadataWithEntries(
125132

126133
try {
127134
// Re-load metadata inside lock to get latest state
128-
const currentMetadata = await loadMetadata(config);
135+
const currentMetadata = await loadMetadata(config, filename);
129136

130137
// Apply updates
131138
const updatedMetadata = upsertEntries(currentMetadata, entries);
132139

133140
// Save
134-
await saveMetadata(config, updatedMetadata);
141+
await saveMetadata(config, updatedMetadata, filename);
135142

136143
return updatedMetadata;
137144
} finally {

cmp/compiler/src/plugin/build-translator.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ export interface BuildTranslationOptions {
2626
* If not provided, files won't be generated
2727
*/
2828
publicOutputPath?: string;
29+
30+
/**
31+
* Custom metadata filename for build mode
32+
* If not provided, uses default metadata.json
33+
*/
34+
metadataFilename?: string;
2935
}
3036

3137
export interface BuildTranslationResult {
@@ -60,7 +66,7 @@ export interface BuildTranslationResult {
6066
export async function processBuildTranslations(
6167
options: BuildTranslationOptions,
6268
): Promise<BuildTranslationResult> {
63-
const { config, publicOutputPath } = options;
69+
const { config, publicOutputPath, metadataFilename } = options;
6470

6571
// Determine build mode (env var > options > config)
6672
const buildMode =
@@ -69,7 +75,7 @@ export async function processBuildTranslations(
6975

7076
logger.info(`🌍 Build mode: ${buildMode}`);
7177

72-
const metadata = await loadMetadata(config);
78+
const metadata = await loadMetadata(config, metadataFilename);
7379

7480
if (!metadata || Object.keys(metadata.entries).length === 0) {
7581
logger.info("No translations to process (metadata is empty)");

cmp/compiler/src/plugin/next.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@ import { logger } from "../utils/logger";
1111
import type { PartialLingoConfig } from "../types";
1212
import { lingoUnplugin } from "./unplugin";
1313
import { useI18nRegex } from "./transform/use-i18n";
14+
import {
15+
generateBuildMetadataFilename,
16+
getMetadataPath,
17+
} from "../utils/path-helpers";
18+
import * as fs from "fs/promises";
1419

1520
export type LingoNextPluginOptions = PartialLingoConfig;
1621

22+
// Track build metadata filename per config (use WeakMap for memory safety)
23+
const buildMetadataFilenames = new WeakMap<PartialLingoConfig, string>();
24+
1725
/**
1826
* Check if Next.js supports stable turbopack config (Next.js 16+)
1927
*/
@@ -70,6 +78,10 @@ function buildLingoConfig(
7078
): NextConfig {
7179
const lingoConfig = createLingoConfig(lingoOptions);
7280

81+
// Generate timestamped metadata filename for build mode
82+
const buildMetadataFilename = generateBuildMetadataFilename();
83+
buildMetadataFilenames.set(lingoOptions, buildMetadataFilename);
84+
7385
// Prepare Turbopack loader configuration
7486
const loaderConfig = {
7587
loader: "@lingo.dev/compiler/turbopack-loader",
@@ -78,6 +90,7 @@ function buildLingoConfig(
7890
lingoDir: lingoConfig.lingoDir,
7991
sourceLocale: lingoConfig.sourceLocale,
8092
useDirective: lingoConfig.useDirective,
93+
buildMetadataFilename,
8194
},
8295
};
8396

@@ -201,14 +214,32 @@ function buildLingoConfig(
201214
}
202215

203216
logger.info("Running post-build translation generation...");
217+
logger.info(
218+
`Build mode: Using temporary metadata file: ${buildMetadataFilename}`,
219+
);
204220

205221
try {
206222
const { processBuildTranslations } = await import("./build-translator");
207223

208224
await processBuildTranslations({
209225
config: lingoConfig,
210226
publicOutputPath: distDir,
227+
metadataFilename: buildMetadataFilename,
211228
});
229+
230+
// Cleanup: Remove temporary build metadata file
231+
const metadataPath = getMetadataPath(lingoConfig, buildMetadataFilename);
232+
try {
233+
await fs.unlink(metadataPath);
234+
logger.info(
235+
`🧹 Cleaned up temporary metadata file: ${buildMetadataFilename}`,
236+
);
237+
} catch (error: any) {
238+
// Ignore if file doesn't exist
239+
if (error.code !== "ENOENT") {
240+
logger.warn(`Failed to cleanup metadata file: ${error.message}`);
241+
}
242+
}
212243
} catch (error) {
213244
logger.error("Translation generation failed:", error);
214245
throw error;

cmp/compiler/src/plugin/turbopack-loader.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export default async function lingoCompilerTurbopackLoader(
2323
const isDev = process.env.NODE_ENV === "development";
2424

2525
try {
26-
const config: LingoConfig = this.getOptions();
26+
const config: LingoConfig & { buildMetadataFilename: string } =
27+
this.getOptions();
2728

2829
// TODO (AleksandrSl 07/12/2025): Remove too I think
2930
// Check if this file should be transformed
@@ -44,11 +45,13 @@ export default async function lingoCompilerTurbopackLoader(
4445
}
4546

4647
// Load current metadata
47-
const metadata = await loadMetadata(config);
48+
// In build mode, use timestamped metadata file; in dev mode, use default
49+
const metadataFilename = isDev ? undefined : config.buildMetadataFilename;
50+
const metadata = await loadMetadata(config, metadataFilename);
4851
// Update metadata with new entries
4952
if (result.newEntries && result.newEntries.length > 0) {
5053
const updatedMetadata = upsertEntries(metadata, result.newEntries);
51-
await saveMetadata(config, updatedMetadata);
54+
await saveMetadata(config, updatedMetadata, metadataFilename);
5255

5356
// Log new translations discovered (in dev mode)
5457
// Note: In production, translations are generated after build via runAfterProductionCompile

cmp/compiler/src/plugin/unplugin.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import {
2222
import { saveMetadataWithEntries } from "../metadata/manager";
2323
import { createLingoConfig } from "../utils/config-factory";
2424
import { logger } from "../utils/logger";
25-
import { getCacheDir } from "../utils/path-helpers";
25+
import {
26+
generateBuildMetadataFilename,
27+
getCacheDir,
28+
getMetadataPath,
29+
} from "../utils/path-helpers";
2630
import { useI18nRegex } from "./transform/use-i18n";
2731
import {
2832
generateClientLocaleCode,
@@ -34,6 +38,7 @@ import * as fs from "fs";
3438
export type LingoPluginOptions = PartialLingoConfig;
3539

3640
let globalServer: TranslationServer;
41+
let buildMetadataFilename: string | undefined;
3742

3843
/**
3944
* Universal plugin for Lingo.dev compiler
@@ -51,6 +56,16 @@ export const lingoUnplugin = createUnplugin<LingoPluginOptions>((options) => {
5156

5257
// Start translation server on build start (dev mode only)
5358
async buildStart() {
59+
// Generate timestamped metadata filename for build mode
60+
if (!isDev) {
61+
buildMetadataFilename = generateBuildMetadataFilename();
62+
logger.info(
63+
`Build mode: Using temporary metadata file: ${buildMetadataFilename}`,
64+
);
65+
} else {
66+
buildMetadataFilename = undefined;
67+
}
68+
5469
// Only start translation server in development mode
5570
if (isDev && !globalServer) {
5671
globalServer = await startTranslationServer({
@@ -172,7 +187,12 @@ export function persistLocale(locale) {
172187

173188
// TODO (AleksandrSl 30/11/2025): Could make async in the future, so we don't pause the main transform, translation server should be able to know if the metadata is finished writing then.
174189
// Thread-safe atomic update
175-
await saveMetadataWithEntries(config, result.newEntries);
190+
// In build mode, use timestamped metadata file; in dev mode, use default
191+
await saveMetadataWithEntries(
192+
config,
193+
result.newEntries,
194+
buildMetadataFilename,
195+
);
176196

177197
// Log new translations discovered (in dev mode)
178198
if (isDev) {
@@ -206,7 +226,27 @@ export function persistLocale(locale) {
206226
await processBuildTranslations({
207227
config,
208228
publicOutputPath: "public/translations",
229+
metadataFilename: buildMetadataFilename,
209230
});
231+
232+
// Cleanup: Remove temporary build metadata file
233+
if (buildMetadataFilename) {
234+
const metadataPath = getMetadataPath(config, buildMetadataFilename);
235+
try {
236+
await fs.promises.unlink(metadataPath);
237+
logger.info(
238+
`🧹 Cleaned up temporary metadata file: ${buildMetadataFilename}`,
239+
);
240+
} catch (error: any) {
241+
// Ignore if file doesn't exist
242+
if (error.code !== "ENOENT") {
243+
logger.warn(
244+
`Failed to cleanup metadata file: ${error.message}`,
245+
);
246+
}
247+
}
248+
buildMetadataFilename = undefined;
249+
}
210250
} catch (error) {
211251
logger.error("Build-time translation processing failed:", error);
212252
throw error;

cmp/compiler/src/utils/path-helpers.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,39 @@ export function resolveAbsolutePath(
3232
* Get the absolute path to the metadata.json file
3333
*
3434
* @param config - Config with sourceRoot and lingoDir
35-
* @returns Absolute path to metadata.json
35+
* @param filename - Optional custom filename (defaults to "metadata.json")
36+
* @returns Absolute path to metadata file
3637
*
3738
* @example
3839
* ```typescript
3940
* getMetadataPath({ sourceRoot: "src", lingoDir: ".lingo" })
4041
* // -> "/full/path/to/src/.lingo/metadata.json"
42+
*
43+
* getMetadataPath({ sourceRoot: "src", lingoDir: ".lingo" }, "metadata.build-123456.json")
44+
* // -> "/full/path/to/src/.lingo/metadata.build-123456.json"
4145
* ```
4246
*/
43-
export function getMetadataPath(config: PathConfig): string {
47+
export function getMetadataPath(
48+
config: PathConfig,
49+
filename: string = "metadata.json",
50+
): string {
4451
const rootPath = resolveAbsolutePath(config.sourceRoot);
45-
return path.join(rootPath, config.lingoDir, "metadata.json");
52+
return path.join(rootPath, config.lingoDir, filename);
53+
}
54+
55+
/**
56+
* Generate a unique build metadata filename with timestamp
57+
*
58+
* @returns Filename like "metadata.build-1701234567890.json"
59+
*
60+
* @example
61+
* ```typescript
62+
* generateBuildMetadataFilename()
63+
* // -> "metadata.build-1701234567890.json"
64+
* ```
65+
*/
66+
export function generateBuildMetadataFilename(): string {
67+
return `metadata.build-${Date.now()}.json`;
4668
}
4769

4870
/**

0 commit comments

Comments
 (0)