Needle Engine

Changes between version 3.10.2-beta and 3.10.3-beta
Files changed (21) hide show
  1. plugins/common/config.cjs +1 -1
  2. plugins/vite/defines.js +1 -1
  3. plugins/vite/meta.js +1 -1
  4. plugins/next/next.js +9 -5
  5. src/engine/codegen/register_types.js +2 -2
  6. plugins/vite/utils.js +0 -11
  7. src/engine-components/AnimatorController.ts +8 -3
  8. src/engine/engine_constants.ts +13 -2
  9. src/engine/engine_element_attributes.ts +2 -2
  10. src/engine/engine_element.ts +1 -1
  11. src/engine/engine_scenetools.ts +6 -6
  12. src/engine/engine_texture.ts +4 -0
  13. src/engine/extensions/extensions.ts +14 -3
  14. src/engine-components/ui/Graphic.ts +1 -0
  15. src/engine-components/ui/Image.ts +4 -7
  16. src/engine-components/ui/RectTransform.ts +1 -1
  17. src/engine-components/ui/Text.ts +14 -26
  18. src/engine-components/webxr/WebARCameraBackground.ts +12 -2
  19. plugins/common/config.js +20 -0
  20. plugins/common/generator.js +11 -0
  21. plugins/common/version.js +12 -0
plugins/common/config.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
 
3
3
 
