@treenity/react 3.0.2 → 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.
@@ -0,0 +1,15 @@
1
+ import { type ReactNode } from 'react';
2
+ import './load-client';
3
+ import './root.css';
4
+ export interface TreenityProps {
5
+ /** Override initial path */
6
+ path?: string;
7
+ /** Wrap with custom providers */
8
+ children?: ReactNode;
9
+ }
10
+ /**
11
+ * Treenity as a single React component.
12
+ * Includes all providers, auth, SSE, cache.
13
+ */
14
+ export declare function Treenity({ children }: TreenityProps): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=Treenity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Treenity.d.ts","sourceRoot":"","sources":["../src/Treenity.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,eAAe,CAAA;AAEtB,OAAO,YAAY,CAAA;AAMnB,MAAM,WAAW,aAAa;IAC5B,4BAA4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iCAAiC;IACjC,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,EAAE,aAAa,2CAQnD"}
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
+ import { enablePatches } from 'immer';
4
+ import { App } from './App';
5
+ import './load-client';
6
+ import { Toaster } from './components/ui/sonner';
7
+ import './root.css';
8
+ enablePatches();
9
+ const queryClient = new QueryClient();
10
+ /**
11
+ * Treenity as a single React component.
12
+ * Includes all providers, auth, SSE, cache.
13
+ */
14
+ export function Treenity({ children }) {
15
+ return (_jsxs(QueryClientProvider, { client: queryClient, children: [_jsx(App, {}), _jsx(Toaster, {}), children] }));
16
+ }
17
+ //# sourceMappingURL=Treenity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Treenity.js","sourceRoot":"","sources":["../src/Treenity.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAErC,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAC3B,OAAO,eAAe,CAAA;AACtB,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAChD,OAAO,YAAY,CAAA;AAEnB,aAAa,EAAE,CAAA;AAEf,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAA;AASrC;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAE,QAAQ,EAAiB;IAClD,OAAO,CACL,MAAC,mBAAmB,IAAC,MAAM,EAAE,WAAW,aACtC,KAAC,GAAG,KAAG,EACP,KAAC,OAAO,KAAG,EACV,QAAQ,IACW,CACvB,CAAA;AACH,CAAC"}
package/dist/main.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import 'reflect-metadata';
2
2
  import './load-client';
3
3
  import './root.css';
4
+ /** Mount Treenity UI into a DOM element */
5
+ export declare function boot(el?: HTMLElement | string): void;
4
6
  //# sourceMappingURL=main.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.tsx"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAM1B,OAAO,eAAe,CAAC;AAEvB,OAAO,YAAY,CAAC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.tsx"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAM1B,OAAO,eAAe,CAAC;AAEvB,OAAO,YAAY,CAAC;AAUpB,2CAA2C;AAC3C,wBAAgB,IAAI,CAAC,EAAE,GAAE,WAAW,GAAG,MAAgB,QAWtD"}
package/dist/main.js CHANGED
@@ -11,13 +11,16 @@ enablePatches();
11
11
  const queryClient = new QueryClient();
12
12
  // StrictMode off: FlowGram inversify container breaks on double-mount
13
13
  // https://github.com/bytedance/flowgram.ai/issues/402
14
- // TODO: re-enable once FlowGram fixes React 19 StrictMode support
15
- // const Strict = import.meta.env.VITE_STRICT_MODE !== 'false'
16
- // ? StrictMode
17
- // : ({ children }: { children: ReactNode }) => children;
18
14
  const Strict = ({ children }) => children;
