@fanee/vite 0.6.0
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/README.md +66 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @fanee/vite
|
|
2
|
+
|
|
3
|
+
Vite plugin for the Open Translation Bundle (OTB) format.
|
|
4
|
+
|
|
5
|
+
Scans an OTB bundle at build time, resolves module hierarchy, performs the OTB merge algorithm, and exposes the merged resources as a virtual module.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -D @fanee/vite
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### TypeScript Configuration
|
|
14
|
+
|
|
15
|
+
In order to make virtual modules available in TypeScript, add the following to your `tsconfig.json`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"compilerOptions": {
|
|
20
|
+
"types": ["vite/client", "@fanee/vite/client"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// vite.config.ts
|
|
29
|
+
import { defineConfig } from "vite";
|
|
30
|
+
import { fanee } from "@fanee/vite";
|
|
31
|
+
|
|
32
|
+
export default defineConfig({
|
|
33
|
+
plugins: [
|
|
34
|
+
fanee({
|
|
35
|
+
bundlePath: "./i18n",
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// main.tsx
|
|
43
|
+
import { resources } from "virtual:fanee";
|
|
44
|
+
import { FaneeRuntime } from "@fanee/core";
|
|
45
|
+
|
|
46
|
+
const runtime = new FaneeRuntime().config({
|
|
47
|
+
defaultLocale: "en",
|
|
48
|
+
currentLocale: "en",
|
|
49
|
+
resources,
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Options
|
|
54
|
+
|
|
55
|
+
| Option | Type | Default | Description |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `bundlePath` | `string` | — | Path to the OTB bundle directory. |
|
|
58
|
+
| `virtualId` | `string` | `"virtual:fanee"` | Virtual module ID to expose. |
|
|
59
|
+
|
|
60
|
+
## HMR
|
|
61
|
+
|
|
62
|
+
During development, editing `manifest.json`, `messages/*.json`, or `modules/**` files triggers HMR for the virtual module.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","names":["BundleResources","resources"],"sources":["../src/client.d.ts"],"mappings":";;gBACeA,eAAAA;EAAAA,aACDC,SAAAA,EAAW,eAAe;AAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
import { BundleResources, LocaleMessages, OTBManifest } from "@fanee/core";
|
|
3
|
+
|
|
4
|
+
//#region src/plugin.d.ts
|
|
5
|
+
interface FaneePluginOptions {
|
|
6
|
+
/** Path to the OTB bundle directory. */
|
|
7
|
+
bundlePath: string;
|
|
8
|
+
/** Virtual module ID used for importing resources. Defaults to `"virtual:fanee"`. */
|
|
9
|
+
virtualId?: string;
|
|
10
|
+
}
|
|
11
|
+
declare function fanee(options: FaneePluginOptions): Plugin;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/scanner.d.ts
|
|
14
|
+
declare function loadManifest(bundlePath: string): Promise<OTBManifest>;
|
|
15
|
+
declare function loadMessagesDir(dir: string, target: Record<string, LocaleMessages>): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Scan an OTB bundle directory and produce merged BundleResources.
|
|
18
|
+
*
|
|
19
|
+
* Implements the OTB Bundle-phase merge algorithm independently:
|
|
20
|
+
* descendant modules override ancestor modules, and `standalone: true`
|
|
21
|
+
* modules do not inherit ancestor data.
|
|
22
|
+
*/
|
|
23
|
+
declare function scanBundle(bundlePath: string): Promise<BundleResources>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { type FaneePluginOptions, fanee, loadManifest, loadMessagesDir, scanBundle };
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugin.ts","../src/scanner.ts"],"mappings":";;;;UAIiB,kBAAA;;EAEhB,UAAA;EAFgB;EAIhB,SAAS;AAAA;AAAA,iBAKM,KAAA,CAAM,OAAA,EAAS,kBAAA,GAAqB,MAAM;;;iBCTpC,YAAA,CAAa,UAAA,WAAqB,OAAO,CAAC,WAAA;AAAA,iBAW1C,eAAA,CACrB,GAAA,UACA,MAAA,EAAQ,MAAA,SAAe,cAAA,IACrB,OAAA;;ADdH;;;;AAIU;AAKV;iBCoHsB,UAAA,CAAW,UAAA,WAAqB,OAAO,CAAC,eAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{join as e,relative as t,resolve as n}from"node:path";import{readFile as r,readdir as i,stat as a}from"node:fs/promises";async function o(t){let n=await r(e(t,`manifest.json`),`utf-8`),i=JSON.parse(n);if(i.format!==`otb`)throw Error(`[fanee] Invalid bundle format: expected "otb", got "${i.format}"`);return i}async function s(t,n){let a;try{a=await i(t)}catch{return}for(let i of a){if(!i.endsWith(`.json`))continue;let a=i.replace(/\.json$/,``),o=await r(e(t,i),`utf-8`),s=JSON.parse(o);n[a]={...n[a]??{},...s}}}async function c(t,n,o){let s;try{s=await i(t)}catch{return}for(let i of s){let s=e(t,i);if(!(await a(s)).isDirectory())continue;let l=e(s,`manifest.json`),u=null;try{let e=await r(l,`utf-8`);u=JSON.parse(e)}catch{await c(s,n,o);continue}let d=n?`${n}:${i}`:i;o.set(d,{path:s,standalone:u?.standalone??!1}),await c(s,d,o)}}async function l(t,n){let r={},i=e(t,`messages`);r[``]={},await s(i,r[``]);for(let[t,a]of n){if(t===``)continue;let o=t.split(`:`),c={};await s(i,c);let l=!1;for(let t=0;t<o.length;t++){let r=o.slice(0,t+1).join(`:`),i=n.get(r);i&&(i.standalone&&(l=!0),l||await s(e(i.path,`messages`),c))}if(a.standalone){let n={};await s(e(a.path,`messages`),n),r[t]=n}else r[t]=c}return r}async function u(t){let r=n(t);await o(r);let i=new Map;return i.set(``,{path:r,standalone:!1}),await c(e(r,`modules`),``,i),l(r,i)}function d(e){let r=e.virtualId??`virtual:fanee`,i=`${r}?fanee`,a=n(e.bundlePath);async function o(){let e=await u(a);return`export const resources = ${JSON.stringify(e)};\n`}return{name:`fanee`,resolveId(e){return e===r?i:null},async load(e){return e===i?o():null},async handleHotUpdate({file:e,server:n}){let r=t(a,e);if(r.startsWith(`..`)||r.startsWith(`node_modules`))return;let o=n.moduleGraph.getModuleById(i);if(o)return[o]}}}export{d as fanee,o as loadManifest,s as loadMessagesDir,u as scanBundle};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/scanner.ts","../src/plugin.ts"],"sourcesContent":["import { readFile, readdir, stat } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport type { BundleResources, LocaleMessages, Namespace, OTBManifest } from \"@fanee/core\";\n\nexport async function loadManifest(bundlePath: string): Promise<OTBManifest> {\n\tconst content = await readFile(join(bundlePath, \"manifest.json\"), \"utf-8\");\n\tconst manifest = JSON.parse(content) as OTBManifest;\n\n\tif (manifest.format !== \"otb\") {\n\t\tthrow new Error(`[fanee] Invalid bundle format: expected \"otb\", got \"${manifest.format}\"`);\n\t}\n\n\treturn manifest;\n}\n\nexport async function loadMessagesDir(\n\tdir: string,\n\ttarget: Record<string, LocaleMessages>\n): Promise<void> {\n\tlet entries: string[];\n\ttry {\n\t\tentries = await readdir(dir);\n\t} catch {\n\t\treturn;\n\t}\n\n\tfor (const entry of entries) {\n\t\tif (!entry.endsWith(\".json\")) continue;\n\n\t\tconst locale = entry.replace(/\\.json$/, \"\");\n\t\tconst content = await readFile(join(dir, entry), \"utf-8\");\n\t\tconst data = JSON.parse(content) as LocaleMessages;\n\n\t\ttarget[locale] = { ...(target[locale] ?? {}), ...data };\n\t}\n}\n\ntype ModuleInfo = { path: string; standalone: boolean };\n\nasync function collectModules(\n\tmodulesDir: string,\n\tparentNamespace: Namespace,\n\tmodules: Map<Namespace, ModuleInfo>\n): Promise<void> {\n\tlet entries: string[];\n\ttry {\n\t\tentries = await readdir(modulesDir);\n\t} catch {\n\t\treturn;\n\t}\n\n\tfor (const entry of entries) {\n\t\tconst fullPath = join(modulesDir, entry);\n\t\tconst entryStat = await stat(fullPath);\n\t\tif (!entryStat.isDirectory()) continue;\n\n\t\tconst manifestPath = join(fullPath, \"manifest.json\");\n\t\tlet manifest: { standalone?: boolean } | null = null;\n\t\ttry {\n\t\t\tconst content = await readFile(manifestPath, \"utf-8\");\n\t\t\tmanifest = JSON.parse(content) as { standalone?: boolean };\n\t\t} catch {\n\t\t\tawait collectModules(fullPath, parentNamespace, modules);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst namespace: Namespace = parentNamespace\n\t\t\t? (`${parentNamespace}:${entry}` as Namespace)\n\t\t\t: (entry as Namespace);\n\n\t\tmodules.set(namespace, {\n\t\t\tpath: fullPath,\n\t\t\tstandalone: manifest?.standalone ?? false,\n\t\t});\n\n\t\tawait collectModules(fullPath, namespace, modules);\n\t}\n}\n\nasync function buildResources(\n\tbundlePath: string,\n\tmodules: Map<Namespace, ModuleInfo>\n): Promise<BundleResources> {\n\tconst resources: BundleResources = {};\n\tconst rootMessagesDir = join(bundlePath, \"messages\");\n\n\tresources[\"\"] = {};\n\tawait loadMessagesDir(rootMessagesDir, resources[\"\"]);\n\n\tfor (const [namespace, mod] of modules) {\n\t\tif (namespace === \"\") continue;\n\n\t\tconst parts = namespace.split(\":\");\n\t\tconst merged: Record<string, Record<string, string>> = {};\n\t\tawait loadMessagesDir(rootMessagesDir, merged);\n\n\t\tlet hitStandalone = false;\n\t\tfor (let i = 0; i < parts.length; i++) {\n\t\t\tconst segmentNs = parts.slice(0, i + 1).join(\":\") as Namespace;\n\t\t\tconst modInfo = modules.get(segmentNs);\n\t\t\tif (!modInfo) continue;\n\n\t\t\tif (modInfo.standalone) {\n\t\t\t\thitStandalone = true;\n\t\t\t}\n\t\t\tif (!hitStandalone) {\n\t\t\t\tawait loadMessagesDir(join(modInfo.path, \"messages\"), merged);\n\t\t\t}\n\t\t}\n\n\t\tif (mod.standalone) {\n\t\t\tconst standalone: Record<string, Record<string, string>> = {};\n\t\t\tawait loadMessagesDir(join(mod.path, \"messages\"), standalone);\n\t\t\tresources[namespace] = standalone;\n\t\t} else {\n\t\t\tresources[namespace] = merged;\n\t\t}\n\t}\n\n\treturn resources;\n}\n\n/**\n * Scan an OTB bundle directory and produce merged BundleResources.\n *\n * Implements the OTB Bundle-phase merge algorithm independently:\n * descendant modules override ancestor modules, and `standalone: true`\n * modules do not inherit ancestor data.\n */\nexport async function scanBundle(bundlePath: string): Promise<BundleResources> {\n\tconst resolvedPath = resolve(bundlePath);\n\tawait loadManifest(resolvedPath);\n\n\tconst modules = new Map<Namespace, ModuleInfo>();\n\tmodules.set(\"\", {\n\t\tpath: resolvedPath,\n\t\tstandalone: false,\n\t});\n\n\tawait collectModules(join(resolvedPath, \"modules\"), \"\", modules);\n\treturn buildResources(resolvedPath, modules);\n}\n","import type { Plugin } from \"vite\";\nimport { resolve, relative } from \"node:path\";\nimport { scanBundle } from \"./scanner\";\n\nexport interface FaneePluginOptions {\n\t/** Path to the OTB bundle directory. */\n\tbundlePath: string;\n\t/** Virtual module ID used for importing resources. Defaults to `\"virtual:fanee\"`. */\n\tvirtualId?: string;\n}\n\nconst DEFAULT_VIRTUAL_ID = \"virtual:fanee\";\n\nexport function fanee(options: FaneePluginOptions): Plugin {\n\tconst virtualId = options.virtualId ?? DEFAULT_VIRTUAL_ID;\n\tconst resolvedId = `${virtualId}?fanee`;\n\tconst bundlePath = resolve(options.bundlePath);\n\n\tasync function generateModule(): Promise<string> {\n\t\tconst resources = await scanBundle(bundlePath);\n\t\treturn `export const resources = ${JSON.stringify(resources)};\\n`;\n\t}\n\n\treturn {\n\t\tname: \"fanee\",\n\t\tresolveId(id) {\n\t\t\tif (id === virtualId) {\n\t\t\t\treturn resolvedId;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t\tasync load(id) {\n\t\t\tif (id !== resolvedId) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn generateModule();\n\t\t},\n\t\tasync handleHotUpdate({ file, server }) {\n\t\t\tconst rel = relative(bundlePath, file);\n\t\t\tif (rel.startsWith(\"..\") || rel.startsWith(\"node_modules\")) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst moduleNode = server.moduleGraph.getModuleById(resolvedId);\n\t\t\tif (moduleNode) {\n\t\t\t\treturn [moduleNode];\n\t\t\t}\n\t\t},\n\t};\n}\n"],"mappings":"+HAIA,eAAsB,EAAa,EAA0C,CAC5E,IAAM,EAAU,MAAM,EAAS,EAAK,EAAY,eAAe,EAAG,OAAO,EACnE,EAAW,KAAK,MAAM,CAAO,EAEnC,GAAI,EAAS,SAAW,MACvB,MAAU,MAAM,uDAAuD,EAAS,OAAO,EAAE,EAG1F,OAAO,CACR,CAEA,eAAsB,EACrB,EACA,EACgB,CAChB,IAAI,EACJ,GAAI,CACH,EAAU,MAAM,EAAQ,CAAG,CAC5B,MAAQ,CACP,MACD,CAEA,IAAK,IAAM,KAAS,EAAS,CAC5B,GAAI,CAAC,EAAM,SAAS,OAAO,EAAG,SAE9B,IAAM,EAAS,EAAM,QAAQ,UAAW,EAAE,EACpC,EAAU,MAAM,EAAS,EAAK,EAAK,CAAK,EAAG,OAAO,EAClD,EAAO,KAAK,MAAM,CAAO,EAE/B,EAAO,GAAU,CAAE,GAAI,EAAO,IAAW,CAAC,EAAI,GAAG,CAAK,CACvD,CACD,CAIA,eAAe,EACd,EACA,EACA,EACgB,CAChB,IAAI,EACJ,GAAI,CACH,EAAU,MAAM,EAAQ,CAAU,CACnC,MAAQ,CACP,MACD,CAEA,IAAK,IAAM,KAAS,EAAS,CAC5B,IAAM,EAAW,EAAK,EAAY,CAAK,EAEvC,GAAI,EAAC,MADmB,EAAK,CAAQ,EAAA,CACtB,YAAY,EAAG,SAE9B,IAAM,EAAe,EAAK,EAAU,eAAe,EAC/C,EAA4C,KAChD,GAAI,CACH,IAAM,EAAU,MAAM,EAAS,EAAc,OAAO,EACpD,EAAW,KAAK,MAAM,CAAO,CAC9B,MAAQ,CACP,MAAM,EAAe,EAAU,EAAiB,CAAO,EACvD,QACD,CAEA,IAAM,EAAuB,EACzB,GAAG,EAAgB,GAAG,IACtB,EAEJ,EAAQ,IAAI,EAAW,CACtB,KAAM,EACN,WAAY,GAAU,YAAc,EACrC,CAAC,EAED,MAAM,EAAe,EAAU,EAAW,CAAO,CAClD,CACD,CAEA,eAAe,EACd,EACA,EAC2B,CAC3B,IAAM,EAA6B,CAAC,EAC9B,EAAkB,EAAK,EAAY,UAAU,EAEnD,EAAU,IAAM,CAAC,EACjB,MAAM,EAAgB,EAAiB,EAAU,GAAG,EAEpD,IAAK,GAAM,CAAC,EAAW,KAAQ,EAAS,CACvC,GAAI,IAAc,GAAI,SAEtB,IAAM,EAAQ,EAAU,MAAM,GAAG,EAC3B,EAAiD,CAAC,EACxD,MAAM,EAAgB,EAAiB,CAAM,EAE7C,IAAI,EAAgB,GACpB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAM,OAAQ,IAAK,CACtC,IAAM,EAAY,EAAM,MAAM,EAAG,EAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAC1C,EAAU,EAAQ,IAAI,CAAS,EAChC,IAED,EAAQ,aACX,EAAgB,IAEZ,GACJ,MAAM,EAAgB,EAAK,EAAQ,KAAM,UAAU,EAAG,CAAM,EAE9D,CAEA,GAAI,EAAI,WAAY,CACnB,IAAM,EAAqD,CAAC,EAC5D,MAAM,EAAgB,EAAK,EAAI,KAAM,UAAU,EAAG,CAAU,EAC5D,EAAU,GAAa,CACxB,KACC,GAAU,GAAa,CAEzB,CAEA,OAAO,CACR,CASA,eAAsB,EAAW,EAA8C,CAC9E,IAAM,EAAe,EAAQ,CAAU,EACvC,MAAM,EAAa,CAAY,EAE/B,IAAM,EAAU,IAAI,IAOpB,OANA,EAAQ,IAAI,GAAI,CACf,KAAM,EACN,WAAY,EACb,CAAC,EAED,MAAM,EAAe,EAAK,EAAc,SAAS,EAAG,GAAI,CAAO,EACxD,EAAe,EAAc,CAAO,CAC5C,CChIA,SAAgB,EAAM,EAAqC,CAC1D,IAAM,EAAY,EAAQ,WAAa,gBACjC,EAAa,GAAG,EAAU,QAC1B,EAAa,EAAQ,EAAQ,UAAU,EAE7C,eAAe,GAAkC,CAChD,IAAM,EAAY,MAAM,EAAW,CAAU,EAC7C,MAAO,4BAA4B,KAAK,UAAU,CAAS,EAAE,IAC9D,CAEA,MAAO,CACN,KAAM,QACN,UAAU,EAAI,CAIb,OAHI,IAAO,EACH,EAED,IACR,EACA,MAAM,KAAK,EAAI,CAId,OAHI,IAAO,EAGJ,EAAe,EAFd,IAGT,EACA,MAAM,gBAAgB,CAAE,OAAM,UAAU,CACvC,IAAM,EAAM,EAAS,EAAY,CAAI,EACrC,GAAI,EAAI,WAAW,IAAI,GAAK,EAAI,WAAW,cAAc,EACxD,OAGD,IAAM,EAAa,EAAO,YAAY,cAAc,CAAU,EAC9D,GAAI,EACH,MAAO,CAAC,CAAU,CAEpB,CACD,CACD"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fanee/vite",
|
|
3
|
+
"exports": {
|
|
4
|
+
".": {
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"default": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"./client": {
|
|
9
|
+
"types": "./dist/client.d.ts"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"version": "0.6.0",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"private": false,
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"url": "https://github.com/project-cvsa/fanee"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "bunx --bun tsdown",
|
|
21
|
+
"lint": "bunx --bun biome lint",
|
|
22
|
+
"test": "bun test",
|
|
23
|
+
"test:coverage": "bun test --coverage",
|
|
24
|
+
"typecheck": "bunx --bun tsgo --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"vite": ">=5"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@fanee/core": "workspace:*"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/bun": "latest",
|
|
34
|
+
"@types/node": "^22.0.0",
|
|
35
|
+
"tsdown": "^0.22.2",
|
|
36
|
+
"vite": "^8.0.12"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist/"
|
|
40
|
+
]
|
|
41
|
+
}
|