{"version":3,"file":"restructure.mjs","names":[],"sources":["../../../../../src/init/frameworkSetup/nextAppRouter/restructure.ts"],"sourcesContent":["import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport fg from 'fast-glob';\nimport * as recast from 'recast';\nimport { babelTsParser } from '../../../utils/babelParser';\n\nconst { namedTypes: n } = recast.types;\n\n/** Source file extensions whose relative imports must be rewritten after a move. */\nconst SCRIPT_GLOB = '**/*.{ts,tsx,js,jsx,mjs,cjs}';\n\n/** Strips a known script extension from a file name, e.g. `page.tsx` -> `page`. */\nconst stripScriptExtension = (fileName: string): string =>\n  fileName.replace(/\\.(tsx|ts|jsx|js|mjs|cjs)$/, '');\n\n/**\n * Detects whether a top-level App Router entry is already a locale segment, in\n * any of the Next.js dynamic-segment forms:\n * - `[locale]` — required segment (prefix every locale),\n * - `[...locale]` / `[[...locale]]` — catch-all / optional catch-all segments.\n *\n * Used to skip the restructure when the project is already locale-aware, so an\n * existing locale segment is never nested under a freshly created `[locale]`.\n */\nexport const isLocaleSegment = (entryName: string): boolean =>\n  /^\\[\\[?\\.{0,3}locale\\]\\]?$/.test(entryName);\n\n/**\n * Top-level App Router entries that must stay at the app root and never be\n * moved under `[locale]`:\n * - `api/` route handlers (not locale-prefixed),\n * - global stylesheets,\n * - metadata/asset file conventions (favicon, icon, sitemap, robots, manifest…),\n * - `global-error` and `not-found` boundaries (kept as root fallbacks).\n *\n * Everything else (`page`, `loading`, `error`, `template`, `default`, the root\n * `layout`, and nested route folders) is moved so it becomes locale-aware.\n */\nexport const shouldKeepAppEntryAtRoot = (entryName: string): boolean => {\n  if (entryName === 'api') return true;\n  if (entryName.toLowerCase().endsWith('.css')) return true;\n  if (entryName === 'favicon.ico') return true;\n\n  const base = stripScriptExtension(entryName);\n  const keepExactBases = new Set([\n    'not-found',\n    'global-error',\n    'sitemap',\n    'robots',\n    'manifest',\n  ]);\n  if (keepExactBases.has(base)) return true;\n\n  // Image metadata conventions: favicon, icon, apple-icon, opengraph-image,\n  // twitter-image — optionally suffixed (e.g. `icon1`, `opengraph-image-alt`).\n  const imageConventionPrefixes = [\n    'favicon',\n    'icon',\n    'apple-icon',\n    'opengraph-image',\n    'twitter-image',\n  ];\n  if (imageConventionPrefixes.some((prefix) => base.startsWith(prefix))) {\n    return true;\n  }\n\n  return false;\n};\n\ntype RewriteContext = {\n  appDirAbs: string;\n  localeDirAbs: string;\n  movedTopLevelNames: string[];\n};\n\n/**\n * Maps an import target's pre-move absolute path to its post-move absolute path.\n * Targets that live inside a moved top-level entry are relocated under\n * `[locale]`; targets that stayed at the app root or live outside the app\n * directory are returned unchanged.\n */\nconst mapTargetPath = (\n  oldTargetAbs: string,\n  { appDirAbs, localeDirAbs, movedTopLevelNames }: RewriteContext\n): string => {\n  const relFromApp = relative(appDirAbs, oldTargetAbs);\n\n  // Outside the app directory (e.g. `../components/...`) — never moved.\n  if (relFromApp.startsWith('..') || isAbsolute(relFromApp)) {\n    return oldTargetAbs;\n  }\n\n  const firstSegment = relFromApp.split(sep)[0] ?? '';\n  const isMoved = movedTopLevelNames.some(\n    (name) =>\n      name === firstSegment || stripScriptExtension(name) === firstSegment\n  );\n\n  if (!isMoved) return oldTargetAbs;\n\n  return join(localeDirAbs, relFromApp);\n};\n\n/**\n * Rewrites relative import/export/`import()`/`require()` specifiers in a file\n * that has moved from `oldAbs` to `newAbs`, so they keep resolving to the same\n * modules after the move. Non-relative specifiers (bare packages, `@/` aliases)\n * are left untouched. Returns the original code unchanged when nothing matched.\n */\nexport const rewriteRelativeImports = (\n  code: string,\n  oldAbs: string,\n  newAbs: string,\n  context: RewriteContext\n): string => {\n  // babel-ts handles TypeScript *and* JSX (App Router files are `.tsx`).\n  const ast = recast.parse(code, { parser: babelTsParser });\n\n  let changed = false;\n\n  const rewriteSource = (sourceNode: any): void => {\n    if (!sourceNode || !n.StringLiteral.check(sourceNode)) return;\n    const specifier = sourceNode.value;\n    if (typeof specifier !== 'string' || !specifier.startsWith('.')) return;\n\n    const oldTargetAbs = resolve(dirname(oldAbs), specifier);\n    const newTargetAbs = mapTargetPath(oldTargetAbs, context);\n\n    let newSpecifier = relative(dirname(newAbs), newTargetAbs)\n      .split(sep)\n      .join('/');\n    if (!newSpecifier.startsWith('.')) {\n      newSpecifier = `./${newSpecifier}`;\n    }\n\n    if (newSpecifier !== specifier) {\n      sourceNode.value = newSpecifier;\n      changed = true;\n    }\n  };\n\n  recast.visit(ast, {\n    visitImportDeclaration(path) {\n      rewriteSource(path.node.source);\n      return false;\n    },\n    visitExportAllDeclaration(path) {\n      rewriteSource(path.node.source);\n      return false;\n    },\n    visitExportNamedDeclaration(path) {\n      if (path.node.source) rewriteSource(path.node.source);\n      return false;\n    },\n    visitCallExpression(path) {\n      const { callee, arguments: args } = path.node;\n      const isDynamicImport = callee.type === 'Import';\n      const isRequire = n.Identifier.check(callee) && callee.name === 'require';\n      if ((isDynamicImport || isRequire) && args.length > 0) {\n        rewriteSource(args[0]);\n      }\n      this.traverse(path);\n    },\n  });\n\n  if (!changed) return code;\n  return recast.print(ast).code;\n};\n\n/** Outcome of an attempted `[locale]` restructure. */\nexport type RestructureResult =\n  | { status: 'already-structured'; localeSegment: string }\n  | { status: 'nothing-to-move' }\n  | { status: 'moved'; movedEntries: string[] };\n\n/**\n * Moves the routable App Router entries of `appDir` under a new `[locale]`\n * segment and rewrites relative imports in the moved files. Idempotent: it is a\n * no-op when the app is already locale-aware in any prefix mode (see\n * {@link isLocaleSegment}), which is reported via `localeSegment`. Root-only\n * files (see {@link shouldKeepAppEntryAtRoot}) are left in place.\n */\nexport const restructureAppIntoLocale = async (\n  rootDir: string,\n  appDir: string\n): Promise<RestructureResult> => {\n  const appDirAbs = join(rootDir, appDir);\n  const localeDirAbs = join(appDirAbs, '[locale]');\n\n  const entries = await readdir(appDirAbs, { withFileTypes: true });\n\n  // Skip when the app is already locale-aware in any prefix mode — a fresh\n  // `[locale]`, or an existing `[...locale]` / `[[...locale]]` catch-all segment.\n  const existingLocaleSegment = entries.find((entry) =>\n    isLocaleSegment(entry.name)\n  );\n  if (existingLocaleSegment) {\n    return {\n      status: 'already-structured',\n      localeSegment: existingLocaleSegment.name,\n    };\n  }\n\n  const movedTopLevelNames = entries\n    .map((entry) => entry.name)\n    .filter((name) => !shouldKeepAppEntryAtRoot(name));\n\n  if (movedTopLevelNames.length === 0) {\n    return { status: 'nothing-to-move' };\n  }\n\n  await mkdir(localeDirAbs, { recursive: true });\n\n  for (const name of movedTopLevelNames) {\n    await rename(join(appDirAbs, name), join(localeDirAbs, name));\n  }\n\n  const movedFiles = await fg(SCRIPT_GLOB, {\n    cwd: localeDirAbs,\n    absolute: true,\n    onlyFiles: true,\n  });\n\n  const rewriteContext: RewriteContext = {\n    appDirAbs,\n    localeDirAbs,\n    movedTopLevelNames,\n  };\n\n  for (const newAbs of movedFiles) {\n    const relFromLocale = relative(localeDirAbs, newAbs);\n    const oldAbs = join(appDirAbs, relFromLocale);\n    const code = await readFile(newAbs, 'utf8');\n    const rewritten = rewriteRelativeImports(\n      code,\n      oldAbs,\n      newAbs,\n      rewriteContext\n    );\n    if (rewritten !== code) {\n      await writeFile(newAbs, rewritten, 'utf8');\n    }\n  }\n\n  return { status: 'moved', movedEntries: movedTopLevelNames };\n};\n"],"mappings":"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"}