UNPKG

everything-dev

Version:

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

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