@needle-tools/engine 5.1.0-alpha.8 → 5.1.0-beta
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 +17 -0
- package/components.needle.json +1 -1
- package/dist/{materialx-CE2sUv2B.umd.cjs → materialx-B47Bz-xs.umd.cjs} +1 -1
- package/dist/{materialx-u1EqYrhu.min.js → materialx-B85WjP7E.min.js} +1 -1
- package/dist/{materialx-vyB2Zbt4.js → materialx-jkHmVPez.js} +1 -1
- package/dist/{needle-engine.bundle-3LOy9tj8.js → needle-engine.bundle-BWkDSRyF.js} +6248 -6001
- package/dist/{needle-engine.bundle-BuI2pjv1.min.js → needle-engine.bundle-BvgS8xMP.min.js} +171 -171
- package/dist/{needle-engine.bundle-DQy9vQdJ.umd.cjs → needle-engine.bundle-CdVr_iIe.umd.cjs} +142 -142
- package/dist/needle-engine.d.ts +359 -46
- package/dist/needle-engine.js +618 -606
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/{vendor-ButPLzor.umd.cjs → vendor-1UvpPPSB.umd.cjs} +2 -2
- package/dist/{vendor-DqZC4Is7.min.js → vendor-BQ2Vuntm.min.js} +4 -4
- package/dist/{vendor-DkWSNjMV.js → vendor-BslSKZPo.js} +113 -106
- package/lib/engine/api.d.ts +1 -1
- package/lib/engine/api.js +1 -1
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_audio.d.ts +30 -1
- package/lib/engine/engine_audio.js +57 -13
- package/lib/engine/engine_audio.js.map +1 -1
- package/lib/engine/engine_init.js +2 -2
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_license.d.ts +7 -7
- package/lib/engine/engine_license.js +72 -72
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_math.d.ts +1 -1
- package/lib/engine/engine_math.js +10 -4
- package/lib/engine/engine_math.js.map +1 -1
- package/lib/engine/engine_networking_blob.js +3 -3
- package/lib/engine/engine_networking_blob.js.map +1 -1
- package/lib/engine/engine_scenedata.js +7 -0
- package/lib/engine/engine_scenedata.js.map +1 -1
- package/lib/engine/engine_serialization_core.js +7 -2
- package/lib/engine/engine_serialization_core.js.map +1 -1
- package/lib/engine/engine_time_utils.d.ts +10 -6
- package/lib/engine/engine_time_utils.js +10 -6
- package/lib/engine/engine_time_utils.js.map +1 -1
- package/lib/engine/engine_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_qrcode.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
- package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +2 -2
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/TempXRContext.js +2 -2
- package/lib/engine/xr/TempXRContext.js.map +1 -1
- package/lib/engine-components/AudioSource.d.ts +46 -2
- package/lib/engine-components/AudioSource.js +268 -60
- package/lib/engine-components/AudioSource.js.map +1 -1
- package/lib/engine-components/Component.js +2 -1
- package/lib/engine-components/Component.js.map +1 -1
- package/lib/engine-components/DragControls.js +2 -2
- package/lib/engine-components/DragControls.js.map +1 -1
- package/lib/engine-components/DragControlsConstraints.d.ts +0 -15
- package/lib/engine-components/DragControlsConstraints.js +14 -8
- package/lib/engine-components/DragControlsConstraints.js.map +1 -1
- package/lib/engine-components/EventList.js +1 -1
- package/lib/engine-components/EventList.js.map +1 -1
- package/lib/engine-components/SceneSwitcher.d.ts +2 -0
- package/lib/engine-components/SceneSwitcher.js +43 -16
- package/lib/engine-components/SceneSwitcher.js.map +1 -1
- package/lib/engine-components/SeeThrough.d.ts +39 -0
- package/lib/engine-components/SeeThrough.js +69 -1
- package/lib/engine-components/SeeThrough.js.map +1 -1
- package/lib/engine-components/api.d.ts +1 -0
- package/lib/engine-components/api.js +1 -0
- package/lib/engine-components/api.js.map +1 -1
- package/lib/engine-components/codegen/components.d.ts +10 -0
- package/lib/engine-components/codegen/components.js +10 -0
- package/lib/engine-components/codegen/components.js.map +1 -1
- package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
- package/lib/engine-components/timeline/PlayableDirector.js +1 -1
- package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
- package/lib/engine-components/web/ScrollFollow.d.ts +33 -0
- package/lib/engine-components/web/ScrollFollow.js +164 -22
- package/lib/engine-components/web/ScrollFollow.js.map +1 -1
- package/lib/needle-app.d.ts +18 -0
- package/lib/needle-app.js +19 -0
- package/lib/needle-app.js.map +1 -0
- package/package.json +3 -2
- package/plugins/common/license.js +25 -4
- package/plugins/common/needle-engine.js +5 -2
- package/plugins/types/userconfig.d.ts +22 -0
- package/plugins/vite/dts-generator.d.ts +24 -0
- package/plugins/vite/dts-generator.js +40 -7
- package/plugins/vite/index.d.ts +2 -0
- package/plugins/vite/index.js +17 -0
- package/plugins/vite/license.js +4 -4
- package/plugins/vite/needle-app.js +78 -5
- package/plugins/vite/remote.d.ts +28 -0
- package/plugins/vite/remote.js +124 -0
- package/src/engine/api.ts +1 -1
- package/src/engine/engine_audio.ts +68 -15
- package/src/engine/engine_gameobject.ts +1 -1
- package/src/engine/engine_init.ts +2 -2
- package/src/engine/engine_license.ts +69 -69
- package/src/engine/engine_math.ts +11 -4
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_scenedata.ts +7 -2
- package/src/engine/engine_serialization_core.ts +7 -2
- package/src/engine/engine_time_utils.ts +10 -6
- package/src/engine/engine_utils_qrcode.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
- package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
- package/src/engine/webcomponents/needle-engine.ts +2 -2
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine-components/AudioSource.ts +256 -55
- package/src/engine-components/Component.ts +2 -1
- package/src/engine-components/DragControls.ts +3 -2
- package/src/engine-components/DragControlsConstraints.ts +16 -8
- package/src/engine-components/EventList.ts +1 -1
- package/src/engine-components/SceneSwitcher.ts +37 -15
- package/src/engine-components/SeeThrough.ts +75 -1
- package/src/engine-components/api.ts +1 -0
- package/src/engine-components/codegen/components.ts +10 -0
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
- package/src/engine-components/timeline/PlayableDirector.ts +1 -1
- package/src/engine-components/web/ScrollFollow.ts +168 -22
|
@@ -111,7 +111,7 @@ async function requestNeedleCloud(path, options) {
|
|
|
111
111
|
* @param {DefaultOptions & {accessToken?:string, team:string|undefined}} opts
|
|
112
112
|
*/
|
|
113
113
|
export async function replaceLicense(code, opts) {
|
|
114
|
-
const index = code?.indexOf("
|
|
114
|
+
const index = code?.indexOf("$rJboqN");
|
|
115
115
|
if (index >= 0) {
|
|
116
116
|
const licenseResult = await resolveLicense(opts);
|
|
117
117
|
if (!licenseResult) {
|
|
@@ -120,18 +120,18 @@ export async function replaceLicense(code, opts) {
|
|
|
120
120
|
const end = code.indexOf(";", index);
|
|
121
121
|
if (end >= 0) {
|
|
122
122
|
const line = code.substring(index, end);
|
|
123
|
-
const replaced = "
|
|
123
|
+
const replaced = "$rJboqN = \"" + licenseResult.type + "\"";
|
|
124
124
|
code = code.replace(line, replaced);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// Also inject JWT if available
|
|
128
128
|
if (licenseResult.jwt) {
|
|
129
|
-
const jwtIndex = code.indexOf("_$
|
|
129
|
+
const jwtIndex = code.indexOf("_$RRhFut");
|
|
130
130
|
if (jwtIndex >= 0) {
|
|
131
131
|
const jwtEnd = code.indexOf(";", jwtIndex);
|
|
132
132
|
if (jwtEnd >= 0) {
|
|
133
133
|
const jwtLine = code.substring(jwtIndex, jwtEnd);
|
|
134
|
-
const jwtReplaced = "_$
|
|
134
|
+
const jwtReplaced = "_$RRhFut = \"" + licenseResult.jwt + "\"";
|
|
135
135
|
code = code.replace(jwtLine, jwtReplaced);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -318,11 +318,32 @@ function tryParseLicense(str, options = undefined) {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
|
|
321
|
+
/** @type {Map<string, Promise<string | null>>} */
|
|
322
|
+
const _publicIdentifierCache = new Map();
|
|
323
|
+
|
|
321
324
|
/**
|
|
325
|
+
* Memoized public project identifier. Several plugins (defines, needle-app, next) request this per build,
|
|
326
|
+
* but it only needs to be resolved once - cache the promise by project_id to avoid redundant Needle Cloud
|
|
327
|
+
* requests. Keyed by project_id only; `opts` only controls logging and does not affect the result.
|
|
322
328
|
* @param {string | undefined} project_id
|
|
323
329
|
* @param {DefaultOptions | undefined} opts
|
|
330
|
+
* @returns {Promise<string | null>}
|
|
324
331
|
*/
|
|
325
332
|
export async function getPublicIdentifier(project_id, opts = undefined) {
|
|
333
|
+
const key = project_id ?? "";
|
|
334
|
+
let cached = _publicIdentifierCache.get(key);
|
|
335
|
+
if (!cached) {
|
|
336
|
+
cached = getPublicIdentifierUncached(project_id, opts);
|
|
337
|
+
_publicIdentifierCache.set(key, cached);
|
|
338
|
+
}
|
|
339
|
+
return cached;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @param {string | undefined} project_id
|
|
344
|
+
* @param {DefaultOptions | undefined} opts
|
|
345
|
+
*/
|
|
346
|
+
async function getPublicIdentifierUncached(project_id, opts = undefined) {
|
|
326
347
|
|
|
327
348
|
let accessToken = /** @type {string | undefined} */ (undefined);
|
|
328
349
|
if (!accessToken) {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// Matches the `src` attribute of a <needle-engine> tag.
|
|
4
|
+
// The value capture must accept any real src: relative paths (with dots, slashes, spaces),
|
|
5
|
+
// full URLs, query strings, and the JSON-array form `["a.glb","b.glb"]`. It must also
|
|
6
|
+
// tolerate other attributes appearing before or after `src`.
|
|
4
7
|
// @ts-ignore
|
|
5
|
-
const needleEngineRegex = /<needle-engine
|
|
8
|
+
const needleEngineRegex = /<needle-engine\b[^>]*?\ssrc=(?<q>["'])(?<src>.*?)\k<q>/gms;
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
/**
|
|
@@ -175,6 +175,28 @@ export type userSettings = {
|
|
|
175
175
|
*/
|
|
176
176
|
openBrowser?: boolean;
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Enable remote debugging: bind the dev server to your local network (`host`)
|
|
180
|
+
* and serve over HTTPS (via `@vitejs/plugin-basic-ssl`) so you can open the page
|
|
181
|
+
* on a phone, Quest or Apple Vision Pro on the same Wi‑Fi. The Needle logger
|
|
182
|
+
* already streams the device's console, errors and device info back over Vite's
|
|
183
|
+
* websocket (see `node_modules/.needle/logs/latest.client.needle.log`), so this
|
|
184
|
+
* makes "debug a scene on a real device from your laptop" work out of the box.
|
|
185
|
+
* HTTPS is required for WebXR / secure‑context features on the device; the
|
|
186
|
+
* self‑signed certificate shows a one‑time warning you accept on the device.
|
|
187
|
+
*
|
|
188
|
+
* Requires `@vitejs/plugin-basic-ssl` (for HTTPS) and, optionally,
|
|
189
|
+
* `qrcode-terminal` (to print a scannable QR code). Can also be toggled with the
|
|
190
|
+
* `NEEDLE_REMOTE` env var (`NEEDLE_REMOTE=1` / `=false`).
|
|
191
|
+
* - `true` — enable with a terminal QR code
|
|
192
|
+
* - `{ qr: false }` — enable but don't print the QR code
|
|
193
|
+
* @default false
|
|
194
|
+
*/
|
|
195
|
+
remote?: boolean | {
|
|
196
|
+
/** Set to false to skip printing the scannable QR code. @default true */
|
|
197
|
+
qr?: boolean;
|
|
198
|
+
};
|
|
199
|
+
|
|
178
200
|
disableLogging?: boolean;
|
|
179
201
|
|
|
180
202
|
/** Set to true to disable the plugin that ensures VSCode workspace settings for custom-elements.json data */
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper: compute the desired `html.customData` array for a project.
|
|
3
|
+
*
|
|
4
|
+
* Returns `null` when no change is required (the existing array already matches),
|
|
5
|
+
* otherwise the new array that should be written.
|
|
6
|
+
*
|
|
7
|
+
* The produced entry is a project-relative path with forward slashes. We use
|
|
8
|
+
* `path.relative` rather than a string replace because Vite normalises projectRoot
|
|
9
|
+
* to forward slashes while `htmlDataPath` carries the platform separator from
|
|
10
|
+
* `path.join` — a naive replace fails to match on Windows and leaves an absolute path.
|
|
11
|
+
* Any pre-existing entry pointing at a `needle-html-data.json` (e.g. an absolute path
|
|
12
|
+
* written by an older version of this plugin) is dropped so we don't accumulate
|
|
13
|
+
* duplicates or leave stale absolute paths behind.
|
|
14
|
+
*
|
|
15
|
+
* Exported for testing. The `relativeFn` seam lets tests reproduce platform-specific
|
|
16
|
+
* separator behaviour (`path.win32.relative` / `path.posix.relative`) deterministically.
|
|
17
|
+
*
|
|
18
|
+
* @param {unknown[]} existing Current html.customData entries
|
|
19
|
+
* @param {string} projectRoot
|
|
20
|
+
* @param {string} htmlDataPath Absolute path to the generated needle-html-data.json
|
|
21
|
+
* @param {(from: string, to: string) => string} [relativeFn]
|
|
22
|
+
* @returns {string[] | null}
|
|
23
|
+
*/
|
|
24
|
+
export function computeHtmlCustomData(existing: unknown[], projectRoot: string, htmlDataPath: string, relativeFn?: (from: string, to: string) => string): string[] | null;
|
|
1
25
|
/**
|
|
2
26
|
* @param {"build" | "serve"} _command Vite command (unused — runs in both modes)
|
|
3
27
|
* @param {import('../types/needleConfig').needleMeta | null | undefined} _config
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* needleDtsGenerator(command, needleConfig, userSettings)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { join, resolve, dirname } from 'path';
|
|
19
|
+
import { join, resolve, dirname, relative } from 'path';
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync } from 'fs';
|
|
21
21
|
import { tryLoadProjectConfig } from './config.js';
|
|
22
22
|
import { generateBindingsDts } from '../dts-generator/index.js';
|
|
@@ -35,6 +35,40 @@ try {
|
|
|
35
35
|
// will silently skip generation rather than crashing the dev server.
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Pure helper: compute the desired `html.customData` array for a project.
|
|
40
|
+
*
|
|
41
|
+
* Returns `null` when no change is required (the existing array already matches),
|
|
42
|
+
* otherwise the new array that should be written.
|
|
43
|
+
*
|
|
44
|
+
* The produced entry is a project-relative path with forward slashes. We use
|
|
45
|
+
* `path.relative` rather than a string replace because Vite normalises projectRoot
|
|
46
|
+
* to forward slashes while `htmlDataPath` carries the platform separator from
|
|
47
|
+
* `path.join` — a naive replace fails to match on Windows and leaves an absolute path.
|
|
48
|
+
* Any pre-existing entry pointing at a `needle-html-data.json` (e.g. an absolute path
|
|
49
|
+
* written by an older version of this plugin) is dropped so we don't accumulate
|
|
50
|
+
* duplicates or leave stale absolute paths behind.
|
|
51
|
+
*
|
|
52
|
+
* Exported for testing. The `relativeFn` seam lets tests reproduce platform-specific
|
|
53
|
+
* separator behaviour (`path.win32.relative` / `path.posix.relative`) deterministically.
|
|
54
|
+
*
|
|
55
|
+
* @param {unknown[]} existing Current html.customData entries
|
|
56
|
+
* @param {string} projectRoot
|
|
57
|
+
* @param {string} htmlDataPath Absolute path to the generated needle-html-data.json
|
|
58
|
+
* @param {(from: string, to: string) => string} [relativeFn]
|
|
59
|
+
* @returns {string[] | null}
|
|
60
|
+
*/
|
|
61
|
+
export function computeHtmlCustomData(existing, projectRoot, htmlDataPath, relativeFn = relative) {
|
|
62
|
+
const relPath = relativeFn(projectRoot, htmlDataPath).replace(/\\/g, "/");
|
|
63
|
+
|
|
64
|
+
const cleaned = /** @type {string[]} */ (existing.filter(entry =>
|
|
65
|
+
typeof entry !== "string" || !entry.replace(/\\/g, "/").endsWith("needle-html-data.json")));
|
|
66
|
+
const next = [...cleaned, relPath];
|
|
67
|
+
|
|
68
|
+
if (existing.length === next.length && existing.every((v, i) => v === next[i])) return null;
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
|
|
38
72
|
/**
|
|
39
73
|
* Ensure `.vscode/settings.json` references the generated `needle-html-data.json`
|
|
40
74
|
* so VS Code provides `data-bind-needle` completions in HTML files automatically.
|
|
@@ -48,9 +82,6 @@ function ensureVscodeHtmlCustomData(projectRoot, htmlDataPath) {
|
|
|
48
82
|
const vscodeDir = join(projectRoot, ".vscode");
|
|
49
83
|
const settingsPath = join(vscodeDir, "settings.json");
|
|
50
84
|
|
|
51
|
-
// Relative path from project root for portability
|
|
52
|
-
const relPath = htmlDataPath.replace(projectRoot + "/", "").replace(projectRoot + "\\", "");
|
|
53
|
-
|
|
54
85
|
/** @type {Record<string, unknown>} */
|
|
55
86
|
let settings = {};
|
|
56
87
|
if (existsSync(settingsPath)) {
|
|
@@ -60,10 +91,12 @@ function ensureVscodeHtmlCustomData(projectRoot, htmlDataPath) {
|
|
|
60
91
|
}
|
|
61
92
|
|
|
62
93
|
const key = "html.customData";
|
|
63
|
-
const existing = Array.isArray(settings[key]) ? /** @type {
|
|
64
|
-
|
|
94
|
+
const existing = Array.isArray(settings[key]) ? /** @type {unknown[]} */ (settings[key]) : [];
|
|
95
|
+
|
|
96
|
+
const next = computeHtmlCustomData(existing, projectRoot, htmlDataPath);
|
|
97
|
+
if (next === null) return; // already up-to-date
|
|
65
98
|
|
|
66
|
-
settings[key] =
|
|
99
|
+
settings[key] = next;
|
|
67
100
|
|
|
68
101
|
mkdirSync(vscodeDir, { recursive: true });
|
|
69
102
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
package/plugins/vite/index.d.ts
CHANGED
|
@@ -56,7 +56,9 @@ export { needleFacebookInstantGames } from "./facebook-instant-games.js";
|
|
|
56
56
|
export { needleImportsLogger } from "./imports-logger.js";
|
|
57
57
|
export { needleBuildInfo } from "./buildinfo.js";
|
|
58
58
|
export { needleApp } from "./needle-app.js";
|
|
59
|
+
export { needleLogger } from "./logger.js";
|
|
59
60
|
export { needleServer } from "./server.js";
|
|
60
61
|
export * from "./gzip.js";
|
|
61
62
|
export * from "./config.js";
|
|
62
63
|
export { needleMakeFilesLocal, needleLocalFilesSceneAnalysis } from "./local-files.js";
|
|
64
|
+
export { needleRemote, isRemoteEnabled } from "./remote.js";
|
package/plugins/vite/index.js
CHANGED
|
@@ -82,6 +82,10 @@ import { needleServer } from "./server.js";
|
|
|
82
82
|
import { needleNPM } from "./npm.js";
|
|
83
83
|
import { needleTransformCode } from "./transform.js";
|
|
84
84
|
import { needleLogger } from "./logger.js";
|
|
85
|
+
export { needleLogger } from "./logger.js";
|
|
86
|
+
import { needleRemote, isRemoteEnabled } from "./remote.js";
|
|
87
|
+
export { needleRemote, isRemoteEnabled } from "./remote.js";
|
|
88
|
+
import { needleLog } from "./logging.js";
|
|
85
89
|
import { viteFixWorkerImport } from "../common/worker.js";
|
|
86
90
|
|
|
87
91
|
export { needleServer } from "./server.js";
|
|
@@ -163,11 +167,24 @@ export async function needlePlugins(command = undefined, config = undefined, use
|
|
|
163
167
|
needleServer(command, config, userSettings),
|
|
164
168
|
needleNPM(command, config, userSettings),
|
|
165
169
|
needleApp(command, config, userSettings),
|
|
170
|
+
needleRemote(command, config, userSettings),
|
|
166
171
|
viteFixWorkerImport(),
|
|
167
172
|
// IMPORTANT: needleBuildInfo should be the last plugin, since it waits for the build pipeline to finish and then collects information about the build output directory
|
|
168
173
|
needleBuildInfo(command, config, userSettings),
|
|
169
174
|
];
|
|
170
175
|
|
|
176
|
+
// Remote debugging serves over HTTPS so WebXR / secure‑context features work on
|
|
177
|
+
// the device. HTTPS is provided by @vitejs/plugin-basic-ssl (a peer the user
|
|
178
|
+
// installs when they opt in); load it lazily so it's never required otherwise.
|
|
179
|
+
if (command !== "build" && isRemoteEnabled(userSettings)) {
|
|
180
|
+
try {
|
|
181
|
+
const { default: basicSsl } = await import("@vitejs/plugin-basic-ssl");
|
|
182
|
+
array.push(basicSsl());
|
|
183
|
+
} catch {
|
|
184
|
+
needleLog("needle-remote", "Remote HTTPS needs `@vitejs/plugin-basic-ssl` — install it: npm i -D @vitejs/plugin-basic-ssl", "warn");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
171
188
|
const asap = await needleAsap(command, config, userSettings);
|
|
172
189
|
if(asap) array.push(asap);
|
|
173
190
|
|
package/plugins/vite/license.js
CHANGED
|
@@ -49,12 +49,12 @@ export function needleLicense(command, config, userSettings) {
|
|
|
49
49
|
let modified = false;
|
|
50
50
|
|
|
51
51
|
// Replace license type
|
|
52
|
-
const index = src.indexOf("
|
|
52
|
+
const index = src.indexOf("$rJboqN");
|
|
53
53
|
if (index >= 0) {
|
|
54
54
|
const end = src.indexOf(";", index);
|
|
55
55
|
if (end >= 0) {
|
|
56
56
|
const line = src.substring(index, end);
|
|
57
|
-
const replaced = "
|
|
57
|
+
const replaced = "$rJboqN = \"" + licenseResult.type + "\"";
|
|
58
58
|
src = src.replace(line, replaced);
|
|
59
59
|
modified = true;
|
|
60
60
|
}
|
|
@@ -62,12 +62,12 @@ export function needleLicense(command, config, userSettings) {
|
|
|
62
62
|
|
|
63
63
|
// Replace license JWT (same pattern)
|
|
64
64
|
if (licenseResult.jwt) {
|
|
65
|
-
const jwtIndex = src.indexOf("_$
|
|
65
|
+
const jwtIndex = src.indexOf("_$RRhFut");
|
|
66
66
|
if (jwtIndex >= 0) {
|
|
67
67
|
const jwtEnd = src.indexOf(";", jwtIndex);
|
|
68
68
|
if (jwtEnd >= 0) {
|
|
69
69
|
const jwtLine = src.substring(jwtIndex, jwtEnd);
|
|
70
|
-
const jwtReplaced = "_$
|
|
70
|
+
const jwtReplaced = "_$RRhFut = \"" + licenseResult.jwt + "\"";
|
|
71
71
|
src = src.replace(jwtLine, jwtReplaced);
|
|
72
72
|
modified = true;
|
|
73
73
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFile } from 'fs';
|
|
2
2
|
import { tryParseNeedleEngineSrcAttributeFromHtml } from '../common/needle-engine.js';
|
|
3
3
|
import { needleLog } from './logging.js';
|
|
4
|
+
import { getPublicIdentifier } from '../common/license.js';
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
|
|
@@ -21,6 +22,22 @@ export function needleApp(command, config, userSettings) {
|
|
|
21
22
|
|
|
22
23
|
let outputDir = "dist";
|
|
23
24
|
|
|
25
|
+
// The engine API is emitted as a dedicated, deterministically-named facade chunk so that the
|
|
26
|
+
// generated needle-app.js can re-export it - giving users a single import path (the same URL they
|
|
27
|
+
// load needle-app.js from also serves the engine API). See buildStart below.
|
|
28
|
+
// Emitted INTO assets/ (alongside the app's other chunks) on purpose: the app entry lives in assets/
|
|
29
|
+
// and then references the facade with a same-directory specifier ("./needle-app.engine.js"). A root-level
|
|
30
|
+
// facade would be referenced as "../needle-app.engine.js", and Vite computes preload-dependency hints with
|
|
31
|
+
// Node's path.relative, which returns backslashes on Windows for cross-directory paths ("..\\needle-app.engine.js")
|
|
32
|
+
// - a broken modulepreload URL. Keeping it in assets/ avoids the "../" entirely.
|
|
33
|
+
const apiFacadeFileName = "assets/needle-app.engine.js";
|
|
34
|
+
let apiFacadeEmitted = false;
|
|
35
|
+
|
|
36
|
+
// Public project identifier, baked as a comment into the generated needle-app.js for traceability.
|
|
37
|
+
// This is the Needle Cloud `public_key` (the same value injected into client code as NEEDLE_PUBLIC_KEY) -
|
|
38
|
+
// it is PUBLIC and safe to embed. It is NOT the secret NEEDLE_CLOUD_TOKEN. Null when no token is configured.
|
|
39
|
+
let publicKey = null;
|
|
40
|
+
|
|
24
41
|
/**
|
|
25
42
|
* @type {import('vite').Plugin}
|
|
26
43
|
*/
|
|
@@ -28,6 +45,35 @@ export function needleApp(command, config, userSettings) {
|
|
|
28
45
|
{
|
|
29
46
|
name: 'needle:app',
|
|
30
47
|
enforce: "post",
|
|
48
|
+
async buildStart() {
|
|
49
|
+
// Emit @needle-tools/engine/needle-app (a thin re-export of the public API) as its own
|
|
50
|
+
// entry chunk with a fixed filename. Built in the same pass as the app, it deduplicates
|
|
51
|
+
// into the same engine chunks - so importing hooks from it drives the SAME running scene.
|
|
52
|
+
try {
|
|
53
|
+
const resolved = await this.resolve('@needle-tools/engine/needle-app');
|
|
54
|
+
if (resolved?.id) {
|
|
55
|
+
this.emitFile({
|
|
56
|
+
type: 'chunk',
|
|
57
|
+
id: resolved.id,
|
|
58
|
+
fileName: apiFacadeFileName,
|
|
59
|
+
// Preserve the full re-exported API surface. Without this, the build tree-shakes the
|
|
60
|
+
// facade down to only the single export the app internally consumes, so a page-side
|
|
61
|
+
// `import { onStart } from ".../needle-app.js"` fails with "does not provide an export".
|
|
62
|
+
preserveSignature: 'allow-extension',
|
|
63
|
+
});
|
|
64
|
+
apiFacadeEmitted = true;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
needleLog("needle-app", "Could not resolve @needle-tools/engine/needle-app - engine hooks (onStart etc.) will not be importable from needle-app.js", "warn", { dimBody: false });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
needleLog("needle-app", "Failed to emit " + apiFacadeFileName + " facade: " + e.message, "warn", { dimBody: false });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fetch the public project identifier (best-effort; null without a NEEDLE_CLOUD_TOKEN).
|
|
75
|
+
publicKey = await getPublicIdentifier(undefined).catch(() => null);
|
|
76
|
+
},
|
|
31
77
|
configResolved(config) {
|
|
32
78
|
outputDir = config.build.outDir || "dist";
|
|
33
79
|
},
|
|
@@ -52,7 +98,7 @@ export function needleApp(command, config, userSettings) {
|
|
|
52
98
|
|
|
53
99
|
const main_asset = attribute_src?.[0] || imported_glbs?.[0];
|
|
54
100
|
|
|
55
|
-
const webComponent = generateNeedleEmbedWebComponent(path, main_asset);
|
|
101
|
+
const webComponent = generateNeedleEmbedWebComponent(path, main_asset, apiFacadeEmitted ? apiFacadeFileName : null, publicKey);
|
|
56
102
|
await writeFile(`${outputDir}/needle-app.js`, webComponent, (err) => {
|
|
57
103
|
if (err) {
|
|
58
104
|
needleLog("needle-app", "Could not create needle-app.js: " + err.message, "error", { dimBody: false });
|
|
@@ -78,9 +124,11 @@ export function needleApp(command, config, userSettings) {
|
|
|
78
124
|
/**
|
|
79
125
|
* @param {string} filepath
|
|
80
126
|
* @param {string | null} src
|
|
127
|
+
* @param {string | null | undefined} apiFacadeFile Filename of the emitted engine-API facade chunk (e.g. "needle-app.engine.js"). When set, its API is re-exported so engine hooks can be imported from needle-app.js.
|
|
128
|
+
* @param {string | null | undefined} publicKey Public Needle Cloud project identifier, baked into the file header as a comment for traceability.
|
|
81
129
|
* @returns {string}
|
|
82
130
|
*/
|
|
83
|
-
function generateNeedleEmbedWebComponent(filepath, src) {
|
|
131
|
+
function generateNeedleEmbedWebComponent(filepath, src, apiFacadeFile, publicKey) {
|
|
84
132
|
|
|
85
133
|
|
|
86
134
|
// filepath is e.g. `assets/index-XXXXXXXX.js`
|
|
@@ -89,8 +137,27 @@ function generateNeedleEmbedWebComponent(filepath, src) {
|
|
|
89
137
|
|
|
90
138
|
const componentDefaultName = 'needle-app';
|
|
91
139
|
|
|
92
|
-
|
|
140
|
+
// Bake the asset path in as a safely-escaped string literal: paths can contain quotes/spaces/backticks,
|
|
141
|
+
// and JSON string literals are valid JS string literals. No asset → serializes to the literal `null`,
|
|
142
|
+
// matching the previous default. Actual origin resolution happens at runtime via `new URL(...)` below.
|
|
143
|
+
const defaultSrc = JSON.stringify(src ?? null);
|
|
144
|
+
|
|
145
|
+
// Re-export the engine API facade so users have a SINGLE import path: the same URL they load
|
|
146
|
+
// needle-app.js from also serves the engine hooks (`import { onStart } from "<deploy>/needle-app.js"`).
|
|
147
|
+
// The facade is a separate, deterministically-named chunk that shares the app's engine instance,
|
|
148
|
+
// so the re-export resolves to the live scene. Specifier is relative to this file's own URL.
|
|
149
|
+
const apiReexport = apiFacadeFile
|
|
150
|
+
? `export * from ${JSON.stringify("./" + apiFacadeFile)};\n`
|
|
151
|
+
: ``;
|
|
93
152
|
|
|
153
|
+
// File header for traceability: which build produced this file, and (if available) the public project id.
|
|
154
|
+
// Values are sanitized to a single line so they cannot break out of the `//` comment.
|
|
155
|
+
const oneLine = (/** @type {string} */ v) => String(v).replace(/[\r\n]+/g, " ").trim();
|
|
156
|
+
let header = `// needle-app.js - generated by the @needle-tools/engine needle-app plugin\n`;
|
|
157
|
+
header += `// build time: ${oneLine(new Date().toISOString())}\n`;
|
|
158
|
+
if (publicKey) header += `// public key: ${oneLine(publicKey)}\n`;
|
|
159
|
+
|
|
160
|
+
return `${header}${apiReexport}
|
|
94
161
|
// Needle Engine attributes we want to allow to be overriden
|
|
95
162
|
const knownAttributes = [
|
|
96
163
|
"src",
|
|
@@ -170,8 +237,14 @@ if (!customElements.get(componentName)) {
|
|
|
170
237
|
updateAttributes() {
|
|
171
238
|
console.debug("NeedleApp updating attributes");
|
|
172
239
|
|
|
173
|
-
|
|
174
|
-
if(src)
|
|
240
|
+
let src = this.getAttribute('src') || ${defaultSrc};
|
|
241
|
+
if(src) {
|
|
242
|
+
// Resolve the asset URL against this script's own origin (import.meta.url) so relative
|
|
243
|
+
// paths keep resolving to the deployed app even when <needle-app> is embedded on a
|
|
244
|
+
// different domain (e.g. Webflow). Absolute URLs pass through unchanged; spaces get encoded.
|
|
245
|
+
src = new URL(src, this.basePath + '/').href;
|
|
246
|
+
this.needleEngine.setAttribute("src", src);
|
|
247
|
+
}
|
|
175
248
|
else this.needleEngine.removeAttribute("src");
|
|
176
249
|
|
|
177
250
|
for(const attr of knownAttributes) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether remote debugging is enabled. Toggled by the `remote` user setting or the
|
|
3
|
+
* `NEEDLE_REMOTE` env var (the env var wins so you can flip it without editing the
|
|
4
|
+
* config: `NEEDLE_REMOTE=1 npm run dev`, or `NEEDLE_REMOTE=false` to force off).
|
|
5
|
+
* @param {import('../types/userconfig.js').userSettings} [userSettings]
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
export function isRemoteEnabled(userSettings?: import("../types/userconfig.js").userSettings): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Remote debugging plugin.
|
|
11
|
+
*
|
|
12
|
+
* Binds the Vite dev server to the LAN (`server.host = true`) so other devices can
|
|
13
|
+
* reach it, and prints the network URL + a scannable QR code once the server is
|
|
14
|
+
* listening. HTTPS itself is provided by `@vitejs/plugin-basic-ssl`, which
|
|
15
|
+
* {@link needlePlugins} adds to the plugin array when remote is enabled (WebXR and
|
|
16
|
+
* other secure‑context features on the device require https).
|
|
17
|
+
*
|
|
18
|
+
* The page's console / errors / device info already stream back to the dev server
|
|
19
|
+
* via the Needle logger (`needleLogger` + `logger.client.js`) over Vite's
|
|
20
|
+
* websocket, landing in `node_modules/.needle/logs/latest.client.needle.log`. This
|
|
21
|
+
* plugin is read‑only: it does not (yet) inject commands back into the page.
|
|
22
|
+
*
|
|
23
|
+
* @param {"build" | "serve" | undefined} command
|
|
24
|
+
* @param {unknown} _config
|
|
25
|
+
* @param {import('../types/userconfig.js').userSettings} [userSettings]
|
|
26
|
+
* @returns {import('vite').Plugin | null}
|
|
27
|
+
*/
|
|
28
|
+
export function needleRemote(command: "build" | "serve" | undefined, _config: unknown, userSettings?: import("../types/userconfig.js").userSettings): import("vite").Plugin | null;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import os from "os";
|
|
3
|
+
import { needleLog } from "./logging.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether remote debugging is enabled. Toggled by the `remote` user setting or the
|
|
7
|
+
* `NEEDLE_REMOTE` env var (the env var wins so you can flip it without editing the
|
|
8
|
+
* config: `NEEDLE_REMOTE=1 npm run dev`, or `NEEDLE_REMOTE=false` to force off).
|
|
9
|
+
* @param {import('../types/userconfig.js').userSettings} [userSettings]
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isRemoteEnabled(userSettings) {
|
|
13
|
+
const env = process.env.NEEDLE_REMOTE;
|
|
14
|
+
if (env !== undefined && env !== "") {
|
|
15
|
+
const v = env.toLowerCase();
|
|
16
|
+
return !(v === "false" || v === "0" || v === "off" || v === "no");
|
|
17
|
+
}
|
|
18
|
+
const r = userSettings?.remote;
|
|
19
|
+
return r === true || (typeof r === "object" && r !== null);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pick the first non-internal IPv4 LAN address — the one a phone / headset on the
|
|
24
|
+
* same Wi‑Fi can reach. Prefers the common private ranges and otherwise falls back
|
|
25
|
+
* to the first external IPv4 found.
|
|
26
|
+
* @returns {string | null}
|
|
27
|
+
*/
|
|
28
|
+
function lanAddress() {
|
|
29
|
+
const ifaces = os.networkInterfaces();
|
|
30
|
+
/** @type {string | null} */
|
|
31
|
+
let fallback = null;
|
|
32
|
+
for (const name of Object.keys(ifaces)) {
|
|
33
|
+
for (const net of ifaces[name] || []) {
|
|
34
|
+
// Node reports family as the string "IPv4" (older versions: the number 4)
|
|
35
|
+
if (net.family !== "IPv4" && /** @type {unknown} */ (net.family) !== 4) continue;
|
|
36
|
+
if (net.internal) continue;
|
|
37
|
+
if (/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(net.address)) return net.address;
|
|
38
|
+
if (!fallback) fallback = net.address;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Remote debugging plugin.
|
|
46
|
+
*
|
|
47
|
+
* Binds the Vite dev server to the LAN (`server.host = true`) so other devices can
|
|
48
|
+
* reach it, and prints the network URL + a scannable QR code once the server is
|
|
49
|
+
* listening. HTTPS itself is provided by `@vitejs/plugin-basic-ssl`, which
|
|
50
|
+
* {@link needlePlugins} adds to the plugin array when remote is enabled (WebXR and
|
|
51
|
+
* other secure‑context features on the device require https).
|
|
52
|
+
*
|
|
53
|
+
* The page's console / errors / device info already stream back to the dev server
|
|
54
|
+
* via the Needle logger (`needleLogger` + `logger.client.js`) over Vite's
|
|
55
|
+
* websocket, landing in `node_modules/.needle/logs/latest.client.needle.log`. This
|
|
56
|
+
* plugin is read‑only: it does not (yet) inject commands back into the page.
|
|
57
|
+
*
|
|
58
|
+
* @param {"build" | "serve" | undefined} command
|
|
59
|
+
* @param {unknown} _config
|
|
60
|
+
* @param {import('../types/userconfig.js').userSettings} [userSettings]
|
|
61
|
+
* @returns {import('vite').Plugin | null}
|
|
62
|
+
*/
|
|
63
|
+
export function needleRemote(command, _config, userSettings) {
|
|
64
|
+
if (command === "build") return null;
|
|
65
|
+
if (!isRemoteEnabled(userSettings)) return null;
|
|
66
|
+
|
|
67
|
+
const r = userSettings?.remote;
|
|
68
|
+
const showQr = !(typeof r === "object" && r !== null && r.qr === false);
|
|
69
|
+
let announced = false;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
name: "needle:remote",
|
|
73
|
+
enforce: "pre",
|
|
74
|
+
config(config) {
|
|
75
|
+
if (!config.server) config.server = {};
|
|
76
|
+
// Bind 0.0.0.0 so phones / headsets on the same network can connect.
|
|
77
|
+
// Respect an explicit host the user already set.
|
|
78
|
+
if (config.server.host === undefined) config.server.host = true;
|
|
79
|
+
},
|
|
80
|
+
configureServer(server) {
|
|
81
|
+
const announce = async () => {
|
|
82
|
+
if (announced) return;
|
|
83
|
+
const addr = server.httpServer?.address?.();
|
|
84
|
+
const port = (addr && typeof addr === "object" && addr.port) || server.config.server.port;
|
|
85
|
+
const ip = lanAddress();
|
|
86
|
+
if (!port || !ip) {
|
|
87
|
+
needleLog("needle-remote", "Remote debugging is ON but no LAN address was found — is this machine on a network?", "warn");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
announced = true;
|
|
91
|
+
const protocol = server.config.server.https ? "https" : "http";
|
|
92
|
+
const url = `${protocol}://${ip}:${port}/`;
|
|
93
|
+
const note = protocol === "https"
|
|
94
|
+
? "\n (accept the one‑time self‑signed certificate warning on the device)"
|
|
95
|
+
: "\n (no HTTPS — install @vitejs/plugin-basic-ssl for WebXR / secure‑context features)";
|
|
96
|
+
needleLog("needle-remote", `Remote debugging is ON — open on a device on the same Wi‑Fi:\n ${url}${note}`);
|
|
97
|
+
// The device's console / errors stream back over Vite's HMR websocket
|
|
98
|
+
// (via the Needle logger). With HMR disabled there's no socket, so the
|
|
99
|
+
// logs would silently never arrive — warn rather than leave you guessing.
|
|
100
|
+
if (server.config.server.hmr === false) {
|
|
101
|
+
needleLog("needle-remote", "HMR is disabled, so device console logs won't stream back (they ride the HMR websocket). Enable HMR to capture remote logs.", "warn");
|
|
102
|
+
}
|
|
103
|
+
if (showQr) await printQr(url);
|
|
104
|
+
};
|
|
105
|
+
// `listening` fires once the port is bound; resolve the real port from it.
|
|
106
|
+
server.httpServer?.once("listening", announce);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Print a scannable QR code of the URL to the terminal (best effort — needs the
|
|
113
|
+
* optional `qrcode-terminal` package; if it's missing we just say how to get it).
|
|
114
|
+
* @param {string} url
|
|
115
|
+
*/
|
|
116
|
+
async function printQr(url) {
|
|
117
|
+
try {
|
|
118
|
+
const mod = await import("qrcode-terminal");
|
|
119
|
+
const qrcode = /** @type {{ generate: (text: string, opts?: object) => void }} */ (mod.default || mod);
|
|
120
|
+
qrcode.generate(url, { small: true });
|
|
121
|
+
} catch {
|
|
122
|
+
needleLog("needle-remote", "Install `qrcode-terminal` to show a scannable QR code here: npm i -D qrcode-terminal", "log");
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/engine/api.ts
CHANGED
|
@@ -230,7 +230,7 @@ export * from "./engine_input.js";
|
|
|
230
230
|
export { InstancingUtil } from "./engine_instancing.js";
|
|
231
231
|
|
|
232
232
|
/** License checking utilities */
|
|
233
|
-
export {
|
|
233
|
+
export { $fwGZrGe, _$qWUhYZU, _XtEQ } from "./engine_license.js";
|
|
234
234
|
|
|
235
235
|
|
|
236
236
|
// ============================================================================
|