{"version":3,"file":"createFileAdapter.mjs","names":[],"sources":["../../../src/syncPluginKit/createFileAdapter.ts"],"sourcesContent":["import { mkdir, readFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, resolve } from 'node:path';\nimport { parseFilePathPattern } from '@intlayer/config/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { FilePathPattern } from '@intlayer/types/filePathPattern';\nimport fg from 'fast-glob';\nimport { writeFileIfChanged } from '../writeFileIfChanged';\nimport { extractKeyAndLocaleFromPath } from './extractKeyAndLocaleFromPath';\nimport { buildFilePathPatternContext } from './filePathPatternHelpers';\nimport type {\n  ContentAdapter,\n  ContentEntry,\n  FormatCodec,\n  SyncContent,\n  SyncPluginContext,\n} from './types';\n\n/**\n * Discovery strategy of the file adapter:\n * - `strict`: keep only files whose path rebuilds identically from the source\n *   pattern, and fabricate entries for missing locales/keys so every declared\n *   locale has a write-back target (sync plugins).\n * - `inclusive`: keep every glob match without fabricating missing entries\n *   (read-only load plugins, whose patterns may contain free `**` globs).\n */\nexport type FileAdapterDiscovery = 'strict' | 'inclusive';\n\nexport type CreateFileAdapterOptions = {\n  /**\n   * Location of the files, as a static string, a templated string\n   * (e.g. `./messages/{{locale}}/{{key}}.json`) or a function building the\n   * path from the key and locale.\n   */\n  source: FilePathPattern;\n\n  /**\n   * Format of the files: string payload ↔ structured content.\n   */\n  codec: FormatCodec;\n\n  /**\n   * Optional override of how one file is read and parsed. Defaults to a plain\n   * utf-8 read followed by `codec.parse`, returning `undefined` for missing or\n   * unreadable files. Adapters needing richer loading (e.g. JSON5 or\n   * transpiled TS sources) provide their own implementation here.\n   */\n  readEntry?: (\n    absoluteFilePath: string,\n    context: SyncPluginContext\n  ) => Promise<SyncContent | undefined>;\n\n  /**\n   * Discovery strategy. Defaults to `'strict'`.\n   */\n  discovery?: FileAdapterDiscovery;\n};\n\ntype FilePath = string;\ntype MessagesRecord = Record<Locale, Record<string, FilePath>>;\n\n/**\n * Create a filesystem {@link ContentAdapter} for `createSyncPlugin`.\n *\n * Handles glob discovery of the source pattern, key/locale extraction from\n * paths, base-directory resolution, and change-detected atomic writes.\n */\nexport const createFileAdapter = (\n  options: CreateFileAdapterOptions\n): ContentAdapter => {\n  const { source, codec, discovery = 'strict' } = options;\n  const readEntry = options.readEntry ?? createFileReader(codec.parse);\n\n  let patternMarkerPromise: Promise<string> | undefined;\n\n  /**\n   * Source pattern with `{{key}}` / `{{locale}}` markers kept verbatim,\n   * memoized because pattern functions can be asynchronous.\n   */\n  const getPatternMarker = () => {\n    patternMarkerPromise ??= parseFilePathPattern(\n      source,\n      buildFilePathPatternContext('{{key}}', '{{locale}}')\n    );\n\n    return patternMarkerPromise;\n  };\n\n  const listMessages = async (\n    context: SyncPluginContext\n  ): Promise<MessagesRecord> => {\n    const { system, internationalization } = context.configuration;\n    const { baseDir } = system;\n    const { locales } = internationalization;\n\n    const result: MessagesRecord = {} as MessagesRecord;\n\n    for (const locale of locales) {\n      const globPatternLocale = await parseFilePathPattern(\n        source,\n        buildFilePathPatternContext('**', locale)\n      );\n\n      const maskPatternLocale = await parseFilePathPattern(\n        source,\n        buildFilePathPatternContext('{{__KEY__}}', locale)\n      );\n\n      if (!globPatternLocale || !maskPatternLocale) {\n        continue;\n      }\n\n      const normalizedGlobPattern = globPatternLocale.startsWith('./')\n        ? globPatternLocale.slice(2)\n        : globPatternLocale;\n\n      const files = await fg(normalizedGlobPattern, {\n        cwd: baseDir,\n      });\n\n      const hasLocaleInMask = maskPatternLocale.includes('{{__LOCALE__}}');\n      const hasKeyInMask = maskPatternLocale.includes('{{__KEY__}}');\n\n      for (const file of files) {\n        let key: string;\n        let extractedLocale: Locale;\n\n        if (hasLocaleInMask || hasKeyInMask) {\n          const extraction = extractKeyAndLocaleFromPath(\n            file,\n            maskPatternLocale,\n            locales,\n            locale\n          );\n\n          if (!extraction) {\n            continue;\n          }\n\n          key = extraction.key;\n          extractedLocale = extraction.locale;\n        } else if (discovery === 'inclusive') {\n          // Mask has no placeholders — the file was found via a concrete\n          // locale glob. Attribute it directly to the current loop locale.\n          key = 'index';\n          extractedLocale = locale;\n        } else {\n          // Strict mode relies on the fabrication step below to attribute\n          // placeholder-free sources to their canonical paths.\n          continue;\n        }\n\n        const absoluteFoundPath = isAbsolute(file)\n          ? file\n          : resolve(baseDir, file);\n\n        if (discovery === 'strict') {\n          // Rebuild what the path SHOULD be for this key/locale. A mismatch\n          // means the file belongs to another plugin/structure.\n          const expectedPath = await parseFilePathPattern(\n            source,\n            buildFilePathPatternContext(key, extractedLocale)\n          );\n\n          const absoluteExpectedPath = isAbsolute(expectedPath)\n            ? expectedPath\n            : resolve(baseDir, expectedPath);\n\n          if (absoluteFoundPath !== absoluteExpectedPath) {\n            continue;\n          }\n        }\n\n        result[extractedLocale] ??= {};\n        result[extractedLocale][key] = absoluteFoundPath;\n      }\n    }\n\n    if (discovery === 'inclusive') {\n      // Read-only mode only uses actual discovered files; do not fabricate\n      // missing locales or keys, since no outputs are written.\n      return result;\n    }\n\n    // Ensure all declared locales are present even if the file doesn't exist yet\n    const maskWithKey = await parseFilePathPattern(\n      source,\n      buildFilePathPatternContext('{{__KEY__}}', locales[0])\n    );\n\n    const hasKeyInMask = maskWithKey.includes('{{__KEY__}}');\n    const discoveredKeys = new Set<string>();\n\n    for (const locale of Object.keys(result) as Locale[]) {\n      for (const key of Object.keys(result[locale] ?? {})) {\n        discoveredKeys.add(key);\n      }\n    }\n\n    if (!hasKeyInMask) {\n      discoveredKeys.add('index');\n    }\n\n    for (const locale of locales) {\n      result[locale] ??= {};\n\n      for (const key of discoveredKeys) {\n        if (!result[locale][key]) {\n          const builtPath = await parseFilePathPattern(\n            source,\n            buildFilePathPatternContext(key, locale)\n          );\n\n          result[locale][key] = isAbsolute(builtPath)\n            ? builtPath\n            : resolve(baseDir, builtPath);\n        }\n      }\n    }\n\n    return result;\n  };\n\n  return {\n    list: async (context) => {\n      const messages = await listMessages(context);\n\n      const entries: ContentEntry[] = (\n        Object.entries(messages) as [Locale, Record<string, FilePath>][]\n      ).flatMap(([locale, keysRecord]) =>\n        Object.entries(keysRecord).map(([key, absoluteFilePath]) => ({\n          key,\n          locale,\n          uri: absoluteFilePath,\n          filePath: absoluteFilePath,\n        }))\n      );\n\n      return entries;\n    },\n\n    read: async (entry, context) => await readEntry(entry.uri, context),\n\n    write: async ({ key, locale }, content, { configuration }) => {\n      const builderPath = await parseFilePathPattern(\n        source,\n        buildFilePathPatternContext(key, locale)\n      );\n\n      const absoluteFilePath = resolve(\n        configuration.system.baseDir,\n        builderPath\n      );\n\n      await mkdir(dirname(absoluteFilePath), { recursive: true });\n\n      await writeFileIfChanged(\n        absoluteFilePath,\n        codec.serialize(content, { locale }),\n        { tempDir: configuration.system?.tempDir }\n      );\n    },\n\n    resolveUri: async ({ key, locale }, { configuration }) => {\n      const builderPath = await parseFilePathPattern(\n        source,\n        buildFilePathPatternContext(key, locale)\n      );\n\n      return resolve(configuration.system.baseDir, builderPath);\n    },\n\n    getFillPattern: async () => await getPatternMarker(),\n\n    describeSource: async () => await getPatternMarker(),\n\n    detectSplitKeys: async () =>\n      !(await getPatternMarker()).includes('{{key}}'),\n  };\n};\n\n/**\n * Default `readEntry` implementation: plain utf-8 read followed by the given\n * parser. Returns `undefined` for missing or unreadable files.\n */\nexport const createFileReader =\n  (parse: (raw: string) => SyncContent) =>\n  async (absoluteFilePath: string): Promise<SyncContent | undefined> => {\n    try {\n      const raw = await readFile(absoluteFilePath, 'utf-8');\n\n      return parse(raw);\n    } catch {\n      return undefined;\n    }\n  };\n"],"mappings":"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"}