Skip to content

Commit 1bba8ee

Browse files
feat(compiler): support next.js with turbopack enabled (#932)
* feat: extremely experimental turbopack support * fix: remove uneeded code * fix: only apply turbopack is set to true in config * feat: add log * feat: add new todo * feat: turbopack config updates * fix: type adjustments * fix: update logging and add --turbo warning * fix: do not apply webpack config when turbopack is enabled * fix: remove some complicated logic for now (might move to seperate prs) * chore: `pnpm new` * fix: consolidate common logic for component transformation and dictionary loading * feat(compiler): support "auto" for turbopack.enabled --------- Co-authored-by: Max Prilutskiy <[email protected]>
1 parent 81af276 commit 1bba8ee

File tree

5 files changed

+274
-92
lines changed

5 files changed

+274
-92
lines changed

.changeset/gorgeous-olives-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@lingo.dev/_compiler": minor
3+
---
4+
5+
Add support for Next.js Turbopack with the Lingo.dev compiler.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import _ from "lodash";
2+
import path from "path";
3+
import { composeMutations, createOutput, createPayload } from "./_base";
4+
import { LCP_DICTIONARY_FILE_NAME } from "./_const";
5+
import { clientDictionaryLoaderMutation } from "./client-dictionary-loader";
6+
import i18nDirectiveMutation from "./i18n-directive";
7+
import jsxAttributeFlagMutation from "./jsx-attribute-flag";
8+
import { lingoJsxAttributeScopeInjectMutation } from "./jsx-attribute-scope-inject";
9+
import { jsxAttributeScopesExportMutation } from "./jsx-attribute-scopes-export";
10+
import { jsxFragmentMutation } from "./jsx-fragment";
11+
import { jsxHtmlLangMutation } from "./jsx-html-lang";
12+
import jsxProviderMutation from "./jsx-provider";
13+
import { jsxRemoveAttributesMutation } from "./jsx-remove-attributes";
14+
import jsxRootFlagMutation from "./jsx-root-flag";
15+
import jsxScopeFlagMutation from "./jsx-scope-flag";
16+
import { lingoJsxScopeInjectMutation } from "./jsx-scope-inject";
17+
import { jsxScopesExportMutation } from "./jsx-scopes-export";
18+
import { LCP } from "./lib/lcp";
19+
import { LCPServer } from "./lib/lcp/server";
20+
import { reactRouterDictionaryLoaderMutation } from "./react-router-dictionary-loader";
21+
import { rscDictionaryLoaderMutation } from "./rsc-dictionary-loader";
22+
import { parseParametrizedModuleId } from "./utils/module-params";
23+
24+
/**
25+
* Loads a dictionary for a specific locale
26+
*/
27+
export async function loadDictionary(options: {
28+
resourcePath: string;
29+
resourceQuery?: string;
30+
params: any;
31+
sourceRoot: string;
32+
lingoDir: string;
33+
isDev: boolean;
34+
}) {
35+
const { resourcePath, resourceQuery = "", params, sourceRoot, lingoDir, isDev } = options;
36+
const fullResourcePath = `${resourcePath}${resourceQuery}`;
37+
38+
if (!resourcePath.endsWith(LCP_DICTIONARY_FILE_NAME)) {
39+
return null; // Not a dictionary file
40+
}
41+
42+
const moduleInfo = parseParametrizedModuleId(fullResourcePath);
43+
const locale = moduleInfo.params.locale;
44+
45+
if (!locale) {
46+
return null; // No locale specified
47+
}
48+
49+
const lcpParams = {
50+
sourceRoot,
51+
lingoDir,
52+
isDev,
53+
};
54+
55+
await LCP.ready(lcpParams);
56+
const lcp = LCP.getInstance(lcpParams);
57+
58+
const dictionaries = await LCPServer.loadDictionaries({
59+
...params,
60+
lcp: lcp.data,
61+
});
62+
63+
const dictionary = dictionaries[locale];
64+
if (!dictionary) {
65+
throw new Error(
66+
`Lingo.dev: Dictionary for locale "${locale}" could not be generated.`,
67+
);
68+
}
69+
70+
return dictionary;
71+
}
72+
73+
/**
74+
* Transforms component code
75+
*/
76+
export function transformComponent(options: {
77+
code: string;
78+
params: any;
79+
resourcePath: string;
80+
sourceRoot: string;
81+
}) {
82+
const { code, params, resourcePath, sourceRoot } = options;
83+
84+
return _.chain({
85+
code,
86+
params,
87+
relativeFilePath: path
88+
.relative(path.resolve(process.cwd(), sourceRoot), resourcePath)
89+
.split(path.sep)
90+
.join("/"), // Always normalize for consistent dictionaries
91+
})
92+
.thru(createPayload)
93+
.thru(
94+
composeMutations(
95+
i18nDirectiveMutation,
96+
jsxFragmentMutation,
97+
jsxAttributeFlagMutation,
98+
jsxProviderMutation,
99+
jsxHtmlLangMutation,
100+
jsxRootFlagMutation,
101+
jsxScopeFlagMutation,
102+
jsxAttributeScopesExportMutation,
103+
jsxScopesExportMutation,
104+
lingoJsxAttributeScopeInjectMutation,
105+
lingoJsxScopeInjectMutation,
106+
rscDictionaryLoaderMutation,
107+
reactRouterDictionaryLoaderMutation,
108+
jsxRemoveAttributesMutation,
109+
clientDictionaryLoaderMutation,
110+
),
111+
)
112+
.thru(createOutput)
113+
.value();
114+
}

packages/compiler/src/index.ts

Lines changed: 109 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,10 @@ import type { NextConfig } from "next";
33
import packageJson from "../package.json";
44
import _ from "lodash";
55
import dedent from "dedent";
6-
import {
7-
composeMutations,
8-
createPayload,
9-
createOutput,
10-
defaultParams,
11-
} from "./_base";
12-
import i18nDirectiveMutation from "./i18n-directive";
13-
import jsxProviderMutation from "./jsx-provider";
14-
import jsxRootFlagMutation from "./jsx-root-flag";
15-
import jsxScopeFlagMutation from "./jsx-scope-flag";
16-
import jsxAttributeFlagMutation from "./jsx-attribute-flag";
17-
import path from "path";
18-
import { parseParametrizedModuleId } from "./utils/module-params";
19-
import { LCP } from "./lib/lcp";
20-
import { LCPServer } from "./lib/lcp/server";
21-
import { rscDictionaryLoaderMutation } from "./rsc-dictionary-loader";
22-
import { reactRouterDictionaryLoaderMutation } from "./react-router-dictionary-loader";
23-
import { jsxFragmentMutation } from "./jsx-fragment";
24-
import { jsxHtmlLangMutation } from "./jsx-html-lang";
25-
import { jsxAttributeScopesExportMutation } from "./jsx-attribute-scopes-export";
26-
import { jsxScopesExportMutation } from "./jsx-scopes-export";
27-
import { lingoJsxAttributeScopeInjectMutation } from "./jsx-attribute-scope-inject";
28-
import { lingoJsxScopeInjectMutation } from "./jsx-scope-inject";
29-
import { jsxRemoveAttributesMutation } from "./jsx-remove-attributes";
6+
import { defaultParams } from "./_base";
307
import { LCP_DICTIONARY_FILE_NAME } from "./_const";
318
import { LCPCache } from "./lib/lcp/cache";
329
import { getInvalidLocales } from "./utils/locales";
33-
import { clientDictionaryLoaderMutation } from "./client-dictionary-loader";
3410
import {
3511
getGroqKeyFromEnv,
3612
getGroqKeyFromRc,
@@ -41,6 +17,7 @@ import {
4117
} from "./utils/llm-api-key";
4218
import { isRunningInCIOrDocker } from "./utils/env";
4319
import { providerDetails } from "./lib/lcp/api/provider-details";
20+
import { loadDictionary, transformComponent } from "./_loader-utils";
4421

4522
const keyCheckers: Record<
4623
string,
@@ -86,12 +63,12 @@ const unplugin = createUnplugin<Partial<typeof defaultParams> | undefined>(
8663
console.log(dedent`
8764
\n
8865
⚠️ Lingo.dev Localization Compiler requires LLM model setup for the following locales: ${invalidLocales.join(", ")}.
89-
66+
9067
⭐️ Next steps:
9168
1. Refer to documentation for help: https://lingo.dev/compiler
9269
2. If you want to use a different LLM, raise an issue in our open-source repo: https://lingo.dev/go/gh
9370
3. If you have questions, feature requests, or would like to contribute, join our Discord: https://lingo.dev/go/discord
94-
71+
9572
9673
`);
9774
process.exit(1);
@@ -111,27 +88,23 @@ const unplugin = createUnplugin<Partial<typeof defaultParams> | undefined>(
11188
name: packageJson.name,
11289
loadInclude: (id) => !!id.match(LCP_DICTIONARY_FILE_NAME),
11390
async load(id) {
114-
const moduleInfo = parseParametrizedModuleId(id);
115-
116-
const lcpParams = {
91+
const dictionary = await loadDictionary({
92+
resourcePath: id,
93+
resourceQuery: "",
94+
params: {
95+
...params,
96+
models: params.models,
97+
sourceLocale: params.sourceLocale,
98+
targetLocales: params.targetLocales,
99+
},
117100
sourceRoot: params.sourceRoot,
118101
lingoDir: params.lingoDir,
119102
isDev,
120-
};
121-
122-
// wait for LCP file to be generated
123-
await LCP.ready(lcpParams);
124-
const lcp = LCP.getInstance(lcpParams);
125-
126-
const dictionaries = await LCPServer.loadDictionaries({
127-
models: params.models,
128-
lcp: lcp.data,
129-
sourceLocale: params.sourceLocale,
130-
targetLocales: params.targetLocales,
131-
sourceRoot: params.sourceRoot,
132-
lingoDir: params.lingoDir,
133103
});
134-
const dictionary = dictionaries[moduleInfo.params.locale];
104+
105+
if (!dictionary) {
106+
return null;
107+
}
135108

136109
console.log(JSON.stringify(dictionary, null, 2));
137110

@@ -143,44 +116,12 @@ const unplugin = createUnplugin<Partial<typeof defaultParams> | undefined>(
143116
enforce: "pre",
144117
transform(code, id) {
145118
try {
146-
const result = _.chain({
119+
const result = transformComponent({
147120
code,
148121
params,
149-
relativeFilePath: path
150-
.relative(path.resolve(process.cwd(), params.sourceRoot), id)
151-
.split(path.sep)
152-
.join("/"), // Always normalize for consistent dictionaries
153-
})
154-
.thru(createPayload)
155-
.thru(
156-
composeMutations(
157-
i18nDirectiveMutation,
158-
jsxFragmentMutation,
159-
jsxAttributeFlagMutation,
160-
161-
// log here to see transformedfiles
162-
// (input) => {
163-
// console.log(`transform ${id}`);
164-
// return input;
165-
// },
166-
167-
jsxProviderMutation,
168-
jsxHtmlLangMutation,
169-
jsxRootFlagMutation,
170-
jsxScopeFlagMutation,
171-
jsxAttributeFlagMutation,
172-
jsxAttributeScopesExportMutation,
173-
jsxScopesExportMutation,
174-
lingoJsxAttributeScopeInjectMutation,
175-
lingoJsxScopeInjectMutation,
176-
rscDictionaryLoaderMutation,
177-
reactRouterDictionaryLoaderMutation,
178-
jsxRemoveAttributesMutation,
179-
clientDictionaryLoaderMutation,
180-
),
181-
)
182-
.thru(createOutput)
183-
.value();
122+
resourcePath: id,
123+
sourceRoot: params.sourceRoot,
124+
});
184125

185126
return result;
186127
} catch (error) {
@@ -196,19 +137,96 @@ const unplugin = createUnplugin<Partial<typeof defaultParams> | undefined>(
196137

197138
export default {
198139
next:
199-
(compilerParams?: Partial<typeof defaultParams>) =>
200-
(nextConfig: any): NextConfig => ({
201-
...nextConfig,
202-
// what if we already have a webpack config?
203-
webpack: (config, { isServer }) => {
204-
config.plugins.unshift(
205-
unplugin.webpack(
206-
_.merge({}, defaultParams, { rsc: true }, compilerParams),
207-
),
140+
(
141+
compilerParams?: Partial<typeof defaultParams> & {
142+
turbopack?: {
143+
enabled?: boolean | "auto";
144+
useLegacyTurbo?: boolean;
145+
};
146+
},
147+
) =>
148+
(nextConfig: any = {}): NextConfig => {
149+
const mergedParams = _.merge(
150+
{},
151+
defaultParams,
152+
{
153+
rsc: true,
154+
turbopack: {
155+
enabled: "auto",
156+
useLegacyTurbo: false,
157+
},
158+
},
159+
compilerParams,
160+
);
161+
162+
let turbopackEnabled: boolean;
163+
if (mergedParams.turbopack?.enabled === "auto") {
164+
turbopackEnabled =
165+
process.env.TURBOPACK === "1" || process.env.TURBOPACK === "true";
166+
} else {
167+
turbopackEnabled = mergedParams.turbopack?.enabled === true;
168+
}
169+
170+
const supportLegacyTurbo: boolean =
171+
mergedParams.turbopack?.useLegacyTurbo === true;
172+
173+
const hasWebpackConfig = typeof nextConfig.webpack === "function";
174+
const hasTurbopackConfig = typeof nextConfig.turbopack === "function";
175+
if (hasWebpackConfig && turbopackEnabled) {
176+
console.warn(
177+
"⚠️ Turbopack is enabled in the Lingo.dev compiler, but you have webpack config. Lingo.dev will still apply turbopack configuration.",
208178
);
179+
}
180+
if (hasTurbopackConfig && !turbopackEnabled) {
181+
console.warn(
182+
"⚠️ Turbopack is disabled in the Lingo.dev compiler, but you have turbopack config. Lingo.dev will not apply turbopack configuration.",
183+
);
184+
}
185+
186+
// Webpack
187+
const originalWebpack = nextConfig.webpack;
188+
nextConfig.webpack = (config: any, options: any) => {
189+
if (!turbopackEnabled) {
190+
console.log("Applying Lingo.dev webpack configuration...");
191+
config.plugins.unshift(unplugin.webpack(mergedParams));
192+
}
193+
194+
if (typeof originalWebpack === "function") {
195+
return originalWebpack(config, options);
196+
}
209197
return config;
210-
},
211-
}),
198+
};
199+
200+
// Turbopack
201+
if (turbopackEnabled) {
202+
console.log("Applying Lingo.dev Turbopack configuration...");
203+
204+
// Check if the legacy turbo flag is set
205+
let turbopackConfigPath = (nextConfig.turbopack ??= {});
206+
if (supportLegacyTurbo) {
207+
turbopackConfigPath = (nextConfig.experimental ??= {}).turbo ??= {};
208+
}
209+
210+
turbopackConfigPath.rules ??= {};
211+
const rules = turbopackConfigPath.rules;
212+
213+
// Regex for all relevant files for Lingo.dev
214+
const lingoGlob = `**/*.{ts,tsx,js,jsx}`;
215+
216+
const lingoLoaderPath = require.resolve("./lingo-turbopack-loader");
217+
218+
rules[lingoGlob] = {
219+
loaders: [
220+
{
221+
loader: lingoLoaderPath,
222+
options: mergedParams,
223+
},
224+
],
225+
};
226+
}
227+
228+
return nextConfig;
229+
},
212230
vite: (compilerParams?: Partial<typeof defaultParams>) => (config: any) => {
213231
config.plugins.unshift(
214232
unplugin.vite(_.merge({}, defaultParams, { rsc: false }, compilerParams)),

0 commit comments

Comments
 (0)