@needle-tools/engine 2.63.2-pre → 2.63.2-pre.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/engine",
3
- "version": "2.63.2-pre",
3
+ "version": "2.63.2-pre.1",
4
4
  "description": "Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally.",
5
5
  "main": "dist/needle-engine.umd.cjs",
6
6
  "module": "dist/needle-engine.js",
@@ -0,0 +1,20 @@
1
+
2
+ export const needleBuild = (command, config, userSettings) => {
3
+
4
+ // TODO: need to set this when building a dist
5
+ const isDeployOnlyBuild = config?.deployOnly === true;
6
+
7
+ return {
8
+ name: 'build',
9
+ config(config) {
10
+ if (!config.build) {
11
+ config.build = {};
12
+ }
13
+ if (isDeployOnlyBuild)
14
+ {
15
+ console.log("Deploy only build - will not empty output directory")
16
+ config.build.emptyOutDir = false;
17
+ }
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,48 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+
3
+ export async function loadConfig(path) {
4
+ try {
5
+ // First try to get the path from the config
6
+ if (!path) {
7
+ const configJson = tryLoadProjectConfig();
8
+ if (configJson?.codegenDirectory) {
9
+ path = configJson.codegenDirectory + "/meta.json";
10
+ }
11
+ }
12
+ // If that fails, try the default path
13
+ if (!path)
14
+ path = './src/generated/meta.json';
15
+
16
+ if (existsSync(path)) {
17
+ const text = readFileSync(path, 'utf8');
18
+ if (!text) return null;
19
+ return JSON.parse(text);
20
+ }
21
+ else console.error("Could not find config file at " + path);
22
+ return null;
23
+ }
24
+ catch (err) {
25
+ console.error("Error loading config file");
26
+ console.error(err);
27
+ return null;
28
+ }
29
+ }
30
+
31
+ export function tryLoadProjectConfig() {
32
+ try {
33
+ const root = process.cwd();
34
+ const path = root + '/needle.config.json';
35
+ if (existsSync(path)) {
36
+ const text = readFileSync(path);
37
+ if (!text) return null;
38
+ const json = JSON.parse(text);
39
+ return json;
40
+ }
41
+ }
42
+ catch (err) {
43
+ console.error("Error loading config file");
44
+ console.error(err);
45
+ }
46
+
47
+ return null;
48
+ }
@@ -0,0 +1,6 @@
1
+
2
+ // import { existsSync } from 'fs';
3
+
4
+ export function useGzip(config) {
5
+ return config?.gzip ? true : false;
6
+ }
@@ -0,0 +1,23 @@
1
+ import { needleBuild } from "./build.js";
2
+ import { needleMeta } from "./meta.js"
3
+ import { needlePoster } from "./poster.js"
4
+ import { needleReload } from "./reload.js"
5
+
6
+ export * from "./gzip.js";
7
+ export * from "./config.js";
8
+
9
+ const defaultUserSettings = {
10
+ allowRemoveMetaTags: true,
11
+ allowHotReload: true,
12
+ }
13
+
14
+ export const needlePlugins = (command, config, userSettings) => {
15
+ // ensure we have user settings initialized with defaults
16
+ userSettings = { ...defaultUserSettings, ...userSettings }
17
+ return [
18
+ needleMeta(command, config, userSettings),
19
+ needlePoster(command),
20
+ needleReload(command, config, userSettings),
21
+ needleBuild(command, config, userSettings)
22
+ ]
23
+ }
@@ -0,0 +1,128 @@
1
+ import { loadConfig } from './config.js';
2
+ import fs from 'fs';
3
+ import { getPosterPath } from './poster.js';
4
+
5
+ export const needleMeta = (command, config, userSettings) => {
6
+
7
+ // we can check if this is a build
8
+ // const isBuild = command === 'build';
9
+
10
+ async function updateConfig() {
11
+ config = await loadConfig();
12
+ }
13
+
14
+ if (!userSettings) userSettings = {};
15
+
16
+ return {
17
+ // replace meta tags
18
+ name: 'meta-tags',
19
+ transformIndexHtml: {
20
+ enforce: 'pre',
21
+ transform(html, _ctx) {
22
+
23
+ html = insertNeedleCredits(html);
24
+
25
+ if (userSettings.allowMetaPlugin === false) return [];
26
+
27
+ // this is useful to get the latest config exported from editor
28
+ // whenever vite wants to transform the html
29
+ updateConfig();
30
+
31
+ // early out of the config is invalid / doesn't contain meta information
32
+ // TODO might be better to handle these edge cases / special cases right from Unity/Blender and not here
33
+ if (!config) return [];
34
+
35
+ let meta = config.meta;
36
+
37
+ if (!meta) {
38
+ meta = {};
39
+ meta.title = config.sceneName;
40
+ }
41
+
42
+ if (!meta.image?.length) {
43
+ const path = getPosterPath();
44
+ if (fs.existsSync('./' + path))
45
+ meta.image = path;
46
+ }
47
+
48
+ const tags = [];
49
+
50
+ let img = meta.image;
51
+ if (img?.length) {
52
+ // for a regular build the absolutePath is url (since we dont know the deployment target)
53
+ if (config.absolutePath?.length) {
54
+ const baseUrl = config.absolutePath;
55
+ let url = baseUrl + "/" + img;
56
+ url = removeDuplicateSlashesInUrl(url);
57
+ // url = appendVersion(url);
58
+ tags.push({ tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' } });
59
+ tags.push({ tag: 'meta', attrs: { name: 'twitter:image', content: url } });
60
+ tags.push({ tag: 'meta', attrs: { name: 'og:image', content: url } });
61
+ }
62
+ }
63
+
64
+ if (config.absolutePath?.length) {
65
+ html = updateUrlMetaTag(html, config.absolutePath);
66
+ }
67
+
68
+ if (meta.title?.length) {
69
+ tags.push({ tag: 'meta', attrs: { name: 'og:title', content: meta.title } });
70
+
71
+ html = html.replace(
72
+ /<title>(.*?)<\/title>/,
73
+ `<title>${meta.title}</title>`,
74
+ );
75
+
76
+ if (userSettings.allowRemoveMetaTags !== false)
77
+ html = removeMetaTag(html, 'og:title');
78
+
79
+ }
80
+
81
+ if (meta.description?.length) {
82
+ tags.push({ tag: 'meta', attrs: { name: 'description', content: meta.description } });
83
+ tags.push({ tag: 'meta', attrs: { name: 'og:description', content: meta.description } });
84
+
85
+ if (userSettings.allowRemoveMetaTags !== false) {
86
+ html = removeMetaTag(html, 'description');
87
+ html = removeMetaTag(html, 'og:description');
88
+ }
89
+ }
90
+
91
+ return { html, tags }
92
+ },
93
+ }
94
+ }
95
+ }
96
+
97
+ function updateUrlMetaTag(html, url) {
98
+ html = html.replace(`<meta name="url" content="http://needle.tools">`, `<meta name="url" content="${url}">`);
99
+ return html;
100
+ }
101
+
102
+ function appendVersion(str) {
103
+ return str + "?v=" + Date.now();
104
+ }
105
+
106
+ function removeDuplicateSlashesInUrl(url) {
107
+ return url.replace(/([^:]\/)\/+/g, "$1");
108
+ }
109
+
110
+ function removeMetaTag(html, name) {
111
+ // TODO: maybe we could also just replace the content
112
+ const regex = new RegExp(`<meta (name|property)="${name}".+?\/?>`, 'gs');
113
+ const newHtml = html.replace(
114
+ regex,
115
+ '',
116
+ );
117
+ // console.log(newHtml);
118
+ return newHtml;
119
+ }
120
+
121
+ function insertNeedleCredits(html) {
122
+ const needleCredits = `<!-- 🌵 Made with Needle — https://needle.tools -->`;
123
+ html = html.replace(
124
+ /<head>/,
125
+ needleCredits + "\n<head>",
126
+ );
127
+ return html;
128
+ }
@@ -0,0 +1,60 @@
1
+
2
+
3
+ async function generatePoster() {
4
+ const { screenshot } = await import("@needle-tools/engine/engine/engine_utils_screenshot");
5
+
6
+ try {
7
+ const needleEngine = document.querySelector("needle-engine");
8
+ if (!needleEngine) return null;
9
+
10
+ const width = 1920;
11
+ const height = 1920;
12
+ const context = await needleEngine.getContext();
13
+
14
+ // wait until a few update loops have run
15
+ while (context.time.frameCount < 3) {
16
+ await new Promise((resolve) => setTimeout(resolve, 50));
17
+ }
18
+
19
+ const mimeType = "image/webp";
20
+ console.log("Generating poster...");
21
+ const dataUrl = screenshot(context, width, height, mimeType);
22
+
23
+ return dataUrl;
24
+ }
25
+ catch (e) {
26
+ console.error(e);
27
+ return null;
28
+ }
29
+ }
30
+
31
+ async function sendPoster() {
32
+ const blob = await generatePoster();
33
+ import.meta.hot.send("needle:screenshot", { data: blob });
34
+ }
35
+
36
+ // communicate via vite
37
+ if (import.meta.hot) {
38
+ // wait for needle engine to be fully loaded
39
+ const needleEngine = document.querySelector("needle-engine");
40
+ needleEngine?.addEventListener("loadfinished", () => {
41
+ // wait a moment
42
+ setTimeout(() => {
43
+ sendPoster();
44
+ }, 200);
45
+ });
46
+
47
+ // for debugging: build extra button with dev-only options
48
+ /*
49
+ var button = document.createElement("button");
50
+ button.id = "send-msg";
51
+ button.innerHTML = "Generate Poster";
52
+ button.style.position = "fixed";
53
+ button.style.zIndex = "9999";
54
+ document.body.appendChild(button);
55
+
56
+ document.querySelector("#send-msg").addEventListener("click", async () => {
57
+ sendPoster();
58
+ });
59
+ */
60
+ }
@@ -0,0 +1,55 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ export function getPosterPath() {
9
+ return "include/poster.webp";
10
+ }
11
+
12
+ export const needlePoster = (command) => {
13
+ // only relevant for local development
14
+ if (command === 'build') return [];
15
+
16
+ return {
17
+ name: 'save-screenshot',
18
+ configureServer(server) {
19
+ server.ws.on('needle:screenshot', async (data, client) => {
20
+ if(!data?.data){
21
+ console.warn("Received empty screenshot data, ignoring");
22
+ return;
23
+ }
24
+ const targetPath = "./" + getPosterPath();
25
+ console.log("Received poster, saving to " + targetPath);
26
+ // remove data:image/png;base64, from the beginning of the string
27
+ if (targetPath.endsWith(".webp"))
28
+ data.data = data.data.replace(/^data:image\/webp;base64,/, "");
29
+ else
30
+ data.data = data.data.replace(/^data:image\/png;base64,/, "");
31
+ const dir = path.dirname(targetPath);
32
+ if (!fs.existsSync(dir)) {
33
+ fs.mkdirSync(dir, { recursive: true })
34
+ }
35
+ fs.writeFileSync(targetPath, Buffer.from(data.data, "base64"));
36
+ });
37
+ },
38
+ transformIndexHtml: {
39
+ enforce: 'pre',
40
+ transform(html, ctx) {
41
+ const file = path.join(__dirname, 'poster-client.js');
42
+ return [
43
+ {
44
+ tag: 'script',
45
+ attrs: {
46
+ type: 'module',
47
+ },
48
+ children: fs.readFileSync(file, 'utf8'),
49
+ injectTo: 'body',
50
+ },
51
+ ];
52
+ },
53
+ },
54
+ }
55
+ };
@@ -0,0 +1,16 @@
1
+ import { showBalloonMessage } from "@needle-tools/engine";
2
+
3
+ try {
4
+ // communicate via vite
5
+ if (import.meta.hot) {
6
+ // listen to needle-reload event
7
+ import.meta.hot.on('needle:reload', (evt) => {
8
+ console.log("Received reload event");
9
+ showBalloonMessage("Detected files changing\npage will reload in a moment");
10
+ });
11
+
12
+ }
13
+ }
14
+ catch {
15
+ // ignore
16
+ }
@@ -0,0 +1,342 @@
1
+ import path from 'path';
2
+ import { loadConfig, tryLoadProjectConfig } from './config.js';
3
+ import { getPosterPath } from './poster.js';
4
+ import * as crypto from 'crypto';
5
+ import { existsSync, readFileSync, statSync } from 'fs';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ const filesUsingHotReload = new Set();
12
+
13
+ export const needleReload = (command, config, userSettings) => {
14
+ if (command === "build") return;
15
+
16
+
17
+ let isUpdatingConfig = false;
18
+ const updateConfig = async () => {
19
+ if (isUpdatingConfig) return;
20
+ isUpdatingConfig = true;
21
+ const res = await loadConfig();
22
+ isUpdatingConfig = false;
23
+ if (res) config = res;
24
+ }
25
+
26
+
27
+ const projectConfig = tryLoadProjectConfig();
28
+ const buildDirectory = projectConfig?.buildDirectory?.length ? process.cwd().replaceAll("\\", "/") + "/" + projectConfig?.buildDirectory : "";
29
+ if (buildDirectory?.length) {
30
+ setTimeout(() => console.log("Build directory: ", buildDirectory), 100);
31
+ }
32
+
33
+ // These ignore patterns will be injected into user config to better control vite reloading
34
+ const ignorePatterns = ["dist/**/*", "src/generated/*", "**/package~/**/codegen/**/*", "**/codegen/register_types.js"];
35
+ if (projectConfig?.buildDirectory?.length) ignorePatterns.push(`${projectConfig?.buildDirectory}/**/*`);
36
+ if (projectConfig?.codegenDirectory?.length) ignorePatterns.push(`${projectConfig?.codegenDirectory}/**/*`);
37
+
38
+ return {
39
+ name: 'reload',
40
+ config(config) {
41
+ if (!config.server) config.server = { watch: { ignored: [] } };
42
+ else if (!config.server.watch) config.server.watch = { ignored: [] };
43
+ else if (!config.server.watch.ignored) config.server.watch.ignored = [];
44
+ for (const pattern of ignorePatterns)
45
+ config.server.watch.ignored.push(pattern);
46
+ setTimeout(() => console.log("Updated server ignore patterns: ", config.server.watch.ignored), 100);
47
+ },
48
+ handleHotUpdate(args) {
49
+ args.buildDirectory = buildDirectory;
50
+ return handleReload(args);
51
+ },
52
+ transform(src, id) {
53
+ if (!id.includes(".ts")) return;
54
+ updateConfig();
55
+ if (config?.allowHotReload === false) return;
56
+ if (userSettings?.allowHotReload === false) return;
57
+ src = insertScriptRegisterHotReloadCode(src, id);
58
+ return insertScriptHotReloadCode(src, id);
59
+ },
60
+ transformIndexHtml: {
61
+ enforce: 'pre',
62
+ transform(html, _) {
63
+ if (config?.allowHotReload === false) return [html];
64
+ if (userSettings?.allowHotReload === false) return [html];
65
+ const file = path.join(__dirname, 'reload-client.js');
66
+ return [
67
+ {
68
+ tag: 'script',
69
+ attrs: {
70
+ type: 'module',
71
+ },
72
+ children: readFileSync(file, 'utf8'),
73
+ injectTo: 'body',
74
+ },
75
+ ];
76
+ },
77
+ },
78
+ };
79
+ }
80
+
81
+
82
+ const ignorePatterns = [];
83
+ const ignoreRegex = new RegExp(ignorePatterns.join("|"));
84
+
85
+ let lastReloadTime = 0;
86
+ const posterPath = getPosterPath();
87
+ let reloadIsScheduled = false;
88
+ const lockFileName = "needle.lock";
89
+
90
+ function notifyClientWillReload(server, file) {
91
+ console.log("Send reload notification");
92
+ server.ws.send('needle:reload', { type: 'will-reload', file: file });
93
+ }
94
+
95
+ async function handleReload({ file, server, modules, read, buildDirectory }) {
96
+
97
+ // dont reload the full page on css changes
98
+ const isCss = file.endsWith(".css") || file.endsWith(".scss") || file.endsWith(".sass")
99
+ if (isCss) return;
100
+
101
+ // Dont reload the whole server when a file that is using hot reload changes
102
+ if (filesUsingHotReload.has(file)) {
103
+ console.log("File is using hot reload: " + file);
104
+ return;
105
+ }
106
+
107
+ // the poster is generated after the server has started
108
+ // we dont want to specifically handle the png or webp that gets generated
109
+ if (file.includes(posterPath)) return;
110
+
111
+ if (file.endsWith("build.log")) return;
112
+
113
+ // if (file.endsWith("/codegen/register_types.js" || file.endsWith("/generated/register_types.js"))) {
114
+ // console.log("Ignore change in codegen file: " + file);
115
+ // return [];
116
+ // }
117
+
118
+ // This was a test for ignoring files via regex patterns
119
+ // instead of relying on the vite server watch ignore array
120
+ // we could here also match paths that we know we dont want to track
121
+ if (ignorePatterns.length > 0 && ignoreRegex.test(file)) {
122
+ console.log("Ignore change in file: " + getFileNameLog(file));
123
+ return [];
124
+ }
125
+
126
+ // Ignore files changing in output directory
127
+ // this happens during build time when e.g. dist is not ignored in vite config
128
+ // we still dont want to reload the local server in that case
129
+ if (buildDirectory?.length) {
130
+ const dir = path.dirname(file).replaceAll("\\", "/");
131
+ if (dir.startsWith(buildDirectory)) {
132
+ console.log("Ignore change in build directory: " + getFileNameLog(file));
133
+ return [];
134
+ }
135
+ }
136
+
137
+ // Check if codegen files actually changed their content
138
+ // this will return false if its the first update
139
+ // meaning if its the first export after the server starts those will not trigger a reload
140
+ const shouldCheckIfContentChanged = file.includes("/codegen/") || file.includes("/generated/") || file.endsWith("gen.js");// || file.endsWith(".glb") || file.endsWith(".gltf") || file.endsWith(".bin");
141
+ if (shouldCheckIfContentChanged) {
142
+ if (reloadIsScheduled) {
143
+ return [];
144
+ }
145
+ if (await testIfFileContentChanged(file, read) === false) {
146
+ console.log("File content didnt change: " + getFileNameLog(file));
147
+ return [];
148
+ }
149
+ }
150
+
151
+ if (file.endsWith(".vue") || file.endsWith(".ts") || file.endsWith(".js") || file.endsWith(".jsx") || file.endsWith(".tsx"))
152
+ return;
153
+
154
+ if (file.endsWith(lockFileName)) return;
155
+ let fileSize = "";
156
+ const isGlbOrGltfFile = file.endsWith(".glb") || file.endsWith(".bin");
157
+ if (isGlbOrGltfFile && existsSync(file)) {
158
+ fileSize = statSync(file).size;
159
+ // the file is about to be created/written to
160
+ if (fileSize <= 0) {
161
+ // console.log("> File is changing: " + getFileNameLog(file));
162
+ return;
163
+ }
164
+ }
165
+
166
+ console.log("> Detected file change: ", getFileNameLog(file) + " (" + ((fileSize / (1024 * 1024)).toFixed(1)) + " MB)");
167
+ notifyClientWillReload(server);
168
+ scheduleReload(server);
169
+ return [];
170
+ }
171
+
172
+
173
+ async function scheduleReload(server, level = 0) {
174
+ if (reloadIsScheduled && level === 0) return;
175
+ reloadIsScheduled = true;
176
+
177
+ const lockFile = path.join(process.cwd(), lockFileName);
178
+ if (existsSync(lockFile)) {
179
+ if (level === 0)
180
+ console.log("Lock file exists, waiting for export to finish...");
181
+ setTimeout(() => scheduleReload(server, level += 1), 300);
182
+ return;
183
+ }
184
+
185
+ reloadIsScheduled = false;
186
+
187
+ const timeDiff = Date.now() - lastReloadTime;
188
+ if (timeDiff < 1000) {
189
+ // Sometimes file changes happen immediately after triggering a reload
190
+ // we dont want to reload again in that case
191
+ console.log("Ignoring reload, last reload was too recent", timeDiff);
192
+ return;
193
+ }
194
+
195
+ lastReloadTime = Date.now();
196
+ const readableTime = new Date(lastReloadTime).toLocaleTimeString();
197
+ console.log("< Reloading... " + readableTime)
198
+ server.ws.send({
199
+ type: 'full-reload',
200
+ path: '*'
201
+ });
202
+ }
203
+
204
+ const projectDirectory = process.cwd().replaceAll("\\", "/");
205
+
206
+ function getFileNameLog(file) {
207
+ if (file.startsWith(projectDirectory)) {
208
+ return file.substring(projectDirectory.length);
209
+ }
210
+ return file;
211
+ }
212
+
213
+ const hashes = new Map();
214
+ const hash256 = crypto.createHash('sha256');
215
+
216
+ async function testIfFileContentChanged(file, read) {
217
+ let content = await read(file);
218
+ content = removeVersionQueryArgument(content);
219
+
220
+ const hash = hash256.copy();
221
+ hash.update(content);
222
+ // compare if hash string changed
223
+ const newHash = hash.digest('hex');
224
+ const oldHash = hashes.get(file);
225
+ if (oldHash !== newHash) {
226
+ // console.log("Update hash for file: " + getFileNameLog(file) + " to " + newHash);
227
+ hashes.set(file, newHash);
228
+ // if its the first update we dont want to trigger a reload
229
+ if (!oldHash) return false;
230
+ return true;
231
+ }
232
+ return false;
233
+ }
234
+ function removeVersionQueryArgument(content) {
235
+ if (typeof content === "string") {
236
+ // Some codegen files include hashes for loading glb files (e.g. ?v=213213124)
237
+ // Or context.hash = "54543453"
238
+ // We dont want to use those hashes for detecting if the file changed
239
+ let res = content.replaceAll(/(v=[0-9]+)/g, "");
240
+ res = res.replaceAll(/(hash = \"[0-9]+\")/g, "hash = \"\"");
241
+ return res;
242
+ }
243
+ return content;
244
+ }
245
+
246
+
247
+
248
+ function insertScriptRegisterHotReloadCode(src, filePath) {
249
+ if (!filePath.includes("needle-engine.ts")) {
250
+ return src;
251
+ }
252
+
253
+ // this code injects a register call into the component method
254
+ const code = `
255
+
256
+ import { register, unregister } from "./engine/engine_hot_reload";
257
+ import { Component as ComponentType } from "./engine-components/Component";
258
+
259
+ const prototype = ComponentType.prototype;
260
+ const created = prototype.__internalNewInstanceCreated;
261
+ prototype.__internalNewInstanceCreated = function (...args) {
262
+ created.call(this, ...args);
263
+ register(this);
264
+ }
265
+ const destroy = prototype.__internalDestroy;
266
+ prototype.__internalDestroy = function (...args) {
267
+ destroy.call(this, ...args);
268
+ unregister(this);
269
+ }
270
+ `
271
+ return code + src;
272
+ }
273
+
274
+
275
+ const HOT_RELOAD_START_MARKER = "NEEDLE_HOT_RELOAD_BEGIN";
276
+ const HOT_RELOAD_END_MARKER = "NEEDLE_HOT_RELOAD_END";
277
+
278
+ function insertScriptHotReloadCode(src, filePath) {
279
+ if (filePath.includes("engine_hot_reload")) return;
280
+ if (filePath.includes(".vite")) return;
281
+
282
+ const originalFilePath = filePath;
283
+
284
+ // default import path when outside package
285
+ let importPath = "@needle-tools/engine/engine/engine_hot_reload";
286
+
287
+ if (filePath.includes("package~/engine")) {
288
+ // convert local dev path to project node_modules path
289
+ const folderName = "package~";
290
+ const startIndex = filePath.indexOf(folderName);
291
+ const newPath = process.cwd() + "/node_modules/@needle-tools/engine" + filePath.substring(startIndex + folderName.length);
292
+ filePath = newPath;
293
+ }
294
+
295
+ if (filePath.includes("@needle-tools/engine")) {
296
+ // only make engine components hot reloadable
297
+ if (!filePath.includes("engine/engine-components"))
298
+ return;
299
+ // make import path from engine package
300
+ const fullPathToHotReload = process.cwd() + "/node_modules/@needle-tools/engine/engine/engine_hot_reload.ts";
301
+ // console.log(fullPathToHotReload);
302
+ const fileDirectory = path.dirname(filePath);
303
+ // console.log("DIR", fileDirectory)
304
+ const relativePath = path.relative(fileDirectory, fullPathToHotReload);
305
+ importPath = relativePath.replace(/\\/g, "/");
306
+ // console.log("importPath: ", importPath);
307
+ }
308
+
309
+ // console.log(importPath, ">", filePath);
310
+
311
+
312
+ const code = `
313
+
314
+ // ${HOT_RELOAD_START_MARKER}
315
+ // Inserted by needle reload plugin (vite)
316
+ import { applyChanges } from "${importPath}";
317
+ //@ts-ignore
318
+ if (import.meta.hot) {
319
+ //@ts-ignore
320
+ import.meta.hot.accept((newModule) => {
321
+ if (newModule) {
322
+
323
+ const success = applyChanges(newModule);
324
+ if(success === false){
325
+ //@ts-ignore
326
+ import.meta.hot.invalidate()
327
+ }
328
+ }
329
+ })
330
+ }
331
+ // ${HOT_RELOAD_END_MARKER}
332
+ `
333
+
334
+ if (!filesUsingHotReload.has(originalFilePath))
335
+ filesUsingHotReload.add(originalFilePath);
336
+
337
+ return {
338
+ code: code + src,
339
+ map: null
340
+ }
341
+
342
+ }