@treenity/react 3.0.3 → 3.0.4
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/package.json +3 -1
- package/vite-plugin-treenity.ts +230 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treenity/react",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"description": "React binding for Treenity — reactive hooks, admin UI, and context-based component rendering.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"files": [
|
|
74
74
|
"dist",
|
|
75
75
|
"src",
|
|
76
|
+
"vite-plugin-treenity.ts",
|
|
76
77
|
"!src/**/*.test.ts",
|
|
77
78
|
"!src/**/*.test.tsx",
|
|
78
79
|
"LICENSE",
|
|
@@ -106,6 +107,7 @@
|
|
|
106
107
|
"@tiptap/react": "^3.20.0",
|
|
107
108
|
"@tiptap/starter-kit": "^3.20.0",
|
|
108
109
|
"@tiptap/suggestion": "^3.20.0",
|
|
110
|
+
"@treenity/react": "^3.0.3",
|
|
109
111
|
"@trpc/client": "^11.9.0",
|
|
110
112
|
"class-variance-authority": "^0.7.1",
|
|
111
113
|
"clsx": "^2.1.1",
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Treenity vite plugin:
|
|
2
|
+
// 1. Resolve #subpath imports via nearest package.json (Vite doesn't support them)
|
|
3
|
+
// 2. Resolve @treenity/* exports with array conditions (Vite bug #16153)
|
|
4
|
+
// 3. Auto-discover mod client.ts → virtual:mod-clients
|
|
5
|
+
// 4. Block server.ts from frontend bundle
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
8
|
+
import { dirname, join, resolve } from 'node:path';
|
|
9
|
+
import type { Plugin } from 'vite';
|
|
10
|
+
|
|
11
|
+
// ── Package.json resolution ──
|
|
12
|
+
|
|
13
|
+
type SpecValue = string | string[] | Record<string, string | string[]>;
|
|
14
|
+
type FieldMap = Record<string, SpecValue>;
|
|
15
|
+
|
|
16
|
+
const pkgCache = new Map<string, { dir: string; imports?: FieldMap; exports?: FieldMap } | null>();
|
|
17
|
+
|
|
18
|
+
function readPkg(startDir: string) {
|
|
19
|
+
if (pkgCache.has(startDir)) return pkgCache.get(startDir)!;
|
|
20
|
+
|
|
21
|
+
let current = startDir;
|
|
22
|
+
while (current !== dirname(current)) {
|
|
23
|
+
const pkgPath = join(current, 'package.json');
|
|
24
|
+
if (existsSync(pkgPath)) {
|
|
25
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
26
|
+
if (pkg.imports || pkg.exports) {
|
|
27
|
+
const result = { dir: current, imports: pkg.imports as FieldMap, exports: pkg.exports as FieldMap };
|
|
28
|
+
pkgCache.set(startDir, result);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
current = dirname(current);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pkgCache.set(startDir, null);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Cache for @treenity/* package dirs
|
|
40
|
+
const treenityPkgCache = new Map<string, { dir: string; exports: FieldMap } | null>();
|
|
41
|
+
|
|
42
|
+
function findTreenityPkg(name: string): { dir: string; exports: FieldMap } | null {
|
|
43
|
+
if (treenityPkgCache.has(name)) return treenityPkgCache.get(name)!;
|
|
44
|
+
|
|
45
|
+
// Walk up from CWD to find node_modules/@treenity/<name>
|
|
46
|
+
let current = process.cwd();
|
|
47
|
+
while (current !== dirname(current)) {
|
|
48
|
+
const pkgDir = join(current, 'node_modules', name);
|
|
49
|
+
const pkgPath = join(pkgDir, 'package.json');
|
|
50
|
+
if (existsSync(pkgPath)) {
|
|
51
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
52
|
+
// Follow symlink to real path for resolution
|
|
53
|
+
const realDir = realpathSync(pkgDir);
|
|
54
|
+
const result = pkg.exports ? { dir: realDir, exports: pkg.exports as FieldMap } : null;
|
|
55
|
+
treenityPkgCache.set(name, result);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
current = dirname(current);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
treenityPkgCache.set(name, null);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveConditions(pkgDir: string, spec: SpecValue, conditions: string[]): string[] {
|
|
66
|
+
if (typeof spec === 'string') return [resolve(pkgDir, spec)];
|
|
67
|
+
if (Array.isArray(spec)) return spec.map(s => resolve(pkgDir, s));
|
|
68
|
+
|
|
69
|
+
for (const cond of conditions) {
|
|
70
|
+
if (spec[cond]) return resolveConditions(pkgDir, spec[cond], conditions);
|
|
71
|
+
}
|
|
72
|
+
if (spec.default) return resolveConditions(pkgDir, spec.default, conditions);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isFile(p: string): boolean {
|
|
77
|
+
return existsSync(p) && statSync(p).isFile();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const EXT = ['.ts', '.tsx', '.js', '.jsx'];
|
|
81
|
+
const IDX = ['/index.ts', '/index.tsx', '/index.js', '/index.jsx'];
|
|
82
|
+
|
|
83
|
+
function tryResolve(candidates: string[]): string | undefined {
|
|
84
|
+
for (const c of candidates) {
|
|
85
|
+
if (isFile(c)) return c;
|
|
86
|
+
for (const ext of EXT) { if (isFile(c + ext)) return c + ext; }
|
|
87
|
+
for (const idx of IDX) { if (isFile(c + idx)) return c + idx; }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function expandWildcard(spec: SpecValue, matched: string): SpecValue {
|
|
92
|
+
if (typeof spec === 'string') return spec.replace('*', matched);
|
|
93
|
+
if (Array.isArray(spec)) return spec.map(s => s.replace('*', matched));
|
|
94
|
+
return Object.fromEntries(
|
|
95
|
+
Object.entries(spec).map(([k, v]) => [k, expandWildcard(v, matched)])
|
|
96
|
+
) as SpecValue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function matchPattern(id: string, map: FieldMap, pkgDir: string, conditions: string[]): string | undefined {
|
|
100
|
+
for (const [pattern, spec] of Object.entries(map)) {
|
|
101
|
+
if (pattern === id) {
|
|
102
|
+
return tryResolve(resolveConditions(pkgDir, spec, conditions));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (pattern.includes('*')) {
|
|
106
|
+
const [prefix, suffix] = pattern.split('*');
|
|
107
|
+
if (id.startsWith(prefix) && (!suffix || id.endsWith(suffix))) {
|
|
108
|
+
const matched = id.slice(prefix.length, suffix ? -suffix.length || undefined : undefined);
|
|
109
|
+
return tryResolve(resolveConditions(pkgDir, expandWildcard(spec, matched), conditions));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Mod discovery ──
|
|
116
|
+
|
|
117
|
+
const VIRTUAL_ID = 'virtual:mod-clients';
|
|
118
|
+
const RESOLVED_ID = '\0' + VIRTUAL_ID;
|
|
119
|
+
const SERVER_RE = /\/mods\/[^/]+\/server(\.ts)?$/;
|
|
120
|
+
|
|
121
|
+
function scanClients(dir: string): string[] {
|
|
122
|
+
if (!existsSync(dir)) return [];
|
|
123
|
+
const clients: string[] = [];
|
|
124
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
125
|
+
if (!entry.isDirectory()) continue;
|
|
126
|
+
const client = resolve(dir, entry.name, 'client.ts');
|
|
127
|
+
if (existsSync(client)) clients.push(client);
|
|
128
|
+
}
|
|
129
|
+
return clients;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Scan node_modules for @treenity/* packages with treenity.clients field
|
|
133
|
+
function discoverPackageClients(): string[] {
|
|
134
|
+
const imports: string[] = [];
|
|
135
|
+
let current = process.cwd();
|
|
136
|
+
|
|
137
|
+
while (current !== dirname(current)) {
|
|
138
|
+
const nmDir = join(current, 'node_modules', '@treenity');
|
|
139
|
+
if (existsSync(nmDir)) {
|
|
140
|
+
for (const entry of readdirSync(nmDir, { withFileTypes: true })) {
|
|
141
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
142
|
+
const pkgPath = join(nmDir, entry.name, 'package.json');
|
|
143
|
+
if (!existsSync(pkgPath)) continue;
|
|
144
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
145
|
+
if (pkg.treenity?.clients) {
|
|
146
|
+
const realDir = realpathSync(join(nmDir, entry.name));
|
|
147
|
+
const clientsPath = resolve(realDir, pkg.treenity.clients);
|
|
148
|
+
if (existsSync(clientsPath)) imports.push(clientsPath);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
break; // found node_modules, stop walking up
|
|
152
|
+
}
|
|
153
|
+
current = dirname(current);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return imports;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Plugin ──
|
|
160
|
+
|
|
161
|
+
export default function treenityPlugin(opts?: { modsDirs?: string[] }): Plugin {
|
|
162
|
+
const engineRoot = resolve(import.meta.dirname, '../..');
|
|
163
|
+
let conditions: string[] = [];
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
name: 'treenity',
|
|
167
|
+
enforce: 'pre',
|
|
168
|
+
|
|
169
|
+
configResolved(config) {
|
|
170
|
+
conditions = (config.resolve.conditions ?? []).concat('default');
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
resolveId(id, importer) {
|
|
174
|
+
if (id === VIRTUAL_ID) return RESOLVED_ID;
|
|
175
|
+
if (!importer) return;
|
|
176
|
+
|
|
177
|
+
// Block server.ts from frontend
|
|
178
|
+
if (id.startsWith('.')) {
|
|
179
|
+
const resolved = resolve(importer, '..', id).replace(/\\/g, '/');
|
|
180
|
+
if (SERVER_RE.test(resolved)) {
|
|
181
|
+
this.error(
|
|
182
|
+
`Server module imported in frontend build: "${id}"\n` +
|
|
183
|
+
` from: ${importer}\n` +
|
|
184
|
+
` Mods must not import server.ts from client code`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Resolve # imports via nearest package.json imports field
|
|
190
|
+
if (id.startsWith('#')) {
|
|
191
|
+
const pkg = readPkg(dirname(importer));
|
|
192
|
+
if (pkg?.imports) return matchPattern(id, pkg.imports, pkg.dir, conditions);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Resolve @treenity/* exports (Vite doesn't handle array conditions)
|
|
196
|
+
if (id.startsWith('@treenity/')) {
|
|
197
|
+
const parts = id.split('/');
|
|
198
|
+
const pkgName = parts.slice(0, 2).join('/');
|
|
199
|
+
const subpath = './' + parts.slice(2).join('/');
|
|
200
|
+
const pkg = findTreenityPkg(pkgName);
|
|
201
|
+
if (pkg?.exports) {
|
|
202
|
+
return matchPattern(parts.length > 2 ? subpath : '.', pkg.exports, pkg.dir, conditions);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
load(id) {
|
|
208
|
+
if (id !== RESOLVED_ID) return;
|
|
209
|
+
|
|
210
|
+
// 1. Auto-discover @treenity/* packages with treenity.clients
|
|
211
|
+
const pkgClients = discoverPackageClients();
|
|
212
|
+
|
|
213
|
+
// 2. Engine mods (sibling to this plugin's package)
|
|
214
|
+
const engineMods = scanClients(resolve(engineRoot, 'mods'));
|
|
215
|
+
|
|
216
|
+
// 3. Extra mods dirs (passed explicitly from project vite config)
|
|
217
|
+
const extraMods = (opts?.modsDirs ?? []).flatMap(d => scanClients(resolve(d)));
|
|
218
|
+
|
|
219
|
+
// Dedupe by realpath
|
|
220
|
+
const seen = new Set<string>();
|
|
221
|
+
const imports: string[] = [];
|
|
222
|
+
for (const p of [...pkgClients, ...engineMods, ...extraMods]) {
|
|
223
|
+
const real = realpathSync(p);
|
|
224
|
+
if (!seen.has(real)) { seen.add(real); imports.push(p); }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return imports.map(p => `import '${p}';`).join('\n') + '\n';
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|