UNPKG

manifold-3d

Version:

Geometry library for topological robustness

319 lines 13.4 kB
// Copyright 2025 The Manifold Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * The bundler resolves files and modules at load time, bundles them, and allows * the manifoldCAD runtime to provide objects (like manifold itself) and * properties (such as `import.meta.url`). * @packageDocumentation * @group ManifoldCAD * @category Core */ import resolve from '@jridgewell/resolve-uri'; import * as esbuild from 'esbuild-wasm'; import MagicString from 'magic-string'; import { BundlerError, FetchError } from "./error.js"; import { fetchWithRetry, isNode } from "./util.js"; let esbuildWasmUrl = null; let esbuildHasOwnWorker = false; export const setWasmUrl = (url) => { esbuildWasmUrl = url; }; export const setHasOwnWorker = (x) => { esbuildHasOwnWorker = x; }; /** * These content delivery networks provide NPM modules as ES modules, * whether they were published that way or not. */ export const cdnUrlHelpers = { 'esm.sh': (specifier) => `https://esm.sh/${specifier}`, 'jsDelivr': (specifier) => `https://cdn.jsdelivr.net/npm/${specifier}/+esm`, 'skypack': (specifier) => `https://cdn.skypack.dev/${specifier}` }; const cdnUrl = (specifier, jsCDN) => { if (!jsCDN) return specifier; const helper = cdnUrlHelpers[jsCDN]; return helper ? helper(specifier) : `${jsCDN}${specifier}`; }; const insertMetaData = (text, sourceUrl) => { if (sourceUrl) { const st = new MagicString(text); st.prepend(`const _import_meta_url=_manifold_runtime_url ?? '${sourceUrl}';\n`); const map = st.generateMap({ hires: true }); return `${st.toString()}\n//# sourceMappingURL=${map.toUrl()}`; } ; return text; }; /** * This is a plugin for esbuild that has three functions: * * It resolves NPM packages to urls served by various CDNs. * * It fetches imports from http/https urls. * * It provides evaluation context to npm packages. * * ManifoldCAD libraries can include `manifold-3d/manifoldCAD` to get access to * the evaluator context. Outside of the evaluator, this involves importing a * module. Inside the evaluator, imports are in the global namespace. The * imported module will not be able to see the actual evaluator context. Here, * we will swap it out for an object we will inject at evaluation time. */ export const esbuildManifoldPlugin = (options = {}) => ({ name: 'esbuild-manifold-plugin', async setup(build) { let manifoldCADExportPath = null; const manifoldCADExportURLMatch = /manifold-3d(@[0-9.]+)?\/manifoldCAD/; const ManifoldCADExportMatch = /^manifold-3d\/manifoldCAD(.ts|.js)?$/; const manifoldCADExportNames = [ // Manifold classes. 'Mesh', 'Manifold', 'CrossSection', // Manifold methods. 'triangulate', // Scene builder exports. 'show', 'only', 'setMaterial', // GLTFNode and utilities. 'GLTFMaterial', 'GLTFNode', 'getGLTFNodes', 'VisualizationGLTFNode', 'CrossSectionGLTFNode', // Import 'importModel', 'importManifold', // Getters for global properties 'getCircularSegments', 'getMinCircularAngle', 'getMinCircularEdgeLength', 'getAnimationDuration', 'getAnimationFPS', 'getAnimationMode', // Setters for global properties. // These will only be defined for top level scripts 'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments', 'resetToCircularDefaults', 'setMorphStart', 'setMorphEnd', 'setAnimationDuration', 'setAnimationFPS', 'setAnimationMode', 'resetGLTFNodes', // ManifoldCAD specific exports. 'isManifoldCAD' ]; if (isNode()) { // We only need to check against the local manifoldCAD context on disk if // we happen to be running in node. Truthfully, this is really only // necessary in development. End users will almost always import // `manifold-3d/manifoldCAD` instead of `some/path/to/manifoldCAD`. (async () => { const { resolve, dirname } = await import('node:path'); const { fileURLToPath } = await import('node:url'); const dir = ('string' == typeof __dirname && __dirname) || ('string' == typeof import.meta?.dirname && import.meta.dirname) || dirname(fileURLToPath(import.meta.url)); manifoldCADExportPath = resolve(dir, './manifoldCAD.ts'); })(); } let entrypoint = null; // Try to resolve local files. If we can't, blindly resolve them to a CDN. build.onResolve({ filter: /.*/ }, async (args) => { // Avoid loops. if (args.pluginData !== undefined) return null; // Skip a few cases handled elsewhere. if (args.namespace === 'http-url') return null; if (args.path.match(/^https?:\/\//)) return null; if (!entrypoint && args.kind === 'entry-point') { entrypoint = resolve(args.path, args.resolveDir + '/'); } // Is this a manifoldCAD context import? // FIXME resolve path here too. const pluginData = { toplevel: args.importer === entrypoint || args.importer === options.filename || args.importer === '<stdin>', }; if (args.path.match(ManifoldCADExportMatch)) { return { namespace: 'manifold-cad-globals', path: args.path, pluginData }; } // Is this a virtual file? // FIXME Resolve paths! if (options.files && Object.keys(options.files).includes(args.path)) { return { namespace: 'virtual-file', path: args.path }; } // Try esbuilds' resolver first. const result = await build.resolve(args.path, { resolveDir: args.resolveDir, kind: 'import-statement', pluginData: { resolveDir: options.resolveDir } }); // We found a local file! if (result.errors.length === 0) { if (manifoldCADExportPath && manifoldCADExportPath === result.path) { // It resolved to our local manifoldCAD context. return { namespace: 'manifold-cad-globals', path: args.path, pluginData }; } result.pluginData = { resolveDir: args.resolveDir.endsWith('/') ? args.resolveDir : `${args.resolveDir}/` }; return result; } // Built in resolver failed. Are we fetching remote packages? if (options.fetchRemotePackages !== false && options.jsCDN) { return { path: cdnUrl(args.path, options.jsCDN), namespace: 'http-url', }; } // Okay fine. I give up. return null; }); // Instead of loading manifoldCAD.ts, insert an instantiated copy. // The global variables enabling this are set by the worker. build.onLoad({ filter: /.*/, namespace: 'manifold-cad-globals' }, (args) => { // This is a string replace. const globals = args.pluginData?.toplevel ? '_manifold_cad_top_level' : '_manifold_cad_library'; return { // Type hinting isn't necessary. Only esbuild will see the swap, // and it doesn't do type validation. contents: `export const {${manifoldCADExportNames}} = ${globals};`, }; }); // Unless disabled, handle HTTP/HTTPs urls. if (options.fetchRemotePackages !== false) { // Resolve absolute urls. build.onResolve({ filter: /^https?:\/\// }, args => { return { path: args.path, namespace: 'http-url' }; }); // Resolve relative http urls into absolute urls. build.onResolve({ filter: /.*/, namespace: 'http-url' }, args => { const path = new URL(args.path, args.importer).toString(); // Is this a manifoldCAD context import from a remote package? // e.g.: `/npm/manifold-3d/manifoldCAD/+esm` if (path.match(manifoldCADExportURLMatch)) { const response = { path, namespace: 'manifold-cad-globals', pluginData: { toplevel: false } }; return response; } return { path, namespace: 'http-url' }; }); // Fetch urls. build.onLoad({ filter: /.*/, namespace: 'http-url' }, async (args) => { try { const response = await fetchWithRetry(args.path); return { contents: await response.text() }; } catch (err) { if (err instanceof FetchError) { return { errors: [{ text: err.message }] }; } throw err; } }); } // Virtual files. build.onLoad({ filter: /.*/, namespace: 'virtual-file' }, (args) => { const text = (options.files)[args.path]; const contents = insertMetaData(text, `file://${args.path}`); const loader = (args.path.match(/\.js$/)) ? 'js' : 'ts'; return { contents, loader }; }); // Finally, local files. build.onLoad({ filter: /.(ts|js)$/ }, async (args) => { const fs = await import('node:fs/promises'); const text = await fs.readFile(args.path, 'utf8'); const contents = insertMetaData(text, `file://${args.path}`); const loader = (args.path.match(/\.js$/)) ? 'js' : 'ts'; return { contents, loader }; }); }, }); let esbuild_initialized = false; const getEsbuildConfig = async (options = {}) => { if (!esbuild_initialized) { const esbuildOptions = {}; if (typeof esbuildWasmUrl === 'string' && esbuildWasmUrl) { esbuildOptions.wasmURL = esbuildWasmUrl; esbuildOptions.worker = esbuildHasOwnWorker === true; } await esbuild.initialize(esbuildOptions); esbuild_initialized = true; } return { // Create a bundle in memory. bundle: true, write: false, platform: 'neutral', treeShaking: false, sourcemap: 'inline', sourcesContent: false, // We have the source handy already. format: 'cjs', logLevel: 'silent', plugins: [ esbuildManifoldPlugin(options), ], // Some CDN imports will check import.meta.env. This is only present when // generating an ESM bundle. In other cases, it generates log noise, so // let's drop it down a log level. logOverride: { 'empty-import-meta': 'info' }, // Define some paths so we can find resources by relative URL. define: { 'import.meta.url': '_import_meta_url', } }; }; export const bundleFile = async (entrypoint, options = {}) => { try { const built = await esbuild.build({ ...(await getEsbuildConfig({ ...options, filename: entrypoint })), entryPoints: [entrypoint], }); return built.outputFiles[0].text; } catch (error) { if (error.errors?.length) { throw new BundlerError(error); } else { throw error; } } }; export const bundleCode = async (code, options = {}) => { try { let resolveDir; if (isNode() && options.filename) { const { dirname } = await import('node:path'); resolveDir = options.resolveDir ?? dirname(options.filename); } const built = await esbuild.build({ ...(await getEsbuildConfig(options)), stdin: { contents: insertMetaData(code, `file://${options.filename}`), sourcefile: options.filename, resolveDir, loader: 'ts', } }); return built.outputFiles[0].text; } catch (error) { if (error.errors?.length) { throw new BundlerError(error); } else { throw error; } } }; //# sourceMappingURL=bundler.js.map