holosplat 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 +890 -0
- package/bin/holosplat.cjs +374 -0
- package/dist/holosplat.esm.js +766 -0
- package/dist/holosplat.esm.js.map +7 -0
- package/dist/holosplat.iife.js +766 -0
- package/dist/holosplat.iife.js.map +7 -0
- package/holosplat/editor.js +2947 -0
- package/holosplat/index.html +614 -0
- package/holosplat/stats.js +101 -0
- package/package.json +30 -0
- package/server.py +560 -0
- package/src/server.js +198 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const pkg = path.join(__dirname, '..'); // root of the installed package
|
|
8
|
+
const cwd = process.cwd(); // target project root
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const cmd = args[0];
|
|
11
|
+
const flags = new Set(args.slice(1));
|
|
12
|
+
|
|
13
|
+
// ── init ──────────────────────────────────────────────────────────────────────
|
|
14
|
+
if (cmd === 'init') {
|
|
15
|
+
const withServer = flags.has('--with-server');
|
|
16
|
+
const noServer = flags.has('--no-server');
|
|
17
|
+
|
|
18
|
+
// Detect project type from package.json
|
|
19
|
+
const projectPkg = readPkgJson(cwd);
|
|
20
|
+
const framework = detectFramework(projectPkg);
|
|
21
|
+
const hasOwnServer = noServer ? false
|
|
22
|
+
: withServer ? false
|
|
23
|
+
: (framework !== 'none');
|
|
24
|
+
|
|
25
|
+
// ── holosplat/ editor ──────────────────────────────────────────────────────
|
|
26
|
+
const editorDir = path.join(cwd, 'holosplat');
|
|
27
|
+
if (!fs.existsSync(editorDir)) fs.mkdirSync(editorDir, { recursive: true });
|
|
28
|
+
|
|
29
|
+
const html = fs.readFileSync(path.join(pkg, 'holosplat', 'index.html'), 'utf8')
|
|
30
|
+
.replace('../dist/holosplat.iife.js', './holosplat.iife.js');
|
|
31
|
+
fs.writeFileSync(path.join(editorDir, 'index.html'), html);
|
|
32
|
+
|
|
33
|
+
fs.copyFileSync(
|
|
34
|
+
path.join(pkg, 'dist', 'holosplat.iife.js'),
|
|
35
|
+
path.join(editorDir, 'holosplat.iife.js')
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// ── scenes/ ───────────────────────────────────────────────────────────────
|
|
39
|
+
const scenesDir = path.join(cwd, 'scenes');
|
|
40
|
+
if (!fs.existsSync(scenesDir)) {
|
|
41
|
+
fs.mkdirSync(scenesDir, { recursive: true });
|
|
42
|
+
fs.writeFileSync(path.join(scenesDir, '.gitkeep'), '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── .gitignore ────────────────────────────────────────────────────────────
|
|
46
|
+
const giPath = path.join(cwd, '.gitignore');
|
|
47
|
+
const giLine = 'holosplat/holosplat.iife.js';
|
|
48
|
+
if (fs.existsSync(giPath)) {
|
|
49
|
+
const gi = fs.readFileSync(giPath, 'utf8');
|
|
50
|
+
if (!gi.includes(giLine))
|
|
51
|
+
fs.appendFileSync(giPath, '\n# HoloSplat editor runtime\n' + giLine + '\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Server setup ──────────────────────────────────────────────────────────
|
|
55
|
+
if (!hasOwnServer) {
|
|
56
|
+
const dst = path.join(cwd, 'server.py');
|
|
57
|
+
if (!fs.existsSync(dst)) {
|
|
58
|
+
fs.copyFileSync(path.join(pkg, 'server.py'), dst);
|
|
59
|
+
console.log(' server.py ← python server.py to start');
|
|
60
|
+
} else {
|
|
61
|
+
console.log(' (server.py already exists — skipped)');
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
writeServerSnippet(editorDir, framework);
|
|
65
|
+
console.log(' holosplat/server-snippet.js ← add these routes to your server');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── CLAUDE.md ─────────────────────────────────────────────────────────────
|
|
69
|
+
writeClaudeMd(cwd, framework, hasOwnServer);
|
|
70
|
+
console.log(' CLAUDE.md ← HoloSplat section added/created');
|
|
71
|
+
|
|
72
|
+
// ── Done ──────────────────────────────────────────────────────────────────
|
|
73
|
+
console.log('\n HoloSplat initialised\n');
|
|
74
|
+
console.log(' holosplat/index.html ← art direction editor');
|
|
75
|
+
console.log(' holosplat/holosplat.iife.js ← editor runtime (gitignored)');
|
|
76
|
+
console.log(' scenes/ ← drop .spz / .ply / .splat files here');
|
|
77
|
+
|
|
78
|
+
if (!hasOwnServer) {
|
|
79
|
+
console.log('\n Start editing:');
|
|
80
|
+
console.log(' python server.py');
|
|
81
|
+
console.log(' open http://localhost:8080/holosplat/\n');
|
|
82
|
+
} else {
|
|
83
|
+
console.log('\n Next steps:');
|
|
84
|
+
console.log(' 1. Add routes from holosplat/server-snippet.js to your server');
|
|
85
|
+
console.log(' 2. Start your dev server');
|
|
86
|
+
console.log(' 3. Open http://localhost:<port>/holosplat/\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── upgrade ───────────────────────────────────────────────────────────────────
|
|
90
|
+
} else if (cmd === 'upgrade') {
|
|
91
|
+
const editorDir = path.join(cwd, 'holosplat');
|
|
92
|
+
if (!fs.existsSync(editorDir)) {
|
|
93
|
+
console.error('\n Run `npx holosplat init` first.\n');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const html = fs.readFileSync(path.join(pkg, 'holosplat', 'index.html'), 'utf8')
|
|
98
|
+
.replace('../dist/holosplat.iife.js', './holosplat.iife.js');
|
|
99
|
+
fs.writeFileSync(path.join(editorDir, 'index.html'), html);
|
|
100
|
+
fs.copyFileSync(
|
|
101
|
+
path.join(pkg, 'dist', 'holosplat.iife.js'),
|
|
102
|
+
path.join(editorDir, 'holosplat.iife.js')
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const serverDst = path.join(cwd, 'server.py');
|
|
106
|
+
if (fs.existsSync(serverDst))
|
|
107
|
+
fs.copyFileSync(path.join(pkg, 'server.py'), serverDst);
|
|
108
|
+
|
|
109
|
+
const snippetDst = path.join(cwd, 'holosplat', 'server-snippet.js');
|
|
110
|
+
if (fs.existsSync(snippetDst))
|
|
111
|
+
writeServerSnippet(path.join(cwd, 'holosplat'), detectFramework(readPkgJson(cwd)));
|
|
112
|
+
|
|
113
|
+
const version = require(path.join(pkg, 'package.json')).version;
|
|
114
|
+
console.log(`\n HoloSplat upgraded to v${version}\n`);
|
|
115
|
+
|
|
116
|
+
// ── help ──────────────────────────────────────────────────────────────────────
|
|
117
|
+
} else {
|
|
118
|
+
const version = require(path.join(pkg, 'package.json')).version;
|
|
119
|
+
console.log(`
|
|
120
|
+
HoloSplat CLI v${version}
|
|
121
|
+
|
|
122
|
+
npx holosplat init Set up the editor in this project
|
|
123
|
+
npx holosplat init --with-server Also copy server.py (Python dev server)
|
|
124
|
+
npx holosplat init --no-server Skip server setup entirely
|
|
125
|
+
npx holosplat upgrade Refresh editor files after updating the package
|
|
126
|
+
`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function readPkgJson(dir) {
|
|
132
|
+
try { return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); }
|
|
133
|
+
catch { return {}; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function detectFramework(pkg) {
|
|
137
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
138
|
+
if (deps.next) return 'nextjs';
|
|
139
|
+
if (deps.vite) return 'vite';
|
|
140
|
+
if (deps.express) return 'express';
|
|
141
|
+
if (pkg.scripts?.dev || pkg.scripts?.start) return 'generic';
|
|
142
|
+
return 'none';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function writeServerSnippet(dir, framework) {
|
|
146
|
+
const snippets = {
|
|
147
|
+
express: `// Express / Connect
|
|
148
|
+
import { createHsApiHandler } from 'holosplat/server';
|
|
149
|
+
app.use('/hs-api', createHsApiHandler());
|
|
150
|
+
`,
|
|
151
|
+
vite: `// Vite (vite.config.js)
|
|
152
|
+
import { createHsApiHandler } from 'holosplat/server';
|
|
153
|
+
export default defineConfig({
|
|
154
|
+
plugins: [{
|
|
155
|
+
name: 'holosplat',
|
|
156
|
+
configureServer(server) {
|
|
157
|
+
server.middlewares.use('/hs-api', createHsApiHandler());
|
|
158
|
+
},
|
|
159
|
+
}],
|
|
160
|
+
});
|
|
161
|
+
`,
|
|
162
|
+
nextjs: `// Next.js — pages router (pages/api/hs-api/[...route].js)
|
|
163
|
+
import { createHsApiHandler } from 'holosplat/server';
|
|
164
|
+
const handler = createHsApiHandler();
|
|
165
|
+
export default function hsApi(req, res) {
|
|
166
|
+
const sub = '/' + (req.query.route ?? []).join('/');
|
|
167
|
+
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
|
|
168
|
+
req.url = sub + qs;
|
|
169
|
+
handler(req, res);
|
|
170
|
+
}
|
|
171
|
+
export const config = { api: { bodyParser: false } };
|
|
172
|
+
`,
|
|
173
|
+
generic: `// Generic Node.js (mount at /hs-api in your server)
|
|
174
|
+
import { createHsApiHandler } from 'holosplat/server';
|
|
175
|
+
const hsApi = createHsApiHandler();
|
|
176
|
+
// Express: app.use('/hs-api', hsApi);
|
|
177
|
+
// Vite: server.middlewares.use('/hs-api', hsApi);
|
|
178
|
+
// Raw http: if (req.url.startsWith('/hs-api')) { req.url = req.url.slice(7); hsApi(req, res); }
|
|
179
|
+
`,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const body = `// HoloSplat API routes — add to your existing server
|
|
183
|
+
// Generated by: npx holosplat init
|
|
184
|
+
//
|
|
185
|
+
// The /holosplat editor needs GET /hs-api/ls, GET /hs-api/file, PUT /hs-api/file
|
|
186
|
+
// to read and write project files (hs-config.json, scene listings).
|
|
187
|
+
|
|
188
|
+
${snippets[framework] || snippets.generic}
|
|
189
|
+
// ── All frameworks ──────────────────────────────────────────────────────────
|
|
190
|
+
// Express / Connect: app.use('/hs-api', createHsApiHandler())
|
|
191
|
+
// Vite: server.middlewares.use('/hs-api', createHsApiHandler())
|
|
192
|
+
// Next.js pages: see pages/api/hs-api/[...route].js above
|
|
193
|
+
`;
|
|
194
|
+
fs.writeFileSync(path.join(dir, 'server-snippet.js'), body);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function writeClaudeMd(dir, framework, hasOwnServer) {
|
|
198
|
+
const claudePath = path.join(dir, 'CLAUDE.md');
|
|
199
|
+
const marker = '<!-- holosplat -->';
|
|
200
|
+
|
|
201
|
+
const serverSection = hasOwnServer
|
|
202
|
+
? serverInstructions(framework)
|
|
203
|
+
: `### Dev server
|
|
204
|
+
Run \`python server.py\` — serves the project and mounts \`/hs-api\` automatically.
|
|
205
|
+
The editor is at \`http://localhost:8080/holosplat/\`.`;
|
|
206
|
+
|
|
207
|
+
const section = `
|
|
208
|
+
${marker}
|
|
209
|
+
## HoloSplat
|
|
210
|
+
|
|
211
|
+
Installed as \`holosplat\` npm package. WebGPU Gaussian Splat viewer with scroll-driven animation.
|
|
212
|
+
|
|
213
|
+
### Import (ESM / bundler — no script tag needed)
|
|
214
|
+
\`\`\`js
|
|
215
|
+
import { Viewer, player, scrollScene } from 'holosplat';
|
|
216
|
+
\`\`\`
|
|
217
|
+
|
|
218
|
+
${serverSection}
|
|
219
|
+
|
|
220
|
+
### Single embed (auto-init via data attribute)
|
|
221
|
+
\`\`\`html
|
|
222
|
+
<div data-holosplat="/scenes/scene.spz"
|
|
223
|
+
data-holosplat-anim="/scenes/anim.json"
|
|
224
|
+
style="width:100%;height:500px"></div>
|
|
225
|
+
\`\`\`
|
|
226
|
+
|
|
227
|
+
### Scroll-driven scene (HTML structure)
|
|
228
|
+
\`\`\`html
|
|
229
|
+
<div class="hs-scene">
|
|
230
|
+
<!-- sticky canvas -->
|
|
231
|
+
<div class="hs-stage"
|
|
232
|
+
data-holosplat="/scenes/scene.spz"
|
|
233
|
+
data-holosplat-anim="/scenes/anim.json"></div>
|
|
234
|
+
|
|
235
|
+
<!-- scroll track — each child maps to a Blender marker range -->
|
|
236
|
+
<div class="hs-track">
|
|
237
|
+
<div class="hs-act" data-from="intro" data-to="next" style="height:200vh"></div>
|
|
238
|
+
<div class="hs-hold" data-frame="next" style="height:100vh"></div>
|
|
239
|
+
<div class="hs-act" data-from="next" data-to="end" style="height:300vh"></div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
\`\`\`
|
|
243
|
+
- \`data-from\` / \`data-to\` / \`data-frame\` accept Blender marker names or frame numbers.
|
|
244
|
+
- Height (vh) controls how many scroll pixels the animation takes — taller = slower.
|
|
245
|
+
- Special act types: \`data-from="pingpong-start"\` (auto-loop) and \`data-from="freecamera-start"\` (free orbit).
|
|
246
|
+
|
|
247
|
+
### hs-config.json (managed by the /holosplat editor, do not hand-edit)
|
|
248
|
+
The editor writes this file. At runtime, a loader reads it and populates the
|
|
249
|
+
\`.hs-track\` div with the correct \`data-from\`/\`data-to\`/\`style="height"\` attributes.
|
|
250
|
+
\`\`\`json
|
|
251
|
+
{
|
|
252
|
+
"version": 1,
|
|
253
|
+
"scene": "scenes/scene.spz",
|
|
254
|
+
"animation": "scenes/anim.json",
|
|
255
|
+
"acts": [
|
|
256
|
+
{ "id": "intro", "type": "act", "from": "intro", "to": "marker2", "height": 200 },
|
|
257
|
+
{ "id": "loop", "type": "pingpong", "from": "pingpong-start", "to": "pingpong-end", "height": 150 }
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
\`\`\`
|
|
261
|
+
\`type\` is one of \`act\` | \`pingpong\` | \`freecamera\` | \`hold\`. \`from\`/\`to\`/\`frame\` reference Blender timeline marker names.
|
|
262
|
+
|
|
263
|
+
### player(container, opts) — full option reference
|
|
264
|
+
\`\`\`js
|
|
265
|
+
const api = player('#scene', {
|
|
266
|
+
scene: '/scenes/scene.spz', // or 'parts' for a multi-file scene
|
|
267
|
+
animation: '/scenes/anim.json', // Blender-exported camera/markers/state timeline
|
|
268
|
+
clips: ['/scenes/asset-rig.json'], // string or array — product-customization assets, see below
|
|
269
|
+
partsDir: '/scenes/parts', // base dir clip parts resolve against
|
|
270
|
+
scenes: { // markerName → per-scene playback config
|
|
271
|
+
intro: { linkedId: 'intro', playback: 'auto', playOnce: true },
|
|
272
|
+
hero: { linkedId: 'hero', waitForTimeline: true, pingpong: true, blendOut: 46, pan: { enabled: true } },
|
|
273
|
+
},
|
|
274
|
+
masks: { partName: { feather: 0.3 } }, // mask-volume soft-edge overrides, by name
|
|
275
|
+
sh: 3, // global spherical-harmonics degree (0-3); omit to use per-scene/device-tier default
|
|
276
|
+
aaDilation: 0.3, // anti-aliasing covariance dilation
|
|
277
|
+
gpuSort: false, // opt-in GPU compute-shader radix sort
|
|
278
|
+
quality: 'auto', // 'auto'|'low'|'medium'|'high' — device-tier presets
|
|
279
|
+
flipY: false, splatScale: 1.08, autoRotate: false, background: 'transparent',
|
|
280
|
+
fov: 60, near: 0.01, far: 2000,
|
|
281
|
+
onLoad, onProgress, onError,
|
|
282
|
+
});
|
|
283
|
+
\`\`\`
|
|
284
|
+
|
|
285
|
+
### Runtime API (returned by player())
|
|
286
|
+
\`\`\`js
|
|
287
|
+
api.playClip(clipId) // trigger a "<product>-<variant>" clip — radio-group per product, see clips below
|
|
288
|
+
api.playVariant(axis, value) // crossfade an axis-transition (e.g. color swap): api.playVariant('color', 'blue')
|
|
289
|
+
api.playState(axis, value) // seek a continuous state timeline (e.g. fold/unfold, open/close a lid)
|
|
290
|
+
api.setVariant(partId, name) // instant per-part variant swap (palette/geometry), no animation
|
|
291
|
+
api.getVariants(partId) // list variant names available for a part
|
|
292
|
+
api.setMaskFeather(name, value) // override a mask volume's soft-edge falloff at runtime
|
|
293
|
+
api.setSplatScale(s) / setAutoRotate(v) / setFlipY(v) / setShDegree(n) / setAaDilation(v)
|
|
294
|
+
api.setAnimationPaused(v) / setCameraFree(v) / resetCamera()
|
|
295
|
+
api.loadClips(url, opts) / api.unloadClips(ids) // load/remove asset clip files after init
|
|
296
|
+
api.destroy()
|
|
297
|
+
\`\`\`
|
|
298
|
+
|
|
299
|
+
### Clips, axis transitions, and states (product customization)
|
|
300
|
+
These come from asset rig files (\`*-rig.json\`) — exported by the Blender pipeline in
|
|
301
|
+
the HoloSplat repo, never hand-written. A rig file's \`clips\`/\`transitions\`/\`states\`
|
|
302
|
+
map to the three playback primitives above:
|
|
303
|
+
- **clips** — one-shot in/hold/out triggers, grouped by product (\`playClip\`)
|
|
304
|
+
- **transitions** — two-value crossfades on a shared axis, e.g. color swatches (\`playVariant\`)
|
|
305
|
+
- **states** — a continuous per-axis timeline with named markers, e.g. fold/unfold, lid open/closed
|
|
306
|
+
(\`playState\`); also auto-driven by \`"state: <asset>.<axis>=<value>"\` markers in the main
|
|
307
|
+
animation timeline as the user scrolls, so it usually needs no button at all.
|
|
308
|
+
|
|
309
|
+
### Rules
|
|
310
|
+
- Never import from \`holosplat/server\` in browser/client code — it is Node.js only.
|
|
311
|
+
- Never deploy \`holosplat/\` — it is in \`.vercelignore\` and must stay local-only.
|
|
312
|
+
- \`holosplat/holosplat.iife.js\` is gitignored — it is a build copy for the editor preview.
|
|
313
|
+
- Scene files (\`.spz\`, \`.ply\`, \`.splat\`) go in \`scenes/\` or \`public/scenes/\`. Prefer \`.spz\`.
|
|
314
|
+
- \`hs-config.json\` and \`*-rig.json\` files are generated by the editor/Blender export — treat as data, don't hand-edit.
|
|
315
|
+
<!-- /holosplat -->
|
|
316
|
+
`;
|
|
317
|
+
|
|
318
|
+
if (fs.existsSync(claudePath)) {
|
|
319
|
+
let existing = fs.readFileSync(claudePath, 'utf8');
|
|
320
|
+
// Replace existing HoloSplat block if present, otherwise append
|
|
321
|
+
const re = new RegExp(`\n${marker}[\\s\\S]*?<!-- /holosplat -->`, 'g');
|
|
322
|
+
if (re.test(existing)) {
|
|
323
|
+
existing = existing.replace(re, section.trimEnd());
|
|
324
|
+
fs.writeFileSync(claudePath, existing);
|
|
325
|
+
} else {
|
|
326
|
+
fs.appendFileSync(claudePath, section);
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
fs.writeFileSync(claudePath, `# Project\n\nAdd project-specific notes here.\n${section}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function serverInstructions(framework) {
|
|
334
|
+
const examples = {
|
|
335
|
+
vite: `### Server API (Vite — vite.config.js)
|
|
336
|
+
\`\`\`js
|
|
337
|
+
import { createHsApiHandler } from 'holosplat/server';
|
|
338
|
+
export default defineConfig({
|
|
339
|
+
plugins: [{
|
|
340
|
+
name: 'holosplat',
|
|
341
|
+
configureServer(server) {
|
|
342
|
+
server.middlewares.use('/hs-api', createHsApiHandler());
|
|
343
|
+
},
|
|
344
|
+
}],
|
|
345
|
+
});
|
|
346
|
+
\`\`\``,
|
|
347
|
+
express: `### Server API (Express)
|
|
348
|
+
\`\`\`js
|
|
349
|
+
import { createHsApiHandler } from 'holosplat/server';
|
|
350
|
+
app.use('/hs-api', createHsApiHandler());
|
|
351
|
+
\`\`\``,
|
|
352
|
+
nextjs: `### Server API (Next.js — pages/api/hs-api/[...route].js)
|
|
353
|
+
\`\`\`js
|
|
354
|
+
import { createHsApiHandler } from 'holosplat/server';
|
|
355
|
+
const handler = createHsApiHandler();
|
|
356
|
+
export default function hsApi(req, res) {
|
|
357
|
+
const sub = '/' + (req.query.route ?? []).join('/');
|
|
358
|
+
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
|
|
359
|
+
req.url = sub + qs;
|
|
360
|
+
handler(req, res);
|
|
361
|
+
}
|
|
362
|
+
export const config = { api: { bodyParser: false } };
|
|
363
|
+
\`\`\``,
|
|
364
|
+
generic: `### Server API
|
|
365
|
+
\`\`\`js
|
|
366
|
+
import { createHsApiHandler } from 'holosplat/server';
|
|
367
|
+
// Mount at /hs-api in your server:
|
|
368
|
+
app.use('/hs-api', createHsApiHandler()); // Express/Connect
|
|
369
|
+
// or: server.middlewares.use('/hs-api', createHsApiHandler()); // Vite
|
|
370
|
+
\`\`\``,
|
|
371
|
+
};
|
|
372
|
+
return (examples[framework] || examples.generic) +
|
|
373
|
+
'\nThis is required for the `/holosplat` editor to read and write files. **Only enable in development.**';
|
|
374
|
+
}
|