babel-plugin-reactylon 1.2.0 → 1.3.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.
Files changed (3) hide show
  1. package/README.md +18 -17
  2. package/build/index.js +142 -49
  3. 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 detection of Babylon.js components used in JSX
7
- - Automatic import of Babylon.js classes from `@babylonjs/core` e `@babylonjs/gui`
8
- - Implicit side effects management (detect prototype-level side effects in Babylon.js and ensures proper runtime behaviour)
9
- - Explicit side effects support (allows users to define custom side effects manually)
10
- - Automatic registration (ensures that used Babylon.js classes are registered within Reactylon's internal registry)
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
- const sideEffects = [];
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
- if (!babylonImportsSpecifiers.has(type)) {
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,93 @@ export default declare((api) => {
98
145
  }
99
146
  }
100
147
  // Add implicit side effects (derived from static parse of JSX Reactylon code)
101
- const isMultipleCanvas = type === 'Engine' && attributes.some(attr => t.isJSXAttribute(attr) && attr.name.name === "isMultipleCanvas");
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
- sideEffects.push(t.importDeclaration([], t.stringLiteral('@babylonjs/core/Engines/AbstractEngine/abstractEngine.views.js')));
151
+ fileSideEffects.add('@babylonjs/core/Engines/AbstractEngine/abstractEngine.views');
104
152
  }
105
- const isPhysicsEngine = type === 'Scene' && attributes.some(attr => t.isJSXAttribute(attr) && attr.name.name === "physicsOptions");
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
- sideEffects.push(t.importDeclaration([], t.stringLiteral('@babylonjs/core/Physics/physicsEngineComponent.js')));
156
+ fileSideEffects.add('@babylonjs/core/Physics/physicsEngineComponent');
108
157
  }
109
158
  const isAudio = type === 'sound';
110
159
  if (isAudio) {
111
160
  // Audio v1
112
- sideEffects.push(t.importDeclaration([], t.stringLiteral('@babylonjs/core/Audio/audioSceneComponent.js')));
113
- // sideEffects.push(t.importDeclaration([], t.stringLiteral('@babylonjs/core/Audio/audioEngine.js')));
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
- sideEffects.push(t.importDeclaration([], t.stringLiteral('@babylonjs/core/Collisions/collisionCoordinator.js')));
166
+ fileSideEffects.add('@babylonjs/core/Collisions/collisionCoordinator');
118
167
  }
119
168
  const isHighlightLayer = type === 'highlightLayer';
120
169
  if (isHighlightLayer) {
121
- sideEffects.push(t.importDeclaration([], t.stringLiteral('@babylonjs/core/Layers/effectLayerSceneComponent.js')));
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
- sideEffects.push(t.importDeclaration([], t.stringLiteral('@babylonjs/core/Rendering/boundingBoxRenderer.js')));
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(path) {
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
+ path.stop();
185
+ return;
186
+ }
187
+ shouldSkipFile = false;
188
+ },
130
189
  exit(path) {
131
- if (!babylonImportsSpecifiers.size)
190
+ if (shouldSkipFile)
132
191
  return;
192
+ path.traverse({
193
+ // Add implicit prototype-based side effects detected from call expressions, e.g. scene.createDefaultCameraOrLight()
194
+ CallExpression(callPath) {
195
+ const callee = callPath.node.callee;
196
+ if (t.isMemberExpression(callee)) {
197
+ const property = callee.property;
198
+ if (t.isIdentifier(property)) {
199
+ const sideEffectPath = coreSideEffectsMap[property.name];
200
+ // exclude assets folder containing .wasm and relative .js files
201
+ if (typeof sideEffectPath === 'string' && !sideEffectPath.startsWith('@babylonjs/core/assets/')) {
202
+ fileSideEffects.add(sideEffectPath);
203
+ // callPath.stop();
204
+ }
205
+ }
206
+ }
207
+ },
208
+ // Add constructor-based side effects detected from "new" expressions, e.g. new ShadowGenerator(...)
209
+ NewExpression(newPath) {
210
+ const callee = newPath.node.callee;
211
+ let name;
212
+ if (t.isIdentifier(callee)) {
213
+ // e.g.: new ShadowGenerator(...)
214
+ name = callee.name;
215
+ }
216
+ else if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
217
+ // new BABYLON.ShadowGenerator(...)
218
+ name = callee.property.name;
219
+ }
220
+ if (!name)
221
+ return;
222
+ const sideEffectPaths = constructorCoreSideEffectsMap[name];
223
+ if (!sideEffectPaths)
224
+ return;
225
+ for (const sideEffectPath of sideEffectPaths) {
226
+ fileSideEffects.add(sideEffectPath);
227
+ }
228
+ }
229
+ });
230
+ const hasJSXReactylon = babylonImportsSpecifiers.size > 0;
231
+ const hasFileSideEffects = fileSideEffects.size > 0;
232
+ if (!hasJSXReactylon && !hasFileSideEffects) {
233
+ return;
234
+ }
133
235
  const babylonImports = [];
