everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
177 lines (175 loc) • 6.81 kB
JavaScript
const require_fs = require('../fs.cjs');
const require_i18n_seam = require('../checks/i18n-seam.cjs');
let effect = require("effect");
//#region src/services/scaffold/upgrade-audit/steps/migrate-i18n-seam.ts
/**
* Wires the shared i18n runtime into a v2 child app's core UI.
*
* The host's auth plugin consumes shared `@lingui/*` with `import: false` —
* only the app's own UI (the core-ui provider in the child's federation
* graph) can provide them. A core UI that never imports
* `everything-dev/ui/i18n` at runtime ships no `@lingui/*` share-scope
* entries, and boot fails at SSR composition with RUNTIME-012 ("The getter
* for the shared module is not a function … shareKey @lingui/core").
*
* The seam is the upstream canonical one: app-owned `ui/src/i18n/catalogs.ts`
* (locales + message catalogs) and `ui/src/i18n/runtime.tsx`
* (`createLocaleRuntime` → `AppI18nProvider`), mounted in the root route.
* Locale config derives from the app's `appLocale` bootstrap export (the
* generated stubs read it) so the two stay consistent.
*
* Maintenance step for post-upgrade trees:
* `pnpm i18n-seam [--dry-run]`
*/
const DEFAULT_LOCALE_CONFIG = {
locales: ["en"],
defaultLocale: "en",
cookieName: "app_locale"
};
const renderCatalogs = (config) => [
"import type { Messages } from \"@lingui/core\";",
"",
"// No copy in this app uses <Trans> message ids yet. The catalog exists so the",
"// shared i18n runtime ships in this bundle's Module Federation share scope —",
"// the host's auth plugin resolves @lingui/* through it (RUNTIME-012 at SSR",
"// composition otherwise). Add message ids here as surfaces adopt <Trans>.",
`export const APP_LOCALES = [${config.locales.map((l) => `"${l}"`).join(", ")}] as const;`,
"export type AppLocale = (typeof APP_LOCALES)[number];",
`export const DEFAULT_APP_LOCALE: AppLocale = "${config.defaultLocale}";`,
`export const APP_LOCALE_COOKIE = "${config.cookieName}";`,
"",
"const englishMessages = {} satisfies Record<string, string>;",
"",
"const translatedMessages: Record<AppLocale, Partial<Record<string, string>>> = {",
" en: {},",
"};",
"",
"export function getAppMessages(locale: AppLocale): Messages {",
" return { ...englishMessages, ...translatedMessages[locale] };",
"}",
""
].join("\n");
const renderRuntime = () => [
"import { createLocaleRuntime } from \"everything-dev/ui/i18n\";",
"import type { ReactNode } from \"react\";",
"import { APP_LOCALES, APP_LOCALE_COOKIE, DEFAULT_APP_LOCALE, getAppMessages } from \"./catalogs\";",
"",
"const appRuntime = createLocaleRuntime({",
" locales: APP_LOCALES,",
" defaultLocale: DEFAULT_APP_LOCALE,",
" cookieName: APP_LOCALE_COOKIE,",
"});",
"",
"export function AppI18nProvider({",
" children,",
" preferredLocale,",
"}: {",
" children: ReactNode;",
" preferredLocale?: string | null;",
"}) {",
" return (",
" <appRuntime.LocaleProvider messages={getAppMessages} preferredLocale={preferredLocale}>",
" {children}",
" </appRuntime.LocaleProvider>",
" );",
"}",
"",
"export const useAppLocale = appRuntime.useLocale;",
""
].join("\n");
/**
* Wraps the single child of the root's `<div id="root">` shell with the
* provider. Child roots carry app-owned chrome, so anything but the known
* one-child-shell shape is left for manual wiring rather than guessed at.
*/
const mountProvider = (source) => {
const lines = source.split("\n");
const openIdx = lines.findIndex((l) => l.trim() === "<div id=\"root\">");
if (openIdx === -1) return {
source,
mounted: false
};
const openIndent = lines[openIdx].search(/\S/);
let closeIdx = -1;
for (let i = openIdx + 1; i < lines.length; i++) if (lines[i].trim() === "</div>") {
closeIdx = i;
break;
}
if (closeIdx === -1 || closeIdx === openIdx + 1) return {
source,
mounted: false
};
const childLines = lines.slice(openIdx + 1, closeIdx);
const childIndent = childLines[0]?.search(/\S/) ?? openIndent + 2;
if (childIndent <= openIndent) return {
source,
mounted: false
};
const indented = childLines.map((l) => l.trim() ? ` ${l}` : l);
const providerOpen = `${" ".repeat(childIndent)}<AppI18nProvider>`;
const providerClose = `${" ".repeat(childIndent)}</AppI18nProvider>`;
return {
source: [
...lines.slice(0, openIdx + 1),
providerOpen,
...indented,
providerClose,
...lines.slice(closeIdx)
].join("\n"),
mounted: true
};
};
const migrateI18nSeam = (projectDir, dryRun) => effect.Effect.gen(function* () {
const writes = [];
const notes = [];
const appExists = yield* require_fs.fileExists(projectDir, require_i18n_seam.APP_PATH);
const config = appExists ? require_i18n_seam.parseAppLocale(yield* require_fs.readText(projectDir, "ui/src/app.ts")) ?? DEFAULT_LOCALE_CONFIG : DEFAULT_LOCALE_CONFIG;
if (!appExists) notes.push(`ui/src/app.ts absent — defaulting the i18n config to ${config.defaultLocale}/${config.cookieName}`);
if (!(yield* require_fs.fileExists(projectDir, "ui/src/i18n/runtime.tsx"))) for (const [path, render] of [[require_i18n_seam.CATALOGS_PATH, () => renderCatalogs(config)], [require_i18n_seam.RUNTIME_PATH, renderRuntime]]) {
if (yield* require_fs.fileExists(projectDir, path)) {
notes.push(`${path} exists — left untouched`);
continue;
}
if (!dryRun) yield* require_fs.writeText(projectDir, path, render());
writes.push(path);
}
else notes.push("ui/src/i18n/runtime.tsx exists — left untouched");
if (!(yield* require_fs.fileExists(projectDir, "ui/src/routes/__root.tsx"))) {
notes.push("ui/src/routes/__root.tsx absent — provider not mounted, inspect manually");
return {
writes,
deletes: [],
notes
};
}
const rootSource = yield* require_fs.readText(projectDir, require_i18n_seam.ROOT_PATH);
if (rootSource.includes("@/i18n/runtime")) {
notes.push("root route already mounts AppI18nProvider");
return {
writes,
deletes: [],
notes
};
}
const { source: mounted, mounted: ok } = mountProvider(rootSource);
if (!ok) {
notes.push("root route shell shape not recognized (expected a single child under <div id=\"root\">) — mount AppI18nProvider manually");
return {
writes,
deletes: [],
notes
};
}
const withImport = `${`import { AppI18nProvider } from "${require_i18n_seam.RUNTIME_IMPORT}";\n`}${mounted}`;
if (!dryRun) yield* require_fs.writeText(projectDir, require_i18n_seam.ROOT_PATH, withImport);
writes.push(require_i18n_seam.ROOT_PATH);
notes.push("AppI18nProvider mounted in the root route — the UI bundle now provides @lingui/* through the share scope", "optional follow-up: thread the session user's locale as preferredLocale (the canonical upstream root does)");
return {
writes,
deletes: [],
notes
};
});
//#endregion
exports.migrateI18nSeam = migrateI18nSeam;
//# sourceMappingURL=migrate-i18n-seam.cjs.map