babel-plugin-reactylon 1.2.0 → 1.3.1
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 +18 -17
- package/build/index.js +140 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
A Babel plugin designed to enable **tree shaking** in a Reactylon application. It statically analyzes Reactylon JSX elements and automatically manages imports and registrations of the relative Babylon.js classes, ensuring only the necessary parts of the Babylon.js library are included in the final bundle.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
|
-
- Automatic
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- Automatic
|
|
6
|
+
- **Automatic JSX component resolution & import generation**
|
|
7
|
+
|
|
8
|
+
Automatically detects the Babylon.js components used in JSX (e.g. `<box />`, `<arcRotateCamera />`, `<directionalLight />`) and generates the correct tree-shakable ES6 imports from `@babylonjs/core` and `@babylonjs/gui`.
|
|
9
|
+
|
|
10
|
+
- **Automatic Babylon.js side-effect handling**
|
|
11
|
+
|
|
12
|
+
Identifies all Babylon.js features that require side-effect imports and injects them automatically, including:
|
|
13
|
+
|
|
14
|
+
- **JSX-prop–based side effects**
|
|
15
|
+
|
|
16
|
+
Triggered by props like `checkCollisions`, `physicsOptions`, `showBoundingBox`, or by specific JSX elements such as `<audio>` or `<highlightLayer>`.
|
|
17
|
+
|
|
18
|
+
- **Prototype-based side effects**
|
|
19
|
+
|
|
20
|
+
Derived from method calls such as `scene.createDefaultCameraOrLight()` that rely on prototype extensions.
|
|
21
|
+
|
|
22
|
+
- **Constructor-based side effects** like `new ShadowGenerator(...)` that require additional runtime modules.
|
|
23
|
+
|
|
11
24
|
|
|
12
25
|
## Configuration
|
|
13
26
|
|
|
@@ -82,18 +95,6 @@ export default defineConfig({
|
|
|
82
95
|
})
|
|
83
96
|
```
|
|
84
97
|
|
|
85
|
-
### Additional configurations
|
|
86
|
-
Additionally you can manually define extra side effects (see [Babylon.js ES6 support FAQ](https://doc.babylonjs.com/setup/frameworkPackages/es6Support/#faq)).
|
|
87
|
-
|
|
88
|
-
```js
|
|
89
|
-
['babel-plugin-reactylon', {
|
|
90
|
-
// additional side effects (optional)
|
|
91
|
-
sideEffects: [
|
|
92
|
-
'@babylonjs/core/Engines/Extensions/engine.query.js'
|
|
93
|
-
]
|
|
94
|
-
}]
|
|
95
|
-
```
|
|
96
|
-
|
|
97
98
|
## How it works
|
|
98
99
|
The plugin analyzes your JSX code by scanning for Reactylon components. When a component is detected, it automatically imports the corresponding Babylon.js class and registers it with Reactylon, making it available in the rendering context. To remain consistent with Babylon.js modular architecture, the plugin selects the *deepest available class* implementation by recursively scanning the directory tree. This approach ensures that the most specific and optimized module is used, preventing unwanted side effects that could compromise tree shaking.
|
|
99
100
|
|
package/build/index.js
CHANGED
|
@@ -2,14 +2,60 @@ import { types as t } from '@babel/core';
|
|
|
2
2
|
import { declare } from "@babel/helper-plugin-utils";
|
|
3
3
|
import { BabylonPackages, builders, CollidingComponents, ReversedCollidingComponents } from 'reactylon';
|
|
4
4
|
import ParserUtils from './ParserUtils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Internal test purpose only: reset to isolate each test
|
|
7
|
+
*/
|
|
8
|
+
export function __resetReactylonBabelPluginStateForTests() {
|
|
9
|
+
babylonImportsSpecifiers.clear();
|
|
10
|
+
fileSideEffects.clear();
|
|
11
|
+
lastImport = null;
|
|
12
|
+
shouldSkipFile = false;
|
|
13
|
+
initialized = false;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* JSX tag name -> ImportSpecifier Babel
|
|
17
|
+
* Es: "box" -> import { _CreateBox } from "@babylonjs/core/Meshes/Builders/boxBuilder"
|
|
18
|
+
*/
|
|
5
19
|
const babylonImportsSpecifiers = new Map();
|
|
6
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Specific file side-effects for single file
|
|
22
|
+
*/
|
|
23
|
+
let fileSideEffects = new Set();
|
|
7
24
|
let lastImport = null;
|
|
8
25
|
let initialized = false;
|
|
9
26
|
let coreExportsMap = {};
|
|
10
27
|
let coreSideEffectsMap = {};
|
|
11
28
|
let guiExportsMap = {};
|
|
12
29
|
//let guiSideEffectsMap: Record<string, string> = {};
|
|
30
|
+
/**
|
|
31
|
+
* Reactylon "root" components: they are used as structural/entry-point
|
|
32
|
+
* components and for side-effect detection, but they must NOT be imported
|
|
33
|
+
* from Babylon or registered in Reactylon's inventory.
|
|
34
|
+
*/
|
|
35
|
+
const ROOT_REACTYLON_COMPONENTS = new Set([
|
|
36
|
+
'Engine',
|
|
37
|
+
'NativeEngine',
|
|
38
|
+
'Scene',
|
|
39
|
+
'Microgestures'
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Manual mapping for constructor-based side effects.
|
|
43
|
+
*
|
|
44
|
+
* This map exists specifically for side effects that are triggered when a
|
|
45
|
+
* class is instantiated (e.g. `new ShadowGenerator(...)`). These cases do
|
|
46
|
+
* not appear in the prototype-based analysis performed by
|
|
47
|
+
* `ParserUtils.getExportsAndSideEffects`, which detects only side effects
|
|
48
|
+
* added through prototype mutations or `defineProperty`.
|
|
49
|
+
*
|
|
50
|
+
* Reference: Babylon.js ES6 support FAQ (lists both prototype- and
|
|
51
|
+
* constructor-based side effects; only the constructor-based ones go in this map)
|
|
52
|
+
* https://doc.babylonjs.com/setup/frameworkPackages/es6Support/#faq
|
|
53
|
+
*
|
|
54
|
+
*/
|
|
55
|
+
const constructorCoreSideEffectsMap = {
|
|
56
|
+
ShadowGenerator: ['@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent']
|
|
57
|
+
};
|
|
58
|
+
let shouldSkipFile = false;
|
|
13
59
|
function capitalizeFirstLetter(str) {
|
|
14
60
|
return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;
|
|
15
61
|
}
|
|
@@ -20,32 +66,32 @@ export default declare((api) => {
|
|
|
20
66
|
options.plugins.push('jsx');
|
|
21
67
|
},
|
|
22
68
|
pre() {
|
|
23
|
-
var _a;
|
|
24
69
|
if (!initialized) {
|
|
25
|
-
const options = this.opts;
|
|
26
70
|
const coreExportsAndSideEffects = ParserUtils.generateExportsAndSideEffects('@babylonjs/core');
|
|
27
71
|
coreExportsMap = coreExportsAndSideEffects.exports;
|
|
28
72
|
coreSideEffectsMap = coreExportsAndSideEffects.sideEffects;
|
|
29
73
|
const guiExportsAndSideEffects = ParserUtils.generateExportsAndSideEffects('@babylonjs/gui');
|
|
30
74
|
guiExportsMap = guiExportsAndSideEffects.exports;
|
|
31
75
|
//guiSideEffectsMap = guiExportsAndSideEffects.sideEffects;
|
|
32
|
-
// Add explicit side effects (additional side effects declared by user)
|
|
33
|
-
(_a = options === null || options === void 0 ? void 0 : options.sideEffects) === null || _a === void 0 ? void 0 : _a.forEach((sideEffect) => {
|
|
34
|
-
sideEffects.push(t.importDeclaration([], t.stringLiteral(sideEffect)));
|
|
35
|
-
});
|
|
36
76
|
initialized = true;
|
|
37
77
|
}
|
|
38
78
|
},
|
|
39
79
|
post() {
|
|
40
80
|
babylonImportsSpecifiers.clear();
|
|
41
81
|
lastImport = null;
|
|
82
|
+
fileSideEffects.clear();
|
|
83
|
+
shouldSkipFile = false;
|
|
42
84
|
},
|
|
43
85
|
visitor: {
|
|
44
86
|
ImportDeclaration(importPath) {
|
|
87
|
+
if (shouldSkipFile)
|
|
88
|
+
return;
|
|
45
89
|
lastImport = importPath;
|
|
46
90
|
},
|
|
47
91
|
JSXOpeningElement(path) {
|
|
48
92
|
var _a;
|
|
93
|
+
if (shouldSkipFile)
|
|
94
|
+
return;
|
|
49
95
|
// Don't include non-React JSX
|
|
50
96
|
// https://github.com/facebook/jsx/issues/13
|
|
51
97
|
const { name, attributes } = path.node;
|
|
@@ -88,7 +134,8 @@ export default declare((api) => {
|
|
|
88
134
|
// Don't include colliding React JSX components
|
|
89
135
|
if (type in ReversedCollidingComponents)
|
|
90
136
|
return;
|
|
91
|
-
|
|
137
|
+
const shouldRegisterComponent = !ROOT_REACTYLON_COMPONENTS.has(type);
|
|
138
|
+
if (shouldRegisterComponent && !babylonImportsSpecifiers.has(type)) {
|
|
92
139
|
const normalizedType = type in CollidingComponents ? CollidingComponents[type] : type;
|
|
93
140
|
const isBuilder = builders.includes(normalizedType);
|
|
94
141
|
const className = isBuilder ? `Create${capitalizeFirstLetter(normalizedType)}` : capitalizeFirstLetter(normalizedType);
|
|
@@ -98,38 +145,91 @@ export default declare((api) => {
|
|
|
98
145
|
}
|
|
99
146
|
}
|
|
100
147
|
// Add implicit side effects (derived from static parse of JSX Reactylon code)
|
|
101
|
-
|
|
148
|
+
// Don't restrict the check on Engine component (type === 'Engine' && ...) because the user can create an alias of Engine's Reactylon component
|
|
149
|
+
const isMultipleCanvas = attributes.some(attr => t.isJSXAttribute(attr) && attr.name.name === "isMultipleCanvas");
|
|
102
150
|
if (isMultipleCanvas) {
|
|
103
|
-
|
|
151
|
+
fileSideEffects.add('@babylonjs/core/Engines/AbstractEngine/abstractEngine.views');
|
|
104
152
|
}
|
|
105
|
-
|
|
153
|
+
// Don't restrict the check on Scene component (type === 'Scene' && ...) because the user can create an alias of Scene's Reactylon component
|
|
154
|
+
const isPhysicsEngine = attributes.some(attr => t.isJSXAttribute(attr) && attr.name.name === "physicsOptions");
|
|
106
155
|
if (isPhysicsEngine) {
|
|
107
|
-
|
|
156
|
+
fileSideEffects.add('@babylonjs/core/Physics/physicsEngineComponent');
|
|
108
157
|
}
|
|
109
158
|
const isAudio = type === 'sound';
|
|
110
159
|
if (isAudio) {
|
|
111
160
|
// Audio v1
|
|
112
|
-
|
|
113
|
-
//
|
|
161
|
+
fileSideEffects.add('@babylonjs/core/Audio/audioSceneComponent');
|
|
162
|
+
// fileSideEffects.add('@babylonjs/core/Audio/audioEngine');
|
|
114
163
|
}
|
|
115
164
|
const isCheckCollisions = attributes.some(attr => t.isJSXAttribute(attr) && attr.name.name === "checkCollisions");
|
|
116
165
|
if (isCheckCollisions) {
|
|
117
|
-
|
|
166
|
+
fileSideEffects.add('@babylonjs/core/Collisions/collisionCoordinator');
|
|
118
167
|
}
|
|
119
168
|
const isHighlightLayer = type === 'highlightLayer';
|
|
120
169
|
if (isHighlightLayer) {
|
|
121
|
-
|
|
170
|
+
fileSideEffects.add('@babylonjs/core/Layers/effectLayerSceneComponent');
|
|
122
171
|
}
|
|
123
172
|
const isBoundingBox = attributes.some(attr => t.isJSXAttribute(attr) && attr.name.name === "showBoundingBox");
|
|
124
173
|
if (isBoundingBox) {
|
|
125
|
-
|
|
174
|
+
fileSideEffects.add('@babylonjs/core/Rendering/boundingBoxRenderer');
|
|
126
175
|
}
|
|
127
|
-
// add here other side effects (https://doc.babylonjs.com/setup/frameworkPackages/es6Support/#faq)
|
|
128
176
|
},
|
|
129
177
|
Program: {
|
|
178
|
+
enter() {
|
|
179
|
+
var _a, _b;
|
|
180
|
+
const filename = ((_b = (_a = this.file) === null || _a === void 0 ? void 0 : _a.opts) === null || _b === void 0 ? void 0 : _b.filename) || '';
|
|
181
|
+
if (filename.includes('node_modules')) {
|
|
182
|
+
//console.log(this.file?.opts?.filename);
|
|
183
|
+
shouldSkipFile = true;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
shouldSkipFile = false;
|
|
187
|
+
},
|
|
130
188
|
exit(path) {
|
|
131
|
-
if (
|
|
189
|
+
if (shouldSkipFile)
|
|
132
190
|
return;
|
|
191
|
+
path.traverse({
|
|
192
|
+
// Add implicit prototype-based side effects detected from call expressions, e.g. scene.createDefaultCameraOrLight()
|
|
193
|
+
CallExpression(callPath) {
|
|
194
|
+
const callee = callPath.node.callee;
|
|
195
|
+
if (t.isMemberExpression(callee)) {
|
|
196
|
+
const property = callee.property;
|
|
197
|
+
if (t.isIdentifier(property)) {
|
|
198
|
+
const sideEffectPath = coreSideEffectsMap[property.name];
|
|
199
|
+
// exclude assets folder containing .wasm and relative .js files
|
|
200
|
+
if (typeof sideEffectPath === 'string' && !sideEffectPath.startsWith('@babylonjs/core/assets/')) {
|
|
201
|
+
fileSideEffects.add(sideEffectPath);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
// Add constructor-based side effects detected from "new" expressions, e.g. new ShadowGenerator(...)
|
|
207
|
+
NewExpression(newPath) {
|
|
208
|
+
const callee = newPath.node.callee;
|
|
209
|
+
let name;
|
|
210
|
+
if (t.isIdentifier(callee)) {
|
|
211
|
+
// e.g.: new ShadowGenerator(...)
|
|
212
|
+
name = callee.name;
|
|
213
|
+
}
|
|
214
|
+
else if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
215
|
+
// new BABYLON.ShadowGenerator(...)
|
|
216
|
+
name = callee.property.name;
|
|
217
|
+
}
|
|
218
|
+
if (!name)
|
|
219
|
+
return;
|
|
220
|
+
const sideEffectPaths = constructorCoreSideEffectsMap[name];
|
|
221
|
+
if (!sideEffectPaths)
|
|
222
|
+
return;
|
|
223
|
+
for (const sideEffectPath of sideEffectPaths) {
|
|
224
|
+
fileSideEffects.add(sideEffectPath);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
const hasJSXReactylon = babylonImportsSpecifiers.size > 0;
|
|
229
|
+
const hasFileSideEffects = fileSideEffects.size > 0;
|
|
230
|
+
if (!hasJSXReactylon && !hasFileSideEffects) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
133
233
|
const babylonImports = [];
|
|
134
234
|
babylonImportsSpecifiers.forEach((importSpecifier, type) => {
|
|
135
235
|
let packageName = null;
|
|
@@ -146,37 +246,28 @@ export default declare((api) => {
|
|
|
146
246
|
const importDeclaration = t.importDeclaration([importSpecifier], t.stringLiteral(importPath));
|
|
147
247
|
babylonImports.push([type, importDeclaration, packageName]);
|
|
148
248
|
});
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
t.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
importSpecifier.
|
|
156
|
-
t.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// exclude assets folder containing .wasm and relative .js files
|
|
170
|
-
if (sideEffectPath && !sideEffectPath.startsWith('@babylonjs/core/assets/')) {
|
|
171
|
-
sideEffects.push(t.importDeclaration([], t.stringLiteral(sideEffectPath)));
|
|
172
|
-
// callPath.stop();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
});
|
|
249
|
+
const nodes = [];
|
|
250
|
+
if (hasJSXReactylon) {
|
|
251
|
+
const registerSpecifier = t.importSpecifier(path.scope.generateUidIdentifier('register'), t.identifier('register'));
|
|
252
|
+
const registerImportDeclaration = t.importDeclaration([registerSpecifier], t.stringLiteral('reactylon'));
|
|
253
|
+
const registerCall = t.expressionStatement(t.callExpression(registerSpecifier.local, [
|
|
254
|
+
t.objectExpression(babylonImports.map(([type, importDeclaration, packageName]) => {
|
|
255
|
+
const importSpecifier = importDeclaration.specifiers[0];
|
|
256
|
+
return t.objectProperty(t.stringLiteral(type), t.arrayExpression([
|
|
257
|
+
importSpecifier.local,
|
|
258
|
+
t.numericLiteral(packageName)
|
|
259
|
+
]), false, false);
|
|
260
|
+
})),
|
|
261
|
+
]));
|
|
262
|
+
const babylonImportDeclarations = babylonImports.map(([, importDeclaration]) => importDeclaration);
|
|
263
|
+
nodes.push(registerCall, registerImportDeclaration, ...babylonImportDeclarations);
|
|
264
|
+
}
|
|
265
|
+
// Add all file side effects (explicit, implicit prototype-based)
|
|
266
|
+
for (const pathStr of fileSideEffects) {
|
|
267
|
+
nodes.push(t.importDeclaration([], t.stringLiteral(pathStr)));
|
|
268
|
+
}
|
|
178
269
|
// Add imports and call register
|
|
179
|
-
for (const node of
|
|
270
|
+
for (const node of nodes) {
|
|
180
271
|
if (lastImport) {
|
|
181
272
|
lastImport.insertAfter(node);
|
|
182
273
|
}
|
package/package.json
CHANGED