@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
173 lines (147 loc) • 7.12 kB
JavaScript
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { tryGetNeedleEngineVersion } from '../common/version.js';
import { tryGetGenerator } from '../common/generator.js';
import { getConfig, getMeta } from '../common/config.cjs';
import { alias } from './alias.cjs';
import { createBuildInfoFile } from '../common/buildinfo.js';
import { getPublicIdentifier } from '../common/license.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/** Integrates Needle Engine into a Next.js project.
* Pass a nextConfig object in to add the needle specific settings.
* Optionally omit nextConfig and it will be created for you.
* @param {import('next').NextConfig} [nextConfig]
* @param {import('../types').userSettings} [userSettings]
* @returns {Promise<import('next').NextConfig>}
*/
export async function needleNext(nextConfig, userSettings) {
console.log("Apply 🌵 needle next config");
if (!nextConfig) {
nextConfig = {
reactStrictMode: true,
};
}
const needleConfig = getConfig();
// add transpile packages
if (!nextConfig.transpilePackages) {
nextConfig.transpilePackages = [];
}
// ESM packages must be transpiled so webpack can resolve their imports
nextConfig.transpilePackages.push(
"three",
"peerjs",
"three-mesh-ui",
"three-mesh-bvh"
);
if (nextConfig.output === undefined) {
console.log("Set output to 'export' (see 'https://nextjs.org/docs/pages/building-your-application/deploying/static-exports#configuration' for more information)");
nextConfig.output = "export";
// we *also* need to turn OFF image optimization for static HTML files to be generated
// see https://github.com/vercel/next.js/issues/40240
if (nextConfig.images === undefined) {
nextConfig.images = {
unoptimized: true
}
}
}
if (nextConfig.distDir == undefined) {
console.log("Export to 'dist'");
if (needleConfig?.buildDirectory) {
console.log(`Using build directory from needle config: ${needleConfig.buildDirectory}`);
nextConfig.distDir = needleConfig.buildDirectory;
}
else {
console.log("Using default build directory 'dist'. You can override the output directory via the needle config or by setting nextConfig.distDir");
nextConfig.distDir = "dist";
}
}
const projectId = await getPublicIdentifier(undefined).catch(e => { /*ignore*/ })
// add webpack config
if (!nextConfig.webpack) nextConfig.webpack = nextWebPack;
else {
const webpackFn = nextConfig.webpack;
nextConfig.webpack = (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
nextWebPack(config, { buildId, dev, isServer, defaultLoaders, webpack });
return webpackFn(config, { buildId, dev, isServer, defaultLoaders, webpack });
}
}
/** @param {import ('next').NextConfig config } */
function nextWebPack(config, { buildId, dev, isServer, defaultLoaders, webpack }) {
// TODO: get public identifier key from license server
const meta = getMeta();
let useRapier = true;
if (userSettings.useRapier === false) useRapier = false;
else if (meta && meta.useRapier === false) useRapier = false;
// add defines
const webpackModule = userSettings.modules?.webpack;
const definePlugin = webpackModule && new webpackModule.DefinePlugin({
NEEDLE_ENGINE_VERSION: JSON.stringify(tryGetNeedleEngineVersion() ?? "0.0.0"),
NEEDLE_ENGINE_GENERATOR: JSON.stringify(tryGetGenerator() ?? "unknown"),
NEEDLE_USE_RAPIER: JSON.stringify(useRapier),
NEEDLE_PUBLIC_KEY: JSON.stringify(projectId),
// TODO globalThis is not solved via DefinePlugin
parcelRequire: undefined,
});
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 };`");
else
config.plugins.push(definePlugin);
if (!config.module) config.module = {};
if (!config.module.rules) config.module.rules = [];
// add license plugin
const team_id = userSettings?.license?.team || undefined;
config.module.rules.push({
test: /engine_license\.(ts|js)$/,
loader: resolve(__dirname, `license.cjs`),
options: {
team: team_id,
accessToken: userSettings?.license?.accessToken,
}
});
// Rewrite the bare package specifier in GenerateMeshBVHWorker.js to a relative path
// so webpack 5 can detect and bundle the worker with its dependencies
config.module.rules.push({
test: /GenerateMeshBVHWorker\.js$/,
loader: resolve(__dirname, 'meshbvhworker-import.cjs')
});
// three.js accesses `window` at module scope, but Workers only have `self`.
// BannerPlugin prepends this shim to alias `self.window = self` so three.js doesn't crash.
// `raw: true` inserts the string as code, not a comment.
// `test: /\.js$/` restricts it to JS files only — without it, the banner would also be
// prepended to CSS assets, breaking PostCSS/SCSS parsing during minification.
// Only applied to client-side bundles (not server) to avoid `self is not defined` in Node.
if (webpackModule && !isServer) {
config.plugins.push(new webpackModule.BannerPlugin({
banner: 'if(typeof window==="undefined"&&typeof self!=="undefined")self.window=self;',
raw: true,
test: /\.js$/,
}));
}
// Handle Vite's ?url imports for wasm and txt files (used by @needle-tools/materialx)
config.module.rules.push({
resourceQuery: /url/,
type: 'asset/resource',
});
config.experiments = config.experiments || {};
config.experiments.asyncWebAssembly = true;
alias(config);
// these hooks are invoked but nextjs deletes the files again:
// add webpack done plugin https://webpack.js.org/api/compiler-hooks/
// config.plugins.push({
// apply(compiler) {
// compiler.hooks.shutdown.tap('NeedleDonePlugin', (stats) => {
// return createBuildInfoFile(nextConfig.distDir);
// });
// }
// });
// so as a workaround for above's problem:
// hook into process quit event since there doesn't seem to be a next hook for "after emit"
// node's beforeExit event is not called :(
process.on('exit', (code) => {
if (code === 0)
return createBuildInfoFile(nextConfig.distDir);
});
return config;
}
return nextConfig;
}