@scenoco-three/vite 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SceNoCo contributors
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,28 @@
1
+ # @scenoco-three/vite
2
+
3
+ The [Vite](https://vite.dev) plugin for [SceNoCo](https://github.com/): import a scene XML
4
+ file and get a compiled bundle, with exactly the component and node modules that scene uses
5
+ imported alongside it — nothing more.
6
+
7
+ ```ts
8
+ // vite.config.ts
9
+ import { bundlePlugin } from '@scenoco-three/vite';
10
+ export default defineConfig({
11
+ plugins: [bundlePlugin({ componentRoots: ['src/components'] })],
12
+ });
13
+ ```
14
+
15
+ ```ts
16
+ import sceneBundle from './scenes/level.scene.xml?bundle'; // compiled in Node
17
+ engine.loadScene(sceneBundle);
18
+ ```
19
+
20
+ The scene is validated and compiled in Node (via `@scenoco-three/compiler`); the browser
21
+ receives the bundle data plus usage-based imports of the components/tags it needs (including
22
+ those used by embedded prefabs). No XML parser, validator, or compiler reaches the browser.
23
+
24
+ `vite` is a peer dependency. See the [repository](../../README.md).
25
+
26
+ ## License
27
+
28
+ [MIT](./LICENSE)
@@ -0,0 +1,37 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Vite plugin: `import scene from './x.scene.xml?bundle'`.
4
+ *
5
+ * At build/serve time (in Node) the scene XML is validated and compiled into an
6
+ * optimized SceNoCo bundle — the browser receives the bundle data plus imports
7
+ * of **exactly the component modules the scene uses**, nothing else:
8
+ *
9
+ * // generated module for x.scene.xml?bundle
10
+ * import '/abs/path/components/Rotator.ts'; // ← used by this scene
11
+ * import '/abs/path/components/Follow.ts'; // ← used by this scene
12
+ * export default [1, [...strings], [...asset]];
13
+ *
14
+ * So loading a scene automatically registers its components in the runtime
15
+ * Registry (decorator side effect), unused components are never shipped, and
16
+ * no XML parser/validator/compiler reaches the browser.
17
+ *
18
+ * Component discovery is shared with the CLI (`loadComponents` in
19
+ * @scenoco-three/compiler): files containing `@component({ name: '…' })` under
20
+ * `componentRoots` are compiled and registered in the Node process so the
21
+ * validator can check the scene, and the same scan maps each component name to
22
+ * the file the browser must import.
23
+ */
24
+ export interface BundlePluginOptions {
25
+ /**
26
+ * Directories (relative to the Vite project cwd) scanned for files declaring
27
+ * `@component` / `@node` / … tags. Defaults to `['src']`.
28
+ */
29
+ componentRoots?: string[];
30
+ /**
31
+ * npm packages that register tags on import (e.g. `['@scenoco-three/rapier']`).
32
+ * Their tags are validated like any other, and a scene that uses one imports
33
+ * the package; scenes that don't, don't.
34
+ */
35
+ registerPackages?: string[];
36
+ }
37
+ export declare function bundlePlugin(options?: BundlePluginOptions): Plugin;
package/dist/index.js ADDED
@@ -0,0 +1,106 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, isAbsolute, resolve } from 'node:path';
3
+ import { builtinNodeModule, bundlePrefab, bundleScene, compilePrefabXml, compileSceneXml, formatDiagnostic, loadComponents, } from '@scenoco-three/compiler';
4
+ export function bundlePlugin(options = {}) {
5
+ const componentRoots = options.componentRoots ?? ['src'];
6
+ const registerPackages = options.registerPackages ?? [];
7
+ let isServe = false;
8
+ return {
9
+ name: 'scenoco:bundle',
10
+ configResolved(config) {
11
+ isServe = config.command === 'serve';
12
+ },
13
+ async load(id) {
14
+ if (!id.endsWith('.xml?bundle'))
15
+ return null;
16
+ const file = id.slice(0, -'?bundle'.length);
17
+ const xml = readFileSync(file, 'utf8');
18
+ // Register project components in this Node process (validator needs the
19
+ // full set for checks and "did you mean" suggestions) and get the
20
+ // name → file map for the usage-based imports below.
21
+ const scan = await loadComponents(componentRoots, undefined, { packages: registerPackages }).catch((e) => {
22
+ const msg = e instanceof Error ? e.message : String(e);
23
+ this.error(`failed to load tag modules from [${componentRoots.join(', ')}]: ${msg}`);
24
+ });
25
+ // Recompile when any component file changes (a property change affects
26
+ // validation and compiled output), not just when the XML changes.
27
+ for (const componentFile of scan.files.keys())
28
+ this.addWatchFile(componentFile);
29
+ // Track `src=` resource files (materials, future prefabs) the same way.
30
+ const resolveAndWatch = (request, referrer) => {
31
+ const resolved = fsResolver(request, referrer);
32
+ if (resolved)
33
+ this.addWatchFile(resolved.path);
34
+ return resolved;
35
+ };
36
+ // `.prefab.xml` compiles to a standalone prefab bundle (for
37
+ // `engine.instantiate`); any other XML is a scene.
38
+ const isPrefab = file.endsWith('.prefab.xml');
39
+ const result = isPrefab
40
+ ? compilePrefabXml(xml, { path: file, resolve: resolveAndWatch })
41
+ : compileSceneXml(xml, { path: file, resolve: resolveAndWatch });
42
+ if (!result.ok) {
43
+ const lines = result.diagnostics.map((d) => formatDiagnostic(d, file)).join('\n');
44
+ this.error(`${isPrefab ? 'prefab' : 'scene'} failed to compile:\n${lines}`);
45
+ }
46
+ // Usage-based imports: only the modules that register the component types
47
+ // and node/geometry/material/setting tags this scene/prefab actually uses.
48
+ // A module is either a project file (absolute path → /@fs import) or a bare
49
+ // package specifier (a tag package, or a @scenoco-three/core built-in
50
+ // subpath). Unused tags (and their three classes) never ship.
51
+ const projectFiles = new Set(); // absolute paths
52
+ const bareSpecifiers = new Set(); // package specifiers
53
+ const addModule = (mod) => {
54
+ if (!mod)
55
+ return;
56
+ if (isAbsolute(mod))
57
+ projectFiles.add(mod);
58
+ else
59
+ bareSpecifiers.add(mod);
60
+ };
61
+ const addTag = (tag) => {
62
+ const mod = scan.byTag.get(tag);
63
+ if (mod)
64
+ addModule(mod);
65
+ else {
66
+ const builtin = builtinNodeModule(tag);
67
+ if (builtin)
68
+ bareSpecifiers.add(builtin);
69
+ }
70
+ };
71
+ // Walk the scene and every embedded prefab (referenced by `prefab` props):
72
+ // a spawned prefab registers its own components/tags, so they must ship too.
73
+ const collectModules = (scene) => {
74
+ for (const c of scene.components)
75
+ addModule(scan.byName.get(c.type));
76
+ for (const n of scene.nodes)
77
+ addTag(n.tag);
78
+ for (const a of scene.attachments)
79
+ addTag(a.tag);
80
+ for (const p of scene.prefabs ?? [])
81
+ collectModules(p);
82
+ };
83
+ collectModules(result.scene);
84
+ const imports = [
85
+ ...[...projectFiles].map((f) => `import ${JSON.stringify(asImportSpecifier(f, isServe))};`),
86
+ ...[...bareSpecifiers].map((spec) => `import ${JSON.stringify(spec)};`),
87
+ ].join('\n');
88
+ const bundle = isPrefab ? bundlePrefab(result.scene) : bundleScene(result.scene);
89
+ return `${imports}\nexport default ${JSON.stringify(bundle)};`;
90
+ },
91
+ };
92
+ }
93
+ /**
94
+ * Dev server needs the `/@fs/` protocol for absolute paths that may live
95
+ * outside the Vite root; the production build resolves plain absolute paths.
96
+ */
97
+ function asImportSpecifier(filePath, isServe) {
98
+ const normalized = filePath.replace(/\\/g, '/');
99
+ if (!isServe)
100
+ return normalized;
101
+ return normalized.startsWith('/') ? `/@fs${normalized}` : `/@fs/${normalized}`;
102
+ }
103
+ const fsResolver = (request, referrer) => {
104
+ const path = resolve(referrer ? dirname(referrer) : '.', request);
105
+ return existsSync(path) ? { path, contents: readFileSync(path, 'utf8') } : null;
106
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@scenoco-three/vite",
3
+ "version": "0.1.0",
4
+ "description": "Vite plugin for SceNoCo XML scene bundling",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "keywords": ["three", "threejs", "vite", "vite-plugin", "scene", "components", "gamedev"],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.build.json",
24
+ "typecheck": "npm --prefix ../core run build && npm --prefix ../compiler run build && tsc --noEmit -p tsconfig.json"
25
+ },
26
+ "dependencies": {
27
+ "@scenoco-three/compiler": "^0.1.0"
28
+ },
29
+ "peerDependencies": {
30
+ "vite": ">=5"
31
+ }
32
+ }