19
- const root = document.getElementById('root');
20
- if (!root)
21
- throw new Error('No #root element');
22
- createRoot(root).render(_jsx(Strict, { children: _jsxs(QueryClientProvider, { client: queryClient, children: [_jsx(App, {}), _jsx(Toaster, {})] }) }));
15
+ /** Mount Treenity UI into a DOM element */
16
+ export function boot(el = '#root') {
17
+ const root = typeof el === 'string' ? document.querySelector(el) : el;
18
+ if (!root)
19
+ throw new Error(`Treenity boot: element "${el}" not found`);
20
+ createRoot(root).render(_jsx(Strict, { children: _jsxs(QueryClientProvider, { client: queryClient, children: [_jsx(App, {}), _jsx(Toaster, {})] }) }));
21
+ }
22
+ // Auto-boot when loaded directly (not imported)
23
+ if (typeof document !== 'undefined' && document.getElementById('root')) {
24
+ boot('#root');
25
+ }
23
26
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.tsx"],"names":[],"mappings":";AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEtC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,YAAY,CAAC;AAEpB,aAAa,EAAE,CAAC;AAEhB,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;AAEtC,sEAAsE;AACtE,sDAAsD;AACtD,kEAAkE;AAClE,8DAA8D;AAC9D,iBAAiB;AACjB,2DAA2D;AAC3D,MAAM,MAAM,GAAG,CAAC,EAAE,QAAQ,EAA2B,EAAE,EAAE,CAAC,QAAQ,CAAC;AAEnE,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;AAC7C,IAAI,CAAC,IAAI;IAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;AAC/C,UAAU,CAAC,IAAI,CAAC,CAAC,MAAM,CACrB,KAAC,MAAM,cACL,MAAC,mBAAmB,IAAC,MAAM,EAAE,WAAW,aACtC,KAAC,GAAG,KAAG,EACP,KAAC,OAAO,KAAG,IACS,GACf,CACV,CAAC"}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.tsx"],"names":[],"mappings":";AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEtC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,YAAY,CAAC;AAEpB,aAAa,EAAE,CAAC;AAEhB,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;AAEtC,sEAAsE;AACtE,sDAAsD;AACtD,MAAM,MAAM,GAAG,CAAC,EAAE,QAAQ,EAA2B,EAAE,EAAE,CAAC,QAAQ,CAAC;AAEnE,2CAA2C;AAC3C,MAAM,UAAU,IAAI,CAAC,KAA2B,OAAO;IACrD,MAAM,IAAI,GAAG,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACtE,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,EAAE,aAAa,CAAC,CAAC;IACvE,UAAU,CAAC,IAAmB,CAAC,CAAC,MAAM,CACpC,KAAC,MAAM,cACL,MAAC,mBAAmB,IAAC,MAAM,EAAE,WAAW,aACtC,KAAC,GAAG,KAAG,EACP,KAAC,OAAO,KAAG,IACS,GACf,CACV,CAAC;AACJ,CAAC;AAED,gDAAgD;AAChD,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;IACvE,IAAI,CAAC,OAAO,CAAC,CAAC;AAChB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treenity/react",
3
- "version": "3.0.2",
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,32 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
+ import { enablePatches } from 'immer'
3
+ import { type ReactNode } from 'react'
4
+ import { App } from './App'
5
+ import './load-client'
6
+ import { Toaster } from './components/ui/sonner'
7
+ import './root.css'
8
+
9
+ enablePatches()
10
+
11
+ const queryClient = new QueryClient()
12
+
13
+ export interface TreenityProps {
14
+ /** Override initial path */
15
+ path?: string
16
+ /** Wrap with custom providers */
17
+ children?: ReactNode
18
+ }
19
+
20
+ /**
21
+ * Treenity as a single React component.
22
+ * Includes all providers, auth, SSE, cache.
23
+ */
24
+ export function Treenity({ children }: TreenityProps) {
25
+ return (
26
+ <QueryClientProvider client={queryClient}>
27
+ <App />
28
+ <Toaster />
29
+ {children}
30
+ </QueryClientProvider>
31
+ )
32
+ }
package/src/main.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import 'reflect-metadata';
2
2
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
3
  import { enablePatches } from 'immer';
4
- import { StrictMode, type ReactNode } from 'react';
4
+ import { type ReactNode } from 'react';
5
5
  import { createRoot } from 'react-dom/client';
6
6
  import { App } from './App';
7
7
  import './load-client';
@@ -14,19 +14,23 @@ const queryClient = new QueryClient();
14
14
 
15
15
  // StrictMode off: FlowGram inversify container breaks on double-mount
16
16
  // https://github.com/bytedance/flowgram.ai/issues/402
17
- // TODO: re-enable once FlowGram fixes React 19 StrictMode support
18
- // const Strict = import.meta.env.VITE_STRICT_MODE !== 'false'
19
- // ? StrictMode
20
- // : ({ children }: { children: ReactNode }) => children;
21
17
  const Strict = ({ children }: { children: ReactNode }) => children;
22
18
 
23
- const root = document.getElementById('root');
24
- if (!root) throw new Error('No #root element');
25
- createRoot(root).render(
26
- <Strict>
27
- <QueryClientProvider client={queryClient}>
28
- <App />
29
- <Toaster />
30
- </QueryClientProvider>
31
- </Strict>,
32
- );
19
+ /** Mount Treenity UI into a DOM element */
20
+ export function boot(el: HTMLElement | string = '#root') {
21
+ const root = typeof el === 'string' ? document.querySelector(el) : el;
22
+ if (!root) throw new Error(`Treenity boot: element "${el}" not found`);
23
+ createRoot(root as HTMLElement).render(
24
+ <Strict>
25
+ <QueryClientProvider client={queryClient}>
26
+ <App />
27
+ <Toaster />
28
+ </QueryClientProvider>
29
+ </Strict>,
30
+ );
31
+ }
32
+
33
+ // Auto-boot when loaded directly (not imported)
34
+ if (typeof document !== 'undefined' && document.getElementById('root')) {
35
+ boot('#root');
36
+ }
@@ -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
+ }