@needle-tools/engine 2.63.2-pre → 2.63.3-pre
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/CHANGELOG.md +3 -0
- package/package.json +1 -1
- package/src/plugins/vite/build.js +20 -0
- package/src/plugins/vite/config.js +48 -0
- package/src/plugins/vite/gzip.js +6 -0
- package/src/plugins/vite/index.js +23 -0
- package/src/plugins/vite/meta.js +128 -0
- package/src/plugins/vite/poster-client.js +60 -0
- package/src/plugins/vite/poster.js +55 -0
- package/src/plugins/vite/reload-client.js +16 -0
- package/src/plugins/vite/reload.js +342 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,9 @@ All notable changes to this package will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [2.63.3-pre] - 2023-03-03
|
|
8
|
+
- Fix: engine published to npm was missing vite plugins
|
|
9
|
+
|
|
7
10
|
## [2.63.2-pre] - 2023-03-03
|
|
8
11
|
- Fix: license styling in some cases
|
|
9
12
|
- Fix: duplicatable + draggable issue causing drag to not release the object (due to wrong event handling)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/engine",
|
|
3
|
-
"version": "2.63.
|
|
3
|
+
"version": "2.63.3-pre",
|
|
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,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
|
+
}
|