UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

177 lines (175 loc) • 6.81 kB
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