4
- module.exports.getMeta = function () {
4
+ module.exports.getMeta = function() {
5
5
  const workingDirectory = process.cwd();
6
6
  const needleConfig = require(workingDirectory + "/needle.config.json");
7
7
  if (needleConfig.codegenDirectory) {
plugins/vite/defines.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { loadConfig } from "./config.js";
2
- import { tryGetNeedleEngineVersion } from "./utils.js";
2
+ import { tryGetNeedleEngineVersion } from "../common/version.js";
3
3
 
4
4
  // NOTE: ALL DEFINES MUST BE SET HERE! NEVER ADD OR RELY ON DEFINES IN ANY OTHER PLUGIN
5
5
 
plugins/vite/meta.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { loadConfig } from './config.js';
2
2
  import fs from 'fs';
3
3
  import { getPosterPath } from './poster.js';
4
- import { tryGetNeedleEngineVersion } from './utils.js';
4
+ import { tryGetNeedleEngineVersion } from '../common/version.js';
5
5
 
6
6
  /**
7
7
  * @param {import('../types').userSettings} userSettings
plugins/next/next.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { fileURLToPath } from 'url';
2
2
  import { dirname, resolve } from 'path';
3
+ import { tryGetNeedleEngineVersion } from '../common/version.js';
4
+ import { tryGetGenerator } from '../common/generator.js';
5
+ import { getMeta } from '../common/config.cjs';
3
6
  // import { ApplyLicensePlugin } from './license.js';
4
7
 
5
8
  const __filename = fileURLToPath(import.meta.url);
@@ -34,20 +37,21 @@
34
37
  }
35
38
  /** @param {import ('next').NextConfig config } */
36
39
  function nextWebPack(config, { buildId, dev, isServer, defaultLoaders, webpack }) {
40
+ const meta = getMeta();
41
+ let useRapier = userSettings?.useRapier ?? meta?.useRapier === false ?? true;
37
42
  // add defines
38
43
  const webpackModule = userSettings.modules?.webpack;
39
44
  const definePlugin = webpackModule && new webpackModule.DefinePlugin({
40
- NEEDLE_ENGINE_META: {},
41
- NEEDLE_USE_RAPIER: true,
45
+ NEEDLE_ENGINE_VERSION: JSON.stringify(tryGetNeedleEngineVersion() ?? "0.0.0"),
46
+ NEEDLE_ENGINE_GENERATOR: JSON.stringify(tryGetGenerator() ?? "unknown"),
47
+ NEEDLE_USE_RAPIER: JSON.stringify(useRapier),
48
+ // TODO globalThis is not solved via DefinePlugin
42
49
  parcelRequire: undefined,
43
50
  });
44
51
  if (!definePlugin) console.log("WARN: no define plugin provided. Did you miss adding the webpack module to the next config? You can pass it to the Needle plugins via `nextConfig.modules = { webpack };`");
45
52
  else
46
53
  config.plugins.push(definePlugin);
47
54
 
48
-
49
-
50
-
51
55
  if (!config.module) config.module = {};
52
56
  if (!config.module.rules) config.module.rules = [];
53
57
  // add license plugin
src/engine/codegen/register_types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TypeStore } from "./../engine_typestore"
2
-
2
+
3
3
  // Import types
4
4
  import { __Ignore } from "../../engine-components/codegen/components";
5
5
  import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
@@ -217,7 +217,7 @@
217
217
  import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering";
218
218
  import { XRRig } from "../../engine-components/webxr/WebXRRig";
219
219
  import { XRState } from "../../engine-components/XRFlag";
220
-
220
+
221
221
  // Register types
222
222
  TypeStore.add("__Ignore", __Ignore);
223
223
  TypeStore.add("ActionBuilder", ActionBuilder);
plugins/vite/utils.js DELETED
@@ -1,11 +0,0 @@
1
- import { existsSync, readFileSync } from "fs";
2
-
3
- export function tryGetNeedleEngineVersion() {
4
- const needleEnginePackageJsonPath = process.cwd() + "/node_modules/@needle-tools/engine/package.json";
5
- if (existsSync(needleEnginePackageJsonPath)) {
6
- const json = JSON.parse(readFileSync(needleEnginePackageJsonPath));
7
- const version = json.version;
8
- return version;
9
- }
10
- return null;
11
- }
src/engine-components/AnimatorController.ts CHANGED
@@ -337,7 +337,7 @@
337
337
 
338
338
  state = this.getState(state, layerIndex) as State;
339
339
 
340
- if (!state?.motion || !state.motion.clip) {
340
+ if (!state?.motion || !state.motion.clip || !(state.motion.clip instanceof AnimationClip)) {
341
341
  // if(debug) console.warn("State has no clip or motion", state);
342
342
  return;
343
343
  }
@@ -503,8 +503,13 @@
503
503
 
504
504
  if (state.motion?.clip) {
505
505
  const clip = state.motion.clip;
506
- const action = this.createAction(clip);
507
- state.motion.action = action;
506
+ if (clip instanceof AnimationClip) {
507
+ const action = this.createAction(clip);
508
+ state.motion.action = action;
509
+ }
510
+ else {
511
+ if(debug || isDevEnvironment()) console.warn("No valid animationclip assigned", state);
512
+ }
508
513
  }
509
514
 
510
515
  // create state machine behaviours
src/engine/engine_constants.ts CHANGED
@@ -1,12 +1,23 @@
1
1
  import { getParam } from "../engine/engine_utils";
2
2
  const debug = getParam("debugdefines");
3
3
 
4
- declare const NEEDLE_ENGINE_VERSION: string;
5
- declare const NEEDLE_ENGINE_GENERATOR: string;
4
+ // We jump through hoops like this to support 3 cases:
5
+ // 1) Vanilla js or angular js where global defines are not guaranteed to be made
6
+ // 2) Vite where global defines are made, vite defines are also automatically set to globalThis
7
+ // 3) Webpack where global defines are not made BUT declare const variables are replaces with the actual value (via the webpack DefinePlugin)
6
8
 
7
9
  tryEval(`if(!globalThis["NEEDLE_ENGINE_VERSION"]) globalThis["NEEDLE_ENGINE_VERSION"] = "0.0.0";`)
8
10
  tryEval(`if(!globalThis["NEEDLE_ENGINE_GENERATOR"]) globalThis["NEEDLE_ENGINE_GENERATOR"] = "unknown";`)
9
11
 
12
+ declare const NEEDLE_ENGINE_VERSION: string
13
+ declare const NEEDLE_ENGINE_GENERATOR: string;
14
+
15
+ // Make sure to wrap the new global this define in underscores to prevent the bundler from replacing it with the actual value
16
+ tryEval(`globalThis["__NEEDLE_ENGINE_VERSION__"] = "` + NEEDLE_ENGINE_VERSION + `";`)
17
+ tryEval(`globalThis["__NEEDLE_ENGINE_GENERATOR__"] = "` + NEEDLE_ENGINE_GENERATOR + `";`)
18
+
19
+
20
+
10
21
  export const VERSION = NEEDLE_ENGINE_VERSION;
11
22
  export const GENERATOR = NEEDLE_ENGINE_GENERATOR;
12
23
  if (debug) console.log(`Engine version: ${VERSION} (generator: ${GENERATOR})`);
src/engine/engine_element_attributes.ts CHANGED
@@ -26,8 +26,8 @@
26
26
  /** Add to prevent Needle Engine context from being disposed when the element is removed from the DOM */
27
27
  "keep-alive"? : boolean;
28
28
 
29
- addEventListener(event: "ready", callback: (event: CustomEvent) => void): void;
30
- addEventListener(event: "error", callback: (event: CustomEvent) => void): void;
29
+ addEventListener?(event: "ready", callback: (event: CustomEvent) => void): void;
30
+ addEventListener?(event: "error", callback: (event: CustomEvent) => void): void;
31
31
  }
32
32
 
33
33
  type LoadingAttributes = {
src/engine/engine_element.ts CHANGED
@@ -152,7 +152,7 @@
152
152
  this.onSetupDesktop();
153
153
 
154
154
  if (!this.getAttribute("src")) {
155
- const global = globalThis["needle:codegen_files"];
155
+ const global = globalThis["needle:codegen_files"] as unknown as string;
156
156
  if (global) {
157
157
  if (debug) console.log("globalThis[\"needle:codegen_files\"]", global);
158
158
  this.setAttribute("src", global);
src/engine/engine_scenetools.ts CHANGED
@@ -89,18 +89,18 @@
89
89
  await getLoader().createBuiltinComponents(context, gltfId, gltf, seed, componentsExtension);
90
90
  }
91
91
 
92
- export function createGLTFLoader(url: string, context: Context) {
92
+ export async function createGLTFLoader(url: string, context: Context) {
93
93
  const loader = new GLTFLoader();
94
94
  const sourceId: SourceIdentifier = url;
95
- registerExtensions(loader, context, sourceId);
95
+ await registerExtensions(loader, context, sourceId);
96
96
  return loader;
97
97
  }
98
98
 
99
- export function parseSync(context: Context, data: string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined> {
99
+ export async function parseSync(context: Context, data: string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined> {
100
100
  if (typeof path !== "string") {
101
101
  console.warn("Parse gltf binary without path, this might lead to errors in resolving extensions. Please provide the source path of the gltf/glb file", path, typeof path);
102
102
  }
103
- const loader = createGLTFLoader(path, context);
103
+ const loader = await createGLTFLoader(path, context);
104
104
  const componentsExtension = registerComponentExtension(loader);
105
105
  return new Promise((resolve, reject) => {
106
106
  try {
@@ -125,13 +125,13 @@
125
125
  });
126
126
  }
127
127
 
128
- export function loadSync(context: Context, url: string, sourceId: string, seed: number | UIDProvider | null, prog?: (ProgressEvent) => void): Promise<GLTF | undefined> {
128
+ export async function loadSync(context: Context, url: string, sourceId: string, seed: number | UIDProvider | null, prog?: (ProgressEvent) => void): Promise<GLTF | undefined> {
129
129
  // better to create new loaders every time
130
130
  // (maybe we can cache them...)
131
131
  // but due to the async nature and potentially triggering multiple loads at the same time
132
132
  // we need to make sure the extensions dont override each other
133
133
  // creating new loaders should not be expensive as well
134
- const loader = createGLTFLoader(url, context);
134
+ const loader = await createGLTFLoader(url, context);
135
135
  const componentsExtension = registerComponentExtension(loader);
136
136
  return new Promise((resolve, reject) => {
137
137
  try {
src/engine/engine_texture.ts CHANGED
@@ -18,9 +18,13 @@
18
18
  else {
19
19
  this.onBeforeRender();
20
20
  const prev = renderer.getRenderTarget();
21
+ const xr = renderer.xr.enabled;
22
+ renderer.xr.enabled = false;
21
23
  renderer.setRenderTarget(this);
24
+ renderer.clear(true, true, true);
22
25
  renderer.render(scene, camera);
23
26
  renderer.setRenderTarget(prev);
27
+ renderer.xr.enabled = xr;
24
28
  this.onAfterRender();
25
29
  }
26
30
  }
src/engine/extensions/extensions.ts CHANGED
@@ -16,11 +16,15 @@
16
16
  import { InternalUsageTrackerPlugin } from "./usage_tracker";
17
17
  import { isUsageTrackingEnabled } from "../engine_assetdatabase";
18
18
  import { GLTFLoaderPlugin } from "three/examples/jsm/loaders/GLTFLoader.js";
19
+ import { getParam } from "../engine_utils";
20
+ import { isDevEnvironment } from "../debug";
19
21
  // import { GLTFAnimationPointerExtension } from "three/examples/jsm/loaders/GLTFLoaderAnimationPointer";
20
22
 
23
+ const debug = getParam("debugextensions");
24
+
21
25
  // lazily import the GLTFAnimationPointerExtension in case it doesnt exist (e.g. using vanilla three)
22
- let GLTFAnimationPointerExtension : any;
23
- import("three/examples/jsm/loaders/GLTFLoaderAnimationPointer").then(mod => {
26
+ let GLTFAnimationPointerExtension: any;
27
+ const KHR_ANIMATIONPOINTER_IMPORT = import("three/examples/jsm/loaders/GLTFLoaderAnimationPointer.js").then(async mod => {
24
28
  GLTFAnimationPointerExtension = mod.GLTFAnimationPointerExtension;
25
29
  return GLTFAnimationPointerExtension;
26
30
  }).catch(e => {
@@ -60,7 +64,7 @@
60
64
  }
61
65
  }
62
66
 
63
- export function registerExtensions(loader: GLTFLoader, context: Context, sourceId: SourceIdentifier) {
67
+ export async function registerExtensions(loader: GLTFLoader, context: Context, sourceId: SourceIdentifier) {
64
68
 
65
69
  // Make sure to remove any url parameters from the sourceId (because the source id in the renderer does not have a ?v=xxx so it will not be able to register the resolved lightmap otherwise)
66
70
  const idEnd = sourceId.lastIndexOf("?");
@@ -80,6 +84,7 @@
80
84
  for (const ext of _addedCustomExtension)
81
85
  loader.register(p => new ext(p));
82
86
 
87
+ await KHR_ANIMATIONPOINTER_IMPORT.catch(_ => { })
83
88
  loader.register(p => {
84
89
  if (GLTFAnimationPointerExtension) {
85
90
  const ext = new GLTFAnimationPointerExtension(p);
@@ -87,6 +92,12 @@
87
92
  setPointerResolverFunction.bind(ext)(new PointerResolver());
88
93
  return ext;
89
94
  }
95
+ else {
96
+ if (debug || isDevEnvironment()) console.error("Missing KHR_animation_pointer extension...")
97
+ return {
98
+ name: "KHR_animation_pointer_NOT_AVAILABLE"
99
+ };
100
+ }
90
101
  });
91
102
 
92
103
  }
src/engine-components/ui/Graphic.ts CHANGED
@@ -206,6 +206,7 @@
206
206
  else {
207
207
  this.setOptions({ backgroundImage: null, borderRadius: 0, backgroundOpacity: this.color.alpha });
208
208
  }
209
+ this.markDirty();
209
210
  }
210
211
 
211
212
  protected onAfterAddedToScene(): void {
src/engine-components/ui/Image.ts CHANGED
@@ -13,12 +13,8 @@
13
13
  export class Image extends MaskableGraphic {
14
14
 
15
15
  set image(img: Texture | null) {
16
- if (this.sprite)
17
- this.sprite.texture = img;
18
- else {
19
- this.sprite = new Sprite();
20
- this.sprite.texture = img;
21
- }
16
+ if (!this.sprite) this.sprite = new Sprite();
17
+ this.sprite.texture = img;
22
18
  this.onAfterCreated();
23
19
  }
24
20
  get image(): Texture | null {
@@ -72,8 +68,9 @@
72
68
  }
73
69
 
74
70
  protected onAfterCreated(): void {
75
- if(!this.__didAwake) return;
71
+ if (!this.__didAwake) return;
76
72
  super.onAfterCreated();
73
+ // TODO: @swingingtom setting a built-in sprite at runtime doesnt update the image
77
74
  if (this.isBuiltinSprite()) return;
78
75
  this.setTexture(this.sprite?.texture);
79
76
  }
src/engine-components/ui/RectTransform.ts CHANGED
@@ -112,7 +112,6 @@
112
112
  // TODO: get rid of the initial position
113
113
  this._initialPosition = this.gameObject.position.clone();
114
114
  this._initialPosition.z = 0;
115
- this.onApplyTransform("RectTransform awake");
116
115
 
117
116
  // TODO: we need to replace this with the watch that e.g. Rigibody is using (or the one in utils?)
118
117
  // perhaps we can also just manually check the few properties in the update loops?
@@ -129,6 +128,7 @@
129
128
  this.addShadowComponent(this.rectBlock);
130
129
  this._transformNeedsUpdate = true;
131
130
  this.Canvas?.registerTransform(this);
131
+ // this.onApplyTransform("enable");
132
132
  }
133
133
 
134
134
  onDisable() {
src/engine-components/ui/Text.ts CHANGED
@@ -104,8 +104,7 @@
104
104
 
105
105
  onBeforeRender(): void {
106
106
  // TODO TMUI @swingingtom this is so we don't have text clipping
107
- if (this.uiObject && (this.Canvas?.screenspace || this.context.isInVR))
108
- {
107
+ if (this.uiObject && (this.Canvas?.screenspace || this.context.isInVR)) {
109
108
  this.updateOverflow();
110
109
  }
111
110
  }
@@ -269,11 +268,12 @@
269
268
  return opts;
270
269
  }
271
270
 
272
- private feedText(text: string, richText: boolean) {
271
+ private feedText(text: string, richText: boolean) : void {
273
272
  // if (!text || text.length <= 0) return;
274
273
  // if (!text ) return;
274
+ if (debug) console.log("feedText", this.uiObject, text, richText);
275
275
 
276
- if (!this.uiObject) return null;
276
+ if (!this.uiObject) return;
277
277
  if (!this._textMeshUi)
278
278
  this._textMeshUi = [];
279
279
 
@@ -291,29 +291,25 @@
291
291
  } else {
292
292
  let currentTag = this.getNextTag(text);
293
293
  if (!currentTag) {
294
- //@TODO: @swingingtom how would the text content be set?
295
294
  //@ts-ignore
296
- return this.uiObject.textContent = text;
295
+ // we have to set it to empty string, otherwise TMUI won't update it @swingingtom
296
+ this.uiObject.textContent = ""; // <
297
+ this.setOptions({ textContent: text });
298
+ return;
297
299
  } else if (currentTag.startIndex > 0) {
298
-
299
300
  // First segment should also clear children inlines
300
- for ( let i = this.uiObject.children.length - 1 ; i >= 0; i-- ) {
301
- const child = this.uiObject.children[ i ];
302
-
301
+ for (let i = this.uiObject.children.length - 1; i >= 0; i--) {
302
+ const child = this.uiObject.children[i];
303
303
  // @ts-ignore
304
- if( child.isUI ) {
305
-
306
- this.uiObject.remove( child );
304
+ if (child.isUI) {
305
+ this.uiObject.remove(child);
307
306
  child.clear();
308
-
309
307
  }
310
-
311
308
  }
312
-
313
-
314
309
  const el = new ThreeMeshUI.Inline({ textContent: text.substring(0, currentTag.startIndex), color: 'inherit' });
315
310
  this.uiObject.add(el);
316
311
  }
312
+
317
313
  const stackArray: Array<TagStackEntry> = [];
318
314
  while (currentTag) {
319
315
  const next = this.getNextTag(text, currentTag.endIndex);
@@ -325,17 +321,13 @@
325
321
  }
326
322
 
327
323
  if (next) {
328
-
329
324
  opts.textContent = this.getText(text, currentTag, next);
330
-
331
325
  this.handleTag(currentTag, opts, stackArray);
332
326
  const el = new ThreeMeshUI.Inline(opts);
333
327
  this.uiObject?.add(el)
334
328
 
335
329
  } else {
336
-
337
330
  opts.textContent = text.substring(currentTag.endIndex);
338
-
339
331
  this.handleTag(currentTag, opts, stackArray);
340
332
  const el = new ThreeMeshUI.Inline(opts);
341
333
  this.uiObject?.add(el);
@@ -343,10 +335,6 @@
343
335
  currentTag = next;
344
336
  }
345
337
  }
346
-
347
- if(debug) console.log("feedText", this.uiObject);
348
-
349
- return null;
350
338
  }
351
339
 
352
340
  private _didHandleTextRenderOnTop: boolean = false;
@@ -514,7 +502,7 @@
514
502
  // e.g. -Medium, -Black, -Thin...
515
503
  const styleName = familyName.substring(styleSeparator + 1)?.toLowerCase();
516
504
  if (unsupportedStyleNames.includes(styleName)) {
517
- if(debug) console.warn("Unsupported font style: " + styleName);
505
+ if (debug) console.warn("Unsupported font style: " + styleName);
518
506
  return familyName;
519
507
  }
520
508
 
src/engine-components/webxr/WebARCameraBackground.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  ShaderLib,
12
12
  ShaderMaterial,
13
13
  DoubleSide,
14
- PerspectiveCamera
14
+ PerspectiveCamera,
15
15
  } from "three";
16
16
 
17
17
  export class WebARCameraBackground extends Behaviour {
@@ -74,7 +74,6 @@
74
74
  // from https://stackoverflow.com/a/55084367 to inject a custom texture into three.js
75
75
  if (!this.threeTexture && this.context.renderer) {
76
76
  this.threeTexture = new Texture();
77
- // this.threeTexture.encoding = LinearEncoding;
78
77
  this.forceTextureInitialization(this.context.renderer, this.threeTexture);
79
78
  }
80
79
 
@@ -132,6 +131,17 @@
132
131
  this.backgroundPlane.setTexture(this.threeTexture);
133
132
  this.backgroundPlane.visible = true;
134
133
  }
134
+
135
+ // TODO this would be a lot better but currently
136
+ // setting color space doesn't work.
137
+ // Plus we need to understand how we can supply a custom shader in
138
+ // this case.
139
+ /*
140
+ if (this.threeTexture) {
141
+ this.context.scene.background = this.threeTexture;
142
+ this.threeTexture.colorSpace = NoColorSpace;
143
+ }
144
+ */
135
145
  }
136
146
  }
137
147
  else {
plugins/common/config.js ADDED
@@ -0,0 +1,20 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+
3
+ export function getMeta() {
4
+ const workingDirectory = process.cwd();
5
+ const configPath = workingDirectory + "/needle.config.json";
6
+ if(existsSync(configPath)) {
7
+ const configStr = readFileSync(configPath);
8
+ const config = JSON.parse(configStr);
9
+ if(config.codegenDirectory) {
10
+ const dir = workingDirectory + "/" + config.codegenDirectory + "/meta.json";
11
+ if(existsSync(dir)) {
12
+ const metaStr = readFileSync(dir);
13
+ /**@type {import("../types").needleConfig} */
14
+ const meta = JSON.parse(metaStr);
15
+ return meta;
16
+ }
17
+ }
18
+ }
19
+ return null;
20
+ }
plugins/common/generator.js ADDED
@@ -0,0 +1,11 @@
1
+ import { getMeta } from "./config.js";
2
+
3
+
4
+ /** @returns {string|null} */
5
+ export function tryGetGenerator() {
6
+ const meta = getMeta();
7
+ if (meta) {
8
+ return meta.generator;
9
+ }
10
+ return null;
11
+ }
plugins/common/version.js ADDED
@@ -0,0 +1,12 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+
3
+ /** @returns {string|null} */
4
+ export function tryGetNeedleEngineVersion() {
5
+ const needleEnginePackageJsonPath = process.cwd() + "/node_modules/@needle-tools/engine/package.json";
6
+ if (existsSync(needleEnginePackageJsonPath)) {
7
+ const json = JSON.parse(readFileSync(needleEnginePackageJsonPath));
8
+ const version = json.version;
9
+ return version;
10
+ }
11
+ return null;
12
+ }