{"version":3,"file":"tempest-icons.cjs","names":[],"sources":["../../src/vite/tempest-icons.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\nimport { iconAliases } from \"@/icons/generated/aliases\";\nimport { iconNames } from \"@/icons/generated/icon-names\";\n\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** The module id an app imports to get its static registry. */\nexport const TEMPEST_ICONS_ID = \"tempest-react-sdk/icons/virtual\";\n\n/**\n * The original virtual id, still honoured.\n *\n * Published as the documented import before the subpath existed, so apps have it\n * written in their entrypoints. It resolves to the same generated module — but it\n * only ever resolves *with the plugin*, which is the reason the subpath above\n * replaced it: that one also resolves without it, to an empty stub.\n */\nexport const TEMPEST_ICONS_VIRTUAL_ID = \"virtual:tempest-icons\";\n\nconst RESOLVED_ID = `\\0${TEMPEST_ICONS_VIRTUAL_ID}`;\n\n/**\n * Slugs written as a literal `name` prop or a literal `name:` field.\n *\n * Matching the *attribute* rather than any kebab-case string is what keeps the\n * scan from sweeping in unrelated text — a CSS class, a translation key and a\n * route segment all look like slugs otherwise. Everything matched is still\n * validated against lucide's real slug list before it reaches the module.\n */\nconst NAME_PATTERNS = [\n    /\\bname\\s*=\\s*[\"']([a-z0-9-]+)[\"']/g,\n    /\\bname\\s*=\\s*\\{\\s*[\"']([a-z0-9-]+)[\"']\\s*\\}/g,\n    /\\bname\\s*:\\s*[\"']([a-z0-9-]+)[\"']/g,\n];\n\nconst SOURCE_FILE = /\\.[cm]?[jt]sx?$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n\nexport interface TempestIconsOptions {\n    /**\n     * Directory to scan, relative to the Vite root. Default: `\"src\"`.\n     */\n    dir?: string;\n    /**\n     * Extra slugs to include even when the scan cannot see them — a name built by\n     * concatenation, or one that only ever arrives from the API but is worth\n     * paying for statically because it is on the first screen.\n     */\n    include?: readonly string[];\n    /** Directory names to skip. Default: `node_modules`, `dist`, `build`, `coverage`. */\n    skipDirs?: readonly string[];\n}\n\n/**\n * Collect the icon slugs a source file references literally.\n *\n * @param code - File contents.\n * @param known - The set of real lucide slugs, aliases included.\n * @returns The slugs found, validated against `known`.\n */\nexport function scanIconSlugs(code: string, known: ReadonlySet<string>): string[] {\n    const found = new Set<string>();\n    for (const pattern of NAME_PATTERNS) {\n        pattern.lastIndex = 0;\n        let match: RegExpExecArray | null;\n        while ((match = pattern.exec(code)) !== null) {\n            if (known.has(match[1])) found.add(match[1]);\n        }\n    }\n    return [...found];\n}\n\n/**\n * Build the virtual module source for a set of slugs.\n *\n * Aliases resolve to their canonical slug before importing, because only the\n * canonical name is a real lucide export — but the alias key still points at the\n * component, so `<Icon name=\"alert-circle\" />` stays a zero-request lookup.\n *\n * @param slugs - Validated slugs to include.\n * @param aliases - Alias slug → canonical slug.\n * @returns ES module source.\n */\nexport function buildIconsModule(\n    slugs: readonly string[],\n    aliases: Record<string, string>,\n): string {\n    const sorted = [...new Set(slugs)].sort();\n    if (!sorted.length) return \"export const staticIcons = {};\\n\";\n\n    const componentOf = new Map<string, string>();\n    for (const slug of sorted) componentOf.set(slug, pascalCase(aliases[slug] ?? slug));\n\n    const named = [...new Set(componentOf.values())]\n        .sort()\n        .map((name) => (localName(name) === name ? name : `${name} as ${localName(name)}`))\n        .join(\", \");\n    const entries = sorted\n        .map((slug) => `    \"${slug}\": ${localName(componentOf.get(slug) as string)},`)\n        .join(\"\\n\");\n    return `import { ${named} } from \"lucide-react\";\n\nexport const staticIcons = {\n${entries}\n};\n`;\n}\n\n/** `circle-alert` → `CircleAlert`; `a-arrow-down` → `AArrowDown`. */\nfunction pascalCase(slug: string): string {\n    return slug\n        .split(\"-\")\n        .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n        .join(\"\");\n}\n\n/**\n * Export names that would shadow a restricted global if bound directly.\n *\n * Lucide ships an icon literally called `infinity`, whose export is `Infinity`.\n * Binding that at module scope shadows the global, so the import is aliased.\n */\nconst RESERVED_NAMES = new Set([\"Infinity\", \"NaN\", \"undefined\", \"eval\", \"arguments\"]);\n\n/** The local binding to use for a lucide export name. */\nfunction localName(name: string): string {\n    return RESERVED_NAMES.has(name) ? `${name}Icon` : name;\n}\n\n/**\n * Walk a directory and collect every source file path.\n *\n * @param dir - Absolute directory to walk.\n * @param skip - Directory names to skip.\n * @returns Absolute file paths.\n */\nasync function collectSourceFiles(dir: string, skip: ReadonlySet<string>): Promise<string[]> {\n    const out: string[] = [];\n    let entries;\n    try {\n        entries = await readdir(dir, { withFileTypes: true });\n    } catch {\n        return out;\n    }\n    for (const entry of entries) {\n        if (entry.isDirectory()) {\n            if (skip.has(entry.name) || entry.name.startsWith(\".\")) continue;\n            out.push(...(await collectSourceFiles(join(dir, entry.name), skip)));\n        } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {\n            out.push(join(dir, entry.name));\n        }\n    }\n    return out;\n}\n\n/**\n * Generate a static icon registry from the slugs your source mentions.\n *\n * Removes the reason to reach for `lucide-react`'s `DynamicIcon`: the slugs an app\n * writes as literals become ordinary static imports, so the bundler keeps exactly\n * those icons and the browser makes **no** extra request for them. A slug that only\n * exists at runtime still works — `<Icon>` falls back to fetching one shard per\n * initial letter.\n *\n * The scan reads the source tree directly at `buildStart` rather than harvesting\n * modules as they pass through `transform`: relying on transform order would let\n * the virtual module load before every consumer had been seen, and the registry\n * would come out short on a cold dev start — silently, since the missing slugs\n * would still render via the lazy path.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestIcons()] });\n *\n * @example\n * // src/main.tsx\n * import { IconProvider } from \"tempest-react-sdk/icons\";\n * import { staticIcons } from \"tempest-react-sdk/icons/virtual\";\n *\n * <IconProvider registry={staticIcons}>\n *     <App />\n * </IconProvider>\n */\nexport function tempestIcons(options: TempestIconsOptions = {}): TempestVitePlugin {\n    const { dir = \"src\", include = [], skipDirs } = options;\n    const known = new Set<string>(iconNames);\n    const skip = new Set<string>(skipDirs ?? SKIP_DIRS);\n    const slugs = new Set<string>(include.filter((slug) => known.has(slug)));\n    let root = process.cwd();\n\n    /**\n     * Rescan the source tree, keeping the explicitly included slugs.\n     *\n     * @tempest-limits empty-catch — the scan races the editor and the file system:\n     * a file listed a moment ago can be renamed, deleted, or held by another process\n     * by the time it is read. An unreadable file contributes no slugs and the build\n     * continues; failing the whole scan would break `vite dev` over a temp file that\n     * no longer exists.\n     */\n    const rescan = async (): Promise<void> => {\n        const files = await collectSourceFiles(resolve(root, dir), skip);\n        for (const file of files) {\n            try {\n                const code = await readFile(file, \"utf8\");\n                for (const slug of scanIconSlugs(code, known)) slugs.add(slug);\n            } catch {\n                /* empty */\n            }\n        }\n    };\n\n    return {\n        name: \"tempest-icons\",\n        enforce: \"pre\",\n\n        configResolved(config: { root?: string }) {\n            root = config.root ?? root;\n        },\n\n        async buildStart() {\n            await rescan();\n        },\n\n        /**\n         * Claim both spellings of the registry module.\n         *\n         * The subpath is a real file in the installed package, so returning the\n         * virtual id here is what overrides the empty stub with the generated\n         * table. Letting Vite resolve it normally would silently ship the stub.\n         */\n        resolveId(id: string) {\n            return id === TEMPEST_ICONS_ID || id === TEMPEST_ICONS_VIRTUAL_ID ? RESOLVED_ID : null;\n        },\n\n        load(id: string) {\n            return id === RESOLVED_ID ? buildIconsModule([...slugs], iconAliases) : null;\n        },\n\n        /**\n         * Pick up a slug added while the dev server is running.\n         *\n         * A newly written `<Icon name=\"save\" />` would otherwise only render via\n         * the lazy path until the next restart. Rescanning the changed file and\n         * invalidating the virtual module keeps dev and build agreeing on what the\n         * static registry contains.\n         */\n        async handleHotUpdate(ctx: {\n            file: string;\n            read: () => Promise<string>;\n            server: { moduleGraph: { getModuleById: (id: string) => unknown } };\n        }): Promise<unknown[] | undefined> {\n            if (!SOURCE_FILE.test(ctx.file)) return undefined;\n            const before = slugs.size;\n            for (const slug of scanIconSlugs(await ctx.read(), known)) slugs.add(slug);\n            if (slugs.size === before) return undefined;\n            const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);\n            return mod ? [mod] : undefined;\n        },\n    } as TempestVitePlugin;\n}\n"],"mappings":"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"}