134
236
  babylonImportsSpecifiers.forEach((importSpecifier, type) => {
135
237
  let packageName = null;
@@ -146,37 +248,28 @@ export default declare((api) => {
146
248
  const importDeclaration = t.importDeclaration([importSpecifier], t.stringLiteral(importPath));
147
249
  babylonImports.push([type, importDeclaration, packageName]);
148
250
  });
149
- const registerSpecifier = t.importSpecifier(path.scope.generateUidIdentifier('register'), t.identifier('register'));
150
- const registerImportDeclaration = t.importDeclaration([registerSpecifier], t.stringLiteral('reactylon'));
151
- const registerCall = t.expressionStatement(t.callExpression(registerSpecifier.local, [
152
- t.objectExpression(babylonImports.map(([type, importDeclaration, packageName]) => {
153
- const importSpecifier = importDeclaration.specifiers[0];
154
- return t.objectProperty(t.stringLiteral(type), t.arrayExpression([
155
- importSpecifier.local,
156
- t.numericLiteral(packageName)
157
- ]), false, false);
158
- })),
159
- ]));
160
- const babylonImportDeclarations = babylonImports.map(([, importDeclaration]) => importDeclaration);
161
- // Add implicit prototype-level side effects (derived from static parse of JavaScript code, e.g. scene.createDefaultCameraOrLight)
162
- path.traverse({
163
- CallExpression(callPath) {
164
- const callee = callPath.node.callee;
165
- if (t.isMemberExpression(callee)) {
166
- const property = callee.property;
167
- if (t.isIdentifier(property)) {
168
- const sideEffectPath = coreSideEffectsMap[property.name];
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
- });
251
+ const nodes = [];
252
+ if (hasJSXReactylon) {
253
+ const registerSpecifier = t.importSpecifier(path.scope.generateUidIdentifier('register'), t.identifier('register'));
254
+ const registerImportDeclaration = t.importDeclaration([registerSpecifier], t.stringLiteral('reactylon'));
255
+ const registerCall = t.expressionStatement(t.callExpression(registerSpecifier.local, [
256
+ t.objectExpression(babylonImports.map(([type, importDeclaration, packageName]) => {
257
+ const importSpecifier = importDeclaration.specifiers[0];
258
+ return t.objectProperty(t.stringLiteral(type), t.arrayExpression([
259
+ importSpecifier.local,
260
+ t.numericLiteral(packageName)
261
+ ]), false, false);
262
+ })),
263
+ ]));
264
+ const babylonImportDeclarations = babylonImports.map(([, importDeclaration]) => importDeclaration);
265
+ nodes.push(registerCall, registerImportDeclaration, ...babylonImportDeclarations);
266
+ }
267
+ // Add all file side effects (explicit, implicit prototype-based)
268
+ for (const pathStr of fileSideEffects) {
269
+ nodes.push(t.importDeclaration([], t.stringLiteral(pathStr)));
270
+ }
178
271
  // Add imports and call register
179
- for (const node of [registerCall, registerImportDeclaration, ...babylonImportDeclarations, ...sideEffects]) {
272
+ for (const node of nodes) {
180
273
  if (lastImport) {
181
274
  lastImport.insertAfter(node);
182
275
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babel-plugin-reactylon",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Babel plugin to enable tree-shaking of Babylon.js classes within a Reactylon application.",
5
5
  "main": "build/index.js",
6
6
  "type": "module",