{"version":3,"file":"extractorLoader.cjs","names":["getProjectRequire","join","withFileLock","getConfiguration","normalizePath","getAppLogger","dirname","colorize","ANSIColors","colorizePath","relative"],"sources":["../../src/extractorLoader.ts"],"sourcesContent":["import { dirname, join, relative } from 'node:path';\nimport type {\n  detectPackageName as DetectPackageName,\n  extractContent as ExtractContent,\n  ExtractPluginOptions,\n  getExtractPluginOptions as GetExtractPluginOptions,\n  PackageName,\n  writeContentHelper as WriteContentHelper,\n} from '@intlayer/babel';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, colorizePath, getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getProjectRequire, normalizePath } from '@intlayer/config/utils';\nimport { withFileLock } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n\n/**\n * Options the loader accepts. Turbopack serialises loader options to JSON, so\n * every field has to stay a plain JSON value — the Intlayer configuration\n * itself (which carries functions, e.g. `compiler.output`) is therefore loaded\n * by the loader rather than handed to it.\n */\nexport type IntlayerExtractorLoaderOptions = {\n  /**\n   * Whether the host bundler is serving (`dev`) or building (`build`).\n   *\n   * Passed explicitly because a Turbopack loader runs in a worker process whose\n   * environment does not necessarily carry `INTLAYER_IS_DEV_COMMAND`.\n   */\n  mode?: 'dev' | 'build';\n};\n\n/**\n * Structural view of the loader context, limited to what this loader uses.\n * Typed locally rather than imported from `webpack` because the same file is\n * also run by Turbopack's webpack-loader compatibility layer, and `webpack` is\n * only an optional peer dependency.\n */\ntype IntlayerLoaderContext = {\n  /** Absolute path of the file being compiled. */\n  resourcePath: string;\n  /** Switches the loader to asynchronous mode. */\n  async: () => (error: Error | null, content?: string) => void;\n  /** webpack 5 / Turbopack accessor for the loader options. */\n  getOptions?: () => IntlayerExtractorLoaderOptions;\n  /** Legacy options holder, used when `getOptions` is unavailable. */\n  query?: IntlayerExtractorLoaderOptions | string;\n};\n\n/**\n * Subset of `@intlayer/babel` the loader needs. The package is an optional\n * dependency, so it is required lazily and never imported at module scope.\n */\ntype IntlayerBabelModule = {\n  detectPackageName: typeof DetectPackageName;\n  extractContent: typeof ExtractContent;\n  getExtractPluginOptions: typeof GetExtractPluginOptions;\n  writeContentHelper: typeof WriteContentHelper;\n};\n\nconst REQUIRED_BABEL_EXPORTS = [\n  'detectPackageName',\n  'extractContent',\n  'getExtractPluginOptions',\n  'writeContentHelper',\n] as const satisfies readonly (keyof IntlayerBabelModule)[];\n\n/**\n * Serialises the dictionary writes of every process taking part in the build.\n * `writeContentHelper` rebuilds the merged `.intlayer` artifacts, which is a\n * read-modify-write over every dictionary on disk — two extractions running it\n * at the same time would each merge a half-written state.\n */\nconst WRITE_LOCK_FILE_NAME = 'intlayer-compiler-write.lock';\n\n/** Set once per process: the loader runs in a long-lived bundler worker. */\nlet cachedConfiguration: IntlayerConfig | undefined;\nlet cachedExtractOptions: ExtractPluginOptions | undefined;\nlet cachedFilesListSet: Set<string> | undefined;\nlet cachedBabelModule: IntlayerBabelModule | null | undefined;\n\n/** Intlayer package detected per component directory. */\nconst packageNameByDirectory = new Map<string, PackageName>();\n\n/**\n * Content already written for a given dictionary key, as a stable hash.\n *\n * Next.js compiles the same file once per compilation (client, server, edge),\n * and Turbopack re-runs the loader whenever the file changes. Re-writing a\n * dictionary whose content did not move costs a full `.intlayer` rebuild for\n * nothing.\n */\nconst writtenContentHashByDictionaryKey = new Map<string, string>();\n\n/** Serialises the writes issued by this process, before the file lock. */\nlet pendingWrite: Promise<void> = Promise.resolve();\n\n/**\n * Lazily loads `@intlayer/babel`, or `null` when it is not installed or too old\n * to expose the extraction API.\n */\nconst loadIntlayerBabel = (\n  configuration: IntlayerConfig\n): IntlayerBabelModule | null => {\n  if (cachedBabelModule !== undefined) return cachedBabelModule;\n\n  try {\n    const requireFunction = configuration.build?.require ?? getProjectRequire();\n\n    const intlayerBabel = requireFunction(\n      '@intlayer/babel'\n    ) as Partial<IntlayerBabelModule>;\n\n    const hasEveryRequiredExport = REQUIRED_BABEL_EXPORTS.every(\n      (exportName) => typeof intlayerBabel?.[exportName] === 'function'\n    );\n\n    cachedBabelModule = hasEveryRequiredExport\n      ? (intlayerBabel as IntlayerBabelModule)\n      : null;\n  } catch {\n    cachedBabelModule = null;\n  }\n\n  return cachedBabelModule;\n};\n\nconst getOptions = (\n  context: IntlayerLoaderContext\n): IntlayerExtractorLoaderOptions => {\n  if (typeof context.getOptions === 'function') return context.getOptions();\n  if (typeof context.query === 'object' && context.query) return context.query;\n\n  return {};\n};\n\nconst hashContent = (content: Record<string, string>): string =>\n  JSON.stringify(\n    Object.keys(content)\n      .sort()\n      .map((key) => [key, content[key]])\n  );\n\n/**\n * Writes one extracted dictionary, skipping the write when its content is\n * already on disk and holding the cross-process lock while `.intlayer` is\n * rebuilt.\n */\nconst writeDictionary = async (\n  intlayerBabel: IntlayerBabelModule,\n  configuration: IntlayerConfig,\n  dictionaryKey: string,\n  content: Record<string, string>,\n  filePath: string\n): Promise<void> => {\n  const contentHash = hashContent(content);\n\n  if (writtenContentHashByDictionaryKey.get(dictionaryKey) === contentHash) {\n    return;\n  }\n\n  const lockFilePath = join(\n    configuration.system.baseDir,\n    '.intlayer',\n    'cache',\n    WRITE_LOCK_FILE_NAME\n  );\n\n  await withFileLock(lockFilePath, () =>\n    intlayerBabel.writeContentHelper(\n      content,\n      dictionaryKey,\n      filePath,\n      configuration\n    )\n  );\n\n  writtenContentHashByDictionaryKey.set(dictionaryKey, contentHash);\n};\n\n/**\n * Extracts the translatable content of one file and returns the rewritten\n * source, or the original source when there is nothing to extract.\n */\nconst extractFile = async (\n  filePath: string,\n  source: string,\n  mode: 'dev' | 'build'\n): Promise<string> => {\n  cachedConfiguration ??= getConfiguration();\n  const configuration = cachedConfiguration;\n\n  const intlayerBabel = loadIntlayerBabel(configuration);\n\n  // Without `@intlayer/babel` there is no extraction to run; `withIntlayer`\n  // only registers this loader when the package resolves, so reaching this\n  // branch means it was removed after the config was read.\n  if (!intlayerBabel) return source;\n\n  cachedExtractOptions ??= intlayerBabel.getExtractPluginOptions(\n    configuration,\n    mode\n  );\n\n  if (!cachedExtractOptions.enabled) return source;\n\n  cachedFilesListSet ??= new Set(\n    (cachedExtractOptions.filesList ?? []).map(normalizePath)\n  );\n\n  if (!cachedFilesListSet.has(normalizePath(filePath))) return source;\n\n  const appLogger = getAppLogger(configuration);\n  const componentDirectory = dirname(filePath);\n\n  let packageName = packageNameByDirectory.get(componentDirectory);\n\n  if (!packageName) {\n    packageName = intlayerBabel.detectPackageName(componentDirectory);\n    packageNameByDirectory.set(componentDirectory, packageName);\n  }\n\n  const result = await intlayerBabel.extractContent(filePath, packageName, {\n    configuration,\n    code: source,\n    // `extractContent` rewrites the component on disk unless told otherwise,\n    // so the config flag has to be forwarded — the transform is meant to stay\n    // in the bundle output until the user opts into `saveComponents`.\n    declarationOnly: !configuration.compiler.saveComponents,\n    // Supplying `onExtract` turns off the internal dictionary write, so the\n    // writes stay serialised behind the lock below.\n    onExtract: ({ key, content }) => {\n      pendingWrite = pendingWrite\n        .then(() =>\n          writeDictionary(intlayerBabel, configuration, key, content, filePath)\n        )\n        .catch((error) => {\n          appLogger(\n            [\n              `${colorize('Compiler:', ANSIColors.GREY_DARK)} Failed to write the dictionary extracted from ${colorizePath(\n                relative(configuration.system.baseDir, filePath)\n              )}:`,\n              error,\n            ],\n            { level: 'error' }\n          );\n        });\n\n      return pendingWrite;\n    },\n  });\n\n  if (!result?.transformedCode) return source;\n\n  appLogger(\n    `${colorize('Compiler:', ANSIColors.GREY_DARK)} Extracted content from ${colorizePath(\n      relative(configuration.system.baseDir, filePath)\n    )}`,\n    { level: 'debug' }\n  );\n\n  return result.transformedCode;\n};\n\n/**\n * Webpack-compatible loader running the Intlayer compiler extraction pass.\n *\n * It replaces the `babel.config.js` setup on Next.js: Turbopack never reads a\n * Babel config, and on webpack a Babel config would opt the whole project out\n * of SWC. Registered automatically by `withIntlayer` on both bundlers.\n *\n * @example next.config.ts — registered by hand\n * ```ts\n * export default {\n *   turbopack: {\n *     rules: {\n *       // No `as`: it renames the module, so naming the extension the rule\n *       // already matches would ask for `Component.tsx.tsx`.\n *       '*.tsx': {\n *         loaders: [\n *           {\n *             loader: require.resolve('next-intlayer/extractor-loader'),\n *             options: { mode: 'dev' },\n *           },\n *         ],\n *       },\n *     },\n *   },\n * };\n * ```\n */\nconst intlayerExtractorLoader = function (\n  this: IntlayerLoaderContext,\n  source: string\n): void {\n  const callback = this.async();\n  const { mode } = getOptions(this);\n  const filePath = this.resourcePath;\n\n  extractFile(filePath, source, mode ?? 'build')\n    .then((code) => callback(null, code))\n    .catch((error) => {\n      // An extraction failure must not take the build down: the file simply\n      // keeps its own hardcoded strings.\n      const appLogger = getAppLogger(cachedConfiguration);\n\n      appLogger(\n        [\n          `${colorize('Compiler:', ANSIColors.GREY_DARK)} Failed to extract content from ${colorizePath(filePath)}:`,\n          error,\n        ],\n        { level: 'error' }\n      );\n\n      callback(null, source);\n    });\n};\n\nexport default intlayerExtractorLoader;\n"],"mappings":"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"}