zintljs 0.1.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present Khalid F. Shuhail
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,253 @@
1
+ <p align="center">
2
+ <br>
3
+ <br>
4
+ <a href="https://github.com/zintljs/zintl" target="_blank" rel="noopener noreferrer">
5
+ <picture>
6
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/zintljs/zintl/main/examples/website/public/favicon.svg">
7
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/zintljs/zintl/main/examples/website/public/favicon.svg">
8
+ <img alt="Zintl logo" src="https://raw.githubusercontent.com/zintljs/zintl/main/examples/website/public/favicon.svg" height="80">
9
+ </picture>
10
+ </a>
11
+ <br>
12
+ <br>
13
+ </p>
14
+
15
+ <h1 align="center">Zintl โšก</h1>
16
+
17
+ <p align="center">
18
+ <strong>Compiler-driven internationalization system for modern web applications.</strong>
19
+ </p>
20
+
21
+ <p align="center">
22
+ <a href="https://npmjs.com/package/zintljs"><img src="https://img.shields.io/npm/v/zintljs.svg?color=863bff&label=" alt="npm package"></a>
23
+ <a href="https://nodejs.org/en/about/previous-releases"><img src="https://img.shields.io/node/v/zintljs.svg?color=6a2ee3&label=node" alt="node compatibility"></a>
24
+ <a href="https://github.com/zintljs/zintl/actions"><img src="https://img.shields.io/badge/build-passing-success" alt="build status"></a>
25
+ </p>
26
+
27
+ <br/>
28
+
29
+ Zintl (pronounced [`/tsษชntl/`]) is a compile-time internationalization engine built to provide a faster, leaner, and zero-config localization experience for modern web projects. It moves internationalization from a runtime lookup bottleneck to a compile-time optimization pipeline.
30
+
31
+ It consists of two major parts:
32
+
33
+ - **A Vite Plugin & Runtime:** Integrates seamlessly into Vite's module graph to perform surgical code replacement, inlining, and HMR catalog updates.
34
+ - **A Compiler Core:** Builds dependency graphs, splits localized content into optimized translation chunks, and compiles target ICU formats into ultra-fast JS conditional branches at build time.
35
+
36
+ ---
37
+
38
+ ## Features
39
+
40
+ - โšก **Zero-Runtime Overhead (ZCU Baking):** Compiles ICU MessageFormat expressions (plurals, select enums, nesting) into pure JavaScript conditions at build time. No heavy parsing libraries are shipped to the client.
41
+ - ๐Ÿ“‚ **Smart Chunking & Code Splitting:** A graph-based boundary algorithm automatically partitions translations into entry-specific, lazy-loaded, and shared catalog chunks matching your bundler's code-splitting boundaries.
42
+ - ๐Ÿ” **Zero-Config Extraction:** Automatically extracts strings from JSX, template literals, and HTML structures. No manual translation key mapping or tedious function wrappers are required.
43
+ - ๐Ÿ‘ป **Zero-Disk Source Locale (Ghost Mode):** The source locale (typically English) is completely diskless. The compiler virtualizes it on-the-fly from the extraction manifest, eliminating redundant `{ "key": "key" }` files from your repository.
44
+ - ๐ŸŒ **HTML Metadata Projections:** Automatically extracts and translates standard HTML head tags (`title`, `meta[name="description"]`, and directionality `dir`). It bakes translations directly for static targets or injects a minimal head-blocking bootstrap script for dynamic targets.
45
+ - ๐Ÿท๏ธ **Surgical Comment Directives:** Control translation behavior with inline code comments (`@zintl-ignore` with HTML tag scoping, `@zintl-note` for translator context, and `@zintl-pass` to pass grammatical context variables).
46
+ - ๐Ÿ”„ **Lightning Fast HMR:** Surgical invalidation of translation catalogs. Accept hot updates in-place during development with zero page reloads.
47
+
48
+ ---
49
+
50
+ ## Core Architecture
51
+
52
+ Zintl operates as a **Three-Package Monorepo** separating extraction, compilation, and runtime logic:
53
+
54
+ ```
55
+ Source Code โ”€โ”€โ–ถ @zintljs/extractor (AST Scan) โ”€โ”€โ–ถ @zintljs/compiler (Graph & Baking) โ”€โ”€โ–ถ zintl (Vite Plugin & Runtime)
56
+ ```
57
+
58
+ 1. **`@zintljs/extractor`:** A pure metadata provider. It scans code syntax using high-performance AST parsers to identify translation anchors (`zintl()`), template literals, and HTML sinks without modifying source files.
59
+ 2. **`@zintljs/compiler`:** The transformation orchestrator. It builds boundary graphs, resolves file dependencies, manages Levenshtein-based typo reconciliation, and generates chunked catalogs.
60
+ 3. **`zintl`:** The developer-facing entry point. It exports the Vite plugin and runtime macros (`zintl()`, `t()`, `getLocale()`) used in code.
61
+
62
+ ---
63
+
64
+ ## Quick Start
65
+
66
+ ### 1. Installation
67
+
68
+ Install the main Zintl package using Vite+:
69
+
70
+ ```bash
71
+ vp install -D zintljs
72
+ ```
73
+
74
+ ### 2. Configure the Vite Plugin
75
+
76
+ Add the plugin to your `vite.config.ts` configuration file:
77
+
78
+ ```typescript
79
+ import { defineConfig } from "vite";
80
+ import zintl from "zintljs/vite"; // the plugin โ€” a default export
81
+
82
+ export default defineConfig({
83
+ plugins: [
84
+ zintl({
85
+ sourceLocale: "en",
86
+ locales: ["en", "ar", "fr"],
87
+ outputDir: "locales",
88
+ }),
89
+ ],
90
+ });
91
+ ```
92
+
93
+ > The plugin lives at `zintl/vite`. The bare `zintl` entry is the **macro** you call in application code (step 3) โ€” importing that one into `vite.config.ts` gives you an async no-op, not a plugin.
94
+
95
+ #### Plugin Options
96
+
97
+ Every option is optional; most projects only set `locales`. Full documentation, including what each option does to your build, is on the `Options` type โ€” hover or ctrl+click it in your editor.
98
+
99
+ | Option | Type | Default | What it does |
100
+ | --------------------- | --------------------------------- | -------------------------------- | -------------------------------------------------------------------------------- |
101
+ | `locales` | `string[]` | `["en"]` | Every locale the app ships, including the source locale. |
102
+ | `sourceLocale` | `string` | `"en"` | The locale your source is written in. Never written to disk (Ghost Mode). |
103
+ | `outputDir` | `string` | `"./zintl"` | Where catalogs are written, relative to the project root. |
104
+ | `catalogFormat` | `string \| (ctx) => string` | `<path>[.<func>].<locale>.json` | Catalog file naming. Tokens: `[locale] [path] [dir] [name] [func] [bId] [hash]`. |
105
+ | `facets` | `FacetsInput[]` | `["auto"]` | Which capabilities the compiler is built with. `"auto"` detects your framework. |
106
+ | `assetsTarget` | `(string \| AssetTargetConfig)[]` | `["md", "txt"]` | Static content files to localize alongside code. |
107
+ | `virtualAssets` | `boolean` | `false` | Serve localized assets from virtual modules instead of writing them to disk. |
108
+ | `prune` | `boolean` | `true` | Remove catalog keys once no source string produces them. |
109
+ | `debug` | `boolean \| string` | `false` | Verbose tracing. A string filters to one subsystem. |
110
+ | `logLevel` | `LogLevel` | Vite's `logLevel`, then `"info"` | How much Zintl prints. |
111
+ | `similarityThreshold` | `number` | `0.6` | How similar an edited string must be to keep its translation. |
112
+ | `verifyIntegrity` | `boolean` | `true` on build, `false` on dev | Verify catalogs against the manifest and repair drift. |
113
+ | `multiplex` | `boolean` | auto-detected | Build each locale as its own set of HTML entries. |
114
+ | `metadataDir` | `string` | `<root>/node_modules/.zintl` | Where the compiler keeps its own bookkeeping. |
115
+
116
+ ### 3. Initialize in Source Code
117
+
118
+ Establish a **Trust Anchor** in your application entry point. Every file or function calling `zintl()` forms an independent translation boundary with its own lazy catalog loading:
119
+
120
+ ```typescript
121
+ // src/main.ts
122
+ import { zintl } from "zintljs/macro";
123
+
124
+ async function initApp() {
125
+ const userLang = new URLSearchParams(window.location.search).get("lang") || "en";
126
+
127
+ // Sets the active locale and loads necessary catalog chunks
128
+ await zintl(userLang);
129
+
130
+ document.querySelector("#app")!.innerHTML = `
131
+ <h1>Welcome back!</h1>
132
+ <p>You have successfully logged in.</p>
133
+ `;
134
+ }
135
+
136
+ initApp();
137
+ ```
138
+
139
+ ---
140
+
141
+ ## The Comment Directive System
142
+
143
+ Use comments directly in your source code (JavaScript `//`, `/* */` or HTML `<!-- -->`) to guide the compiler surgically:
144
+
145
+ ### `@zintl-ignore`
146
+
147
+ Suppresses translation extraction for the immediate next node or HTML tag and its nested subtree:
148
+
149
+ ```jsx
150
+ <div>
151
+ {/* @zintl-ignore */}
152
+ <span>This text will not be extracted</span>
153
+ <span>This text will be extracted</span>
154
+ </div>
155
+ ```
156
+
157
+ ### `@zintl-note`
158
+
159
+ Attaches translator notes that are automatically injected into the generated translation schema:
160
+
161
+ ```typescript
162
+ // @zintl-note Welcome message on the user's dashboard
163
+ const welcomeMsg = `Hello, user!`;
164
+ ```
165
+
166
+ ### `@zintl-pass`
167
+
168
+ Binds invisible grammatical context variables (like gender, role, or counts) to the extraction scope. This allows target languages to use advanced grammatical logic (e.g., ICU Plurals or Select) even if they aren't visible in the source English text:
169
+
170
+ ```typescript
171
+ // @zintl-pass role={user.role}
172
+ const dashboardTitle = `Welcome to your dashboard!`;
173
+ ```
174
+
175
+ ---
176
+
177
+ ## The ZCU (Component-based ICU) Agreement
178
+
179
+ Zintl implements a **Source Purity** philosophy. Developers do not write complex grammatical logic inside their source code. Instead, source files contain simple template literals, and grammatical variations are managed as catalog data:
180
+
181
+ 1. **Source Code:** Write clean, standard JS template literals:
182
+ ```typescript
183
+ const msg = `You have ${count} items in your cart`;
184
+ ```
185
+ 2. **Target Translation Catalog (`locales/ar.json`):** Translators write standard ICU MessageFormat syntax inside the JSON files, backed by IDE auto-complete schemas:
186
+ ```json
187
+ {
188
+ "$schema": "./.schemas/locales.schema.json",
189
+ "You have {count} items in your cart": "{count, plural, =0 {ุณู„ุชูƒ ูุงุฑุบุฉ} one {ู„ุฏูŠูƒ ุนู†ุตุฑ ูˆุงุญุฏ ููŠ ุณู„ุชูƒ} other {ู„ุฏูŠูƒ {count} ุนู†ุงุตุฑ ููŠ ุณู„ุชูƒ}}"
190
+ }
191
+ ```
192
+ 3. **Compilation:** At build time, the Zintl compiler parses the ICU syntax and compiles ("bakes") it into optimized JavaScript conditional logic:
193
+ ```javascript
194
+ // Compiled output (Smart Manager)
195
+ (params) => {
196
+ const { count } = params;
197
+ if (count === 0) return `ุณู„ุชูƒ ูุงุฑุบุฉ`;
198
+ if (count === 1) return `ู„ุฏูŠูƒ ุนู†ุตุฑ ูˆุงุญุฏ ููŠ ุณู„ุชูƒ`;
199
+ return `ู„ุฏูŠูƒ ${count} ุนู†ุงุตุฑ ููŠ ุณู„ุชูƒ`;
200
+ };
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Packages
206
+
207
+ | Package | Version | Description |
208
+ | :--------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------- |
209
+ | [**`zintl`**](packages/zintl) | [![version](https://img.shields.io/npm/v/zintljs.svg?color=863bff&label=%20)](packages/zintl/CHANGELOG.md) | Vite plugin & macro runtime library. |
210
+ | [**`@zintljs/compiler`**](packages/compiler) | [![version](https://img.shields.io/npm/v/@zintljs/compiler.svg?color=863bff&label=%20)](packages/compiler/CHANGELOG.md) | Graph management, HTML projection & ICU baking compiler. |
211
+ | [**`@zintljs/extractor`**](packages/extractor) | [![version](https://img.shields.io/npm/v/@zintljs/extractor.svg?color=863bff&label=%20)](packages/extractor/CHANGELOG.md) | AST-based string & dependency extraction utility. |
212
+
213
+ ---
214
+
215
+ ## Contributing
216
+
217
+ Contributions are welcome! Please read the [Contributing Guide](CONTRIBUTING.md) to learn about the monorepo setup, development commands, and codebase guidelines.
218
+
219
+ ## License
220
+
221
+ [MIT](LICENSE).
222
+
223
+ ---
224
+
225
+ ## What's genuinely brilliant
226
+
227
+ **The Boundary Graph is a paradigm shift.** Most i18n systems are flat dictionaries. Zintl treats translations as a _dependency graph problem_ โ€” the same mental model bundlers use for code splitting. The idea that "a file with a `zintl()` call becomes a trust anchor, and only strings reachable from that anchor need to be translated" is deeply correct and eliminates an entire class of bloat that ships in every other i18n library.
228
+
229
+ **Ghost Mode (Zero-Disk Source Locale) is elegant.** Generating `{ "key": "key" }` for English is genuinely redundant โ€” the AST already has the strings. Virtualizing the source locale from the extraction manifest instead of materializing it to disk is the kind of insight that only comes from thinking about the problem correctly from first principles.
230
+
231
+ **The Intelligent Stitching engine** โ€” treating template literals, JSX fragments, and HTML strings as _logical units_ rather than raw strings โ€” is what separates Zintl from simple `i18next.t()` wrappers. Most systems make you manually wrap every string. Zintl reads intent from structure.
232
+
233
+ **Hive (translation memory) with fuzzy recovery** is seriously underrated. The idea that a minor edit to a source string should forward-port the existing translation (with a warning) instead of wiping it is exactly how professional translators think. Most tools just orphan translations on source changes.
234
+
235
+ ---
236
+
237
+ ## Where I see real risk
238
+
239
+ **The phantom boundary class of bugs is a structural vulnerability.** The aggressive extraction heuristic (`/zintl|loadI18nInstance|t\(|<|innerHTML/.test(ctx.code)`) runs on every file before the graph is built. The fix we applied today โ€” gating at `verifyIntegrity` and `syncSingleAsset` โ€” is correct, but it means phantom extractions _exist silently in the manifest_ even when they're eventually blocked from writing. In a large project with many non-anchored files, this is unneeded memory and CPU pressure on every build. The longer-term fix would be to not extract at all when there are no entries โ€” but that requires knowing the graph before extracting, which is a chicken-and-egg problem with the current sequential pipeline.
240
+
241
+ **The `resolve.ts` hook is doing too much.** That file is 636 lines handling multiplexing, asset registration, SFC proxy files, HTML fanning, virtual module resolution, AND the translation-neutrality traversal. It's a load-bearing monolith. Any new framework (vinext, Nuxt, Astro) that has a slightly different module resolution model will cause subtle bugs here. Each concern deserves its own hook.
242
+
243
+ **The "special cases" are accumulating.** HTML projections, SSR boundaries, HTML fanning, zero-config mode, zintl markers vs. macros vs. anchors โ€” each is individually justified, but the interaction surface is growing. The test suite is the only thing holding this together right now, and there are already 5 skipped tests.
244
+
245
+ ---
246
+
247
+ ## The meta-observation
248
+
249
+ Zintl is solving the _right_ problem โ€” most i18n tools treat internationalization as a runtime lookup problem, when it's actually a **compilation and bundling problem**. The strings that need to be translated are known at build time. The code-splitting boundary that determines which strings load when is known at build time. Zintl is the only system I know of that treats both of these facts seriously.
250
+
251
+ The risk is that the implementation complexity is approaching the complexity of the problem it's solving. That's usually a sign that the abstraction layer needs a checkpoint โ€” either a stricter API surface (fewer escape hatches), or a cleaner separation between the extractor (which should be dumb and greedy), the compiler (which should be the authority on what's "real"), and the runtime (which should be minimal and trust the compiler completely).
252
+
253
+ The bones are exceptional. The muscle needs careful discipline as the feature surface grows.
@@ -0,0 +1,348 @@
1
+ import { compileExtractionState } from "@zintljs/compiler";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { assetsFacet, clientSpaFacet, htmlFacet, nextjsFacet, reactFacet, ssrFacet, svelteFacet, vanillaFacet, viteFacet, vueFacet } from "@zintljs/compiler/facets";
5
+ //#region src/facets/resolve.ts
6
+ /**
7
+ * Facet resolution โ€” the orchestration layer.
8
+ *
9
+ * This is deliberately part of the *plugin*, not the compiler. Resolution is the
10
+ * act of deciding what the compiler will be, and that decision belongs to the
11
+ * layer that knows about the host toolchain:
12
+ *
13
+ * extractor โ† compiler (core) โ† compiler/facets โ† zintl (plugin)
14
+ *
15
+ * The compiler receives the output of this function and executes it. It never
16
+ * calls in here, and it has no idea that React, Vue, Svelte, Next, SSR or Vite
17
+ * exist.
18
+ *
19
+ * Merge semantics:
20
+ * - Arrays (targets, extensions, rules, ssr entry targets/exports) union.
21
+ * - Runtime booleans OR together.
22
+ * - Function hooks resolve by highest priority, and a tie is a hard error.
23
+ * - Codegen facets stay a list, matched per-file; two facets claiming the same
24
+ * extension at the same priority is a hard error.
25
+ */
26
+ /**
27
+ * Highest-priority-wins for function hooks, with conflict detection on ties.
28
+ */
29
+ function mergeHook(existing, existingPriority, candidate, candidatePriority, hookName, existingFacetName, candidateFacetName) {
30
+ if (candidate === void 0) return existing;
31
+ if (existing === void 0) return candidate;
32
+ if (candidatePriority > existingPriority) return candidate;
33
+ if (existingPriority > candidatePriority) return existing;
34
+ throw new Error(`[Zintl] Facet conflict: both "${existingFacetName}" and "${candidateFacetName}" provide "${hookName}" at the same priority (${existingPriority}). Only one facet may contribute this hook. Increase priority on one, or remove the other.`);
35
+ }
36
+ /**
37
+ * Merge codegen facets with file extension conflict detection.
38
+ */
39
+ function mergeCodegenFacets(existing, candidate, candidateName) {
40
+ if (!candidate) return existing;
41
+ for (const existing_codegen of existing) for (const ext of candidate.extensions) if (existing_codegen.extensions.includes(ext)) {
42
+ const existingPriority = existing_codegen.priority ?? 0;
43
+ if (existingPriority === (candidate.priority ?? 0)) throw new Error(`[Zintl] Facet conflict: codegen facets from "${existing_codegen.name}" and "${candidateName}" both claim extension "${ext}" at the same priority (${existingPriority}). Only one codegen facet may handle a given extension at the same priority.`);
44
+ }
45
+ return [...existing, candidate];
46
+ }
47
+ function createEmptyState() {
48
+ return {
49
+ codegenFacets: [],
50
+ extractionTargets: [],
51
+ extensions: [],
52
+ sfcRules: [],
53
+ suppressionRules: [],
54
+ mustacheRules: [],
55
+ clientReactivityImports: {},
56
+ contentFacets: [],
57
+ ssrEntryTargets: [],
58
+ ssrWrapCode: void 0,
59
+ ssrWrapCodeProvider: "",
60
+ ssrWrapCodePriority: -1,
61
+ ssrWrapExports: [],
62
+ ssrWrapDefault: void 0,
63
+ clientLocaleSync: false,
64
+ serverRequestScope: false,
65
+ streamInjection: false,
66
+ detectLocaleChain: [],
67
+ resolveVirtualPath: void 0,
68
+ resolveVirtualPathProvider: "",
69
+ resolveVirtualPathPriority: -1,
70
+ dynamicImportTemplate: void 0,
71
+ dynamicImportTemplateProvider: "",
72
+ dynamicImportTemplatePriority: -1,
73
+ hmrInjectionCode: void 0,
74
+ hmrInjectionCodeProvider: "",
75
+ hmrInjectionCodePriority: -1,
76
+ getProtectedCatalogKeysChain: []
77
+ };
78
+ }
79
+ function mergeFacet(state, facet) {
80
+ const name = facet.name;
81
+ const priority = facet.priority ?? 0;
82
+ switch (facet.concern) {
83
+ case "extraction":
84
+ for (const t of facet.targets) if (!state.extractionTargets.includes(t)) state.extractionTargets.push(t);
85
+ for (const e of facet.extensions ?? []) if (!state.extensions.includes(e)) state.extensions.push(e);
86
+ if (facet.sfcRules) state.sfcRules.push(...facet.sfcRules);
87
+ if (facet.suppressionRules) state.suppressionRules.push(...facet.suppressionRules);
88
+ if (facet.mustacheRegex) state.mustacheRules.push({
89
+ extensions: facet.extensions || [],
90
+ pattern: facet.mustacheRegex
91
+ });
92
+ break;
93
+ case "content":
94
+ state.contentFacets.push(facet);
95
+ if (facet.getProtectedCatalogKeys) state.getProtectedCatalogKeysChain.push(facet.getProtectedCatalogKeys);
96
+ break;
97
+ case "codegen":
98
+ state.codegenFacets = mergeCodegenFacets(state.codegenFacets, facet, name);
99
+ for (const [source, specifiers] of Object.entries(facet.clientReactivityImports ?? {})) {
100
+ const existing = state.clientReactivityImports[source] ??= [];
101
+ for (const spec of specifiers) if (!existing.includes(spec)) existing.push(spec);
102
+ }
103
+ for (const e of facet.extensions) if (!state.extensions.includes(e)) state.extensions.push(e);
104
+ break;
105
+ case "ssr":
106
+ if (facet.entryTargets) state.ssrEntryTargets.push(...facet.entryTargets);
107
+ if (facet.wrapCode !== void 0) {
108
+ state.ssrWrapCode = mergeHook(state.ssrWrapCode, state.ssrWrapCodePriority, facet.wrapCode, priority, "ssr.wrapCode", state.ssrWrapCodeProvider, name);
109
+ if (state.ssrWrapCode === facet.wrapCode) {
110
+ state.ssrWrapCodeProvider = name;
111
+ state.ssrWrapCodePriority = priority;
112
+ }
113
+ }
114
+ if (facet.wrapExports) state.ssrWrapExports.push(...facet.wrapExports);
115
+ if (facet.wrapDefault !== void 0 && state.ssrWrapDefault === void 0) state.ssrWrapDefault = facet.wrapDefault;
116
+ break;
117
+ case "runtime":
118
+ if (facet.clientLocaleSync) state.clientLocaleSync = true;
119
+ if (facet.serverRequestScope) state.serverRequestScope = true;
120
+ if (facet.streamInjection) state.streamInjection = true;
121
+ if (facet.detectLocale) state.detectLocaleChain.push(facet.detectLocale);
122
+ break;
123
+ case "bundler":
124
+ if (facet.resolveVirtualPath !== void 0) {
125
+ state.resolveVirtualPath = mergeHook(state.resolveVirtualPath, state.resolveVirtualPathPriority, facet.resolveVirtualPath, priority, "bundler.resolveVirtualPath", state.resolveVirtualPathProvider, name);
126
+ if (state.resolveVirtualPath === facet.resolveVirtualPath) {
127
+ state.resolveVirtualPathProvider = name;
128
+ state.resolveVirtualPathPriority = priority;
129
+ }
130
+ }
131
+ if (facet.dynamicImportTemplate !== void 0) {
132
+ state.dynamicImportTemplate = mergeHook(state.dynamicImportTemplate, state.dynamicImportTemplatePriority, facet.dynamicImportTemplate, priority, "bundler.dynamicImportTemplate", state.dynamicImportTemplateProvider, name);
133
+ if (state.dynamicImportTemplate === facet.dynamicImportTemplate) {
134
+ state.dynamicImportTemplateProvider = name;
135
+ state.dynamicImportTemplatePriority = priority;
136
+ }
137
+ }
138
+ if (facet.hmrInjectionCode !== void 0) {
139
+ state.hmrInjectionCode = mergeHook(state.hmrInjectionCode, state.hmrInjectionCodePriority, facet.hmrInjectionCode, priority, "bundler.hmrInjectionCode", state.hmrInjectionCodeProvider, name);
140
+ if (state.hmrInjectionCode === facet.hmrInjectionCode) {
141
+ state.hmrInjectionCodeProvider = name;
142
+ state.hmrInjectionCodePriority = priority;
143
+ }
144
+ }
145
+ break;
146
+ }
147
+ }
148
+ function stateToCapabilities(state) {
149
+ return {
150
+ jsx: state.codegenFacets.some((a) => a.wrapJsxRichText !== void 0 || a.serializeTags !== void 0),
151
+ sfc: state.codegenFacets.some((a) => a.wrapSfcScript !== void 0),
152
+ jsxRichText: state.codegenFacets.some((a) => a.wrapJsxRichText !== void 0),
153
+ clientLocaleSync: state.clientLocaleSync,
154
+ serverRequestScope: state.serverRequestScope,
155
+ streaming: state.streamInjection,
156
+ ssr: state.ssrEntryTargets.length > 0 || state.ssrWrapCode !== void 0 || state.serverRequestScope || state.streamInjection,
157
+ hmr: state.hmrInjectionCode !== void 0,
158
+ localeRouting: state.clientLocaleSync || state.serverRequestScope
159
+ };
160
+ }
161
+ const DEFAULT_RESOLVE_VIRTUAL_PATH = (id) => id;
162
+ const DEFAULT_DYNAMIC_IMPORT_TEMPLATE = (path, _isDev) => `import(${JSON.stringify(path)})`;
163
+ function stateToHooks(state) {
164
+ const chain = state.detectLocaleChain;
165
+ const detectLocale = chain.length === 0 ? void 0 : (context) => {
166
+ for (const fn of chain) {
167
+ const result = fn(context);
168
+ if (result !== void 0) return result;
169
+ }
170
+ };
171
+ const virtualBoundaries = [];
172
+ for (const facet of state.contentFacets) if (facet.virtualBoundaries) virtualBoundaries.push(...facet.virtualBoundaries);
173
+ const getProtectedCatalogKeysChain = state.getProtectedCatalogKeysChain;
174
+ const getProtectedCatalogKeys = async (boundaryId, context) => {
175
+ const allKeys = /* @__PURE__ */ new Set();
176
+ for (const fn of getProtectedCatalogKeysChain) {
177
+ const keys = await fn(boundaryId, context);
178
+ if (keys) for (const k of keys) allKeys.add(k);
179
+ }
180
+ return Array.from(allKeys);
181
+ };
182
+ return {
183
+ codegenFacets: state.codegenFacets,
184
+ extractionTargets: state.extractionTargets,
185
+ extensions: state.extensions,
186
+ sfcRules: state.sfcRules,
187
+ suppressionRules: state.suppressionRules,
188
+ mustacheRules: state.mustacheRules,
189
+ clientReactivityImports: state.clientReactivityImports,
190
+ contentFacets: state.contentFacets,
191
+ virtualBoundaries,
192
+ ssrEntryTargets: state.ssrEntryTargets,
193
+ ssrWrapCode: state.ssrWrapCode,
194
+ ssrWrapExports: state.ssrWrapExports,
195
+ ssrWrapDefault: state.ssrWrapDefault,
196
+ resolveVirtualPath: state.resolveVirtualPath ?? DEFAULT_RESOLVE_VIRTUAL_PATH,
197
+ dynamicImportTemplate: state.dynamicImportTemplate ?? DEFAULT_DYNAMIC_IMPORT_TEMPLATE,
198
+ hmrInjectionCode: state.hmrInjectionCode,
199
+ detectLocale,
200
+ getProtectedCatalogKeys
201
+ };
202
+ }
203
+ /**
204
+ * Resolve a flat list of facet objects into the capabilities handed to the compiler.
205
+ */
206
+ function resolveFacets(facets = []) {
207
+ const sorted = [...[...facets]].sort((a, b) => {
208
+ const pA = a.priority ?? 0;
209
+ return (b.priority ?? 0) - pA;
210
+ });
211
+ const uniqueFacets = [];
212
+ const seen = /* @__PURE__ */ new Set();
213
+ for (const a of sorted) if (!seen.has(a.name)) {
214
+ seen.add(a.name);
215
+ uniqueFacets.push(a);
216
+ }
217
+ const state = createEmptyState();
218
+ for (const facet of uniqueFacets) mergeFacet(state, facet);
219
+ const flags = stateToCapabilities(state);
220
+ const system = stateToHooks(state);
221
+ return {
222
+ flags,
223
+ system,
224
+ facets: uniqueFacets,
225
+ extraction: compileExtractionState(system)
226
+ };
227
+ }
228
+ //#endregion
229
+ //#region src/facets/detect.ts
230
+ /**
231
+ * Framework detection.
232
+ *
233
+ * Part of the orchestration layer: deciding *which* facets a project needs is
234
+ * the plugin's job. Neither the compiler nor the extractor may contain this
235
+ * knowledge.
236
+ */
237
+ /** The framework assumed when detection finds nothing. */
238
+ const FALLBACK_FRAMEWORK = "react";
239
+ /**
240
+ * Detect frameworks from bundler plugin names and the project's package.json.
241
+ *
242
+ * Returns an empty array when nothing matched โ€” callers decide whether to apply
243
+ * {@link FALLBACK_FRAMEWORK}, so the guess stays visible rather than buried.
244
+ */
245
+ function detectFrameworks({ pluginNames = [], root }) {
246
+ const frameworks = /* @__PURE__ */ new Set();
247
+ for (const raw of pluginNames) {
248
+ if (!raw) continue;
249
+ const name = raw.toLowerCase();
250
+ if (name.includes("vue")) frameworks.add("vue");
251
+ if (name.includes("react")) frameworks.add("react");
252
+ if (name.includes("svelte")) frameworks.add("svelte");
253
+ if (name.includes("next") || name.includes("vinext")) frameworks.add("nextjs");
254
+ }
255
+ if (root) try {
256
+ const pkgPath = join(root, "package.json");
257
+ if (existsSync(pkgPath)) {
258
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
259
+ const allDeps = {
260
+ ...pkg.dependencies,
261
+ ...pkg.devDependencies,
262
+ ...pkg.peerDependencies
263
+ };
264
+ if (allDeps["vue"]) frameworks.add("vue");
265
+ if (allDeps["react"]) frameworks.add("react");
266
+ if (allDeps["svelte"] || allDeps["@sveltejs/kit"]) frameworks.add("svelte");
267
+ if (allDeps["next"] || allDeps["vinext"]) frameworks.add("nextjs");
268
+ }
269
+ } catch {}
270
+ return Array.from(frameworks);
271
+ }
272
+ /** Detect, falling back to {@link FALLBACK_FRAMEWORK} when nothing matched. */
273
+ function detectFrameworksOrFallback(input) {
274
+ const detected = detectFrameworks(input);
275
+ return detected.length > 0 ? detected : [FALLBACK_FRAMEWORK];
276
+ }
277
+ //#endregion
278
+ //#region src/facets/assemble.ts
279
+ /**
280
+ * Facet assembly โ€” turning detected frameworks and user input into a flat facet
281
+ * list, ready for resolution.
282
+ *
283
+ * This is the one place that decides which facets a project gets. Every
284
+ * previously-implicit default now lives here, in the open:
285
+ *
286
+ * - the `"auto"` sentinel expands to the detected framework set plus baselines;
287
+ * - `vanillaFacet`, `htmlFacet` and `assetsFacet` are always part of `"auto"`;
288
+ * - `clientSpaFacet` is added unless the project is Next.js;
289
+ * - `ssrFacet` is added for SSR builds that are not Next.js;
290
+ * - `viteFacet` is always appended, and cannot be opted out of.
291
+ */
292
+ /**
293
+ * The facet set that `"auto"` expands to.
294
+ */
295
+ function autoFacets(input) {
296
+ const { frameworks, ssr = false, assetsTarget, virtualAssets } = input;
297
+ const out = [];
298
+ for (const f of frameworks) if (f === "vue") out.push(vueFacet());
299
+ else if (f === "react") out.push(reactFacet());
300
+ else if (f === "svelte") out.push(svelteFacet());
301
+ else if (f === "nextjs") out.push(reactFacet(), nextjsFacet());
302
+ const isNext = frameworks.includes("nextjs");
303
+ if (ssr && !isNext) out.push(ssrFacet());
304
+ if (!isNext) out.push(clientSpaFacet());
305
+ out.push(vanillaFacet(), htmlFacet(), assetsFacet({
306
+ targets: assetsTarget,
307
+ virtualAssets
308
+ }));
309
+ return out.flat(Infinity);
310
+ }
311
+ /**
312
+ * Flatten user facet input, expanding `"auto"`, thunks and nested arrays.
313
+ */
314
+ function flattenFacets(inputs, auto) {
315
+ const result = [];
316
+ function processInput(input) {
317
+ if (!input) return;
318
+ if (input === "auto") {
319
+ for (const f of auto) processInput(f);
320
+ return;
321
+ }
322
+ if (typeof input === "function") {
323
+ processInput(input());
324
+ return;
325
+ }
326
+ if (Array.isArray(input)) {
327
+ for (const item of input) processInput(item);
328
+ return;
329
+ }
330
+ if (typeof input === "object") {
331
+ result.push(input);
332
+ return;
333
+ }
334
+ }
335
+ for (const input of inputs) processInput(input);
336
+ return result;
337
+ }
338
+ /**
339
+ * The complete facet list for a project: user input (with `"auto"` expanded)
340
+ * plus the always-injected Vite bundler facet.
341
+ */
342
+ function assembleFacets(input) {
343
+ const facets = flattenFacets(input.facets ?? ["auto"], autoFacets(input));
344
+ facets.push(viteFacet());
345
+ return facets;
346
+ }
347
+ //#endregion
348
+ export { detectFrameworks as a, FALLBACK_FRAMEWORK as i, autoFacets as n, detectFrameworksOrFallback as o, flattenFacets as r, resolveFacets as s, assembleFacets as t };