From ad7b511ca57466f3389e87b7e204fd7f67466b4c Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Wed, 16 Sep 2026 23:50:59 -0400 Subject: [PATCH 1/6] feat: add pipeline CLI Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 20 ++- README.md | 4 + docs/usage.md | 53 +++++++ eslint.config.mjs | 2 +- package.json | 8 +- packages/cli/LICENSE | 201 +++++++++++++++++++++++++ packages/cli/README.md | 46 ++++++ packages/cli/bin/node-assets.cjs | 10 ++ packages/cli/package.json | 35 +++++ packages/cli/src/cli.ts | 77 ++++++++++ packages/cli/src/pipeline.ts | 76 ++++++++++ packages/cli/vite.config.ts | 21 +++ pnpm-lock.yaml | 6 + pnpm-workspace.yaml | 3 + tests/e2e/cli.test.ts | 243 +++++++++++++++++++++++++++++++ tests/helpers/cli.ts | 54 +++++++ tsconfig.json | 5 +- 17 files changed, 858 insertions(+), 6 deletions(-) create mode 100644 packages/cli/LICENSE create mode 100644 packages/cli/README.md create mode 100755 packages/cli/bin/node-assets.cjs create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/cli.ts create mode 100644 packages/cli/src/pipeline.ts create mode 100644 packages/cli/vite.config.ts create mode 100644 tests/e2e/cli.test.ts create mode 100644 tests/helpers/cli.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f088fd2..cf7a41f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,15 @@ pnpm install pnpm build ``` +The library remains at the repository root; `packages/cli` is a separate pnpm +workspace package that depends on it. Build both packages before running the +local CLI: + +```sh +pnpm cli pipeline input.gltf ktx2 draco output.glb +pnpm cli --help +``` + ## Scripts ```sh @@ -22,8 +31,17 @@ pnpm lint:fix # ESLint autofix pnpm format # Write Prettier formatting pnpm test # Run Vitest pnpm test:watch # Run Vitest in watch mode -pnpm build # Build with Vite and emit dist/ +pnpm build # Build the library, then the CLI +pnpm build:library # Build only the library into dist/ +pnpm cli # Run the built CLI (append pipeline arguments) pnpm typedocs # Generate the TypeDoc API reference ``` +The shared lint, format, and typecheck commands cover both packages. CLI tests +live in `tests/e2e/cli.test.ts` and build isolated package fixtures: + +```sh +pnpm test tests/e2e/cli.test.ts +``` + Make sure you've run `pnpm lint`, `pnpm test`, and `pnpm build` before opening a pull request. diff --git a/README.md b/README.md index 794406d..ec0f3ca 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ A graph-based system for preparing 3D assets for the web. > **⚠️ Notice:** This package is experimental. API is subject to change and not intended for production use. +Use the [library or command-line pipelines](docs/usage.md) to process assets. +The separate [CLI package](packages/cli/README.md) builds a pipeline from file +extensions and optional transform names. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/docs/usage.md b/docs/usage.md index e2c0206..2458b9d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,3 +1,56 @@ +# Command-line pipelines + +The separate `@babylonjs/node-assets-cli` package provides the `node-assets` command: + +```sh +node-assets pipeline input.gltf output.glb +node-assets pipeline input.glb ktx2 draco output.glb +``` + +The syntax is `node-assets pipeline [...blocks] `. +The first path selects the input block, the last selects the output block, and +the names between them select transform blocks in order. + +| Argument | Supported values | Behavior | +| --- | --- | --- | +| Input extension | `.gltf`, `.glb` | Read a local glTF or GLB with `GltfInputBlock`. | +| Output extension | `.glb` | Write GLB with `GltfOutputBlock`. | +| Transform | `draco` | Apply `EncodeDracoBlock` with library defaults. | +| Transform | `meshopt` | Apply `EncodeMeshoptBlock` with library defaults. | +| Transform | `ktx2` | Apply `EncodeKTX2Block` with library defaults. | + +Extensions are case-insensitive; block names are case-sensitive. Missing or +unsupported extensions are errors, including `.gltf` output. With no transforms, +the input connects directly to the output. This is a library read/write round +trip, not a byte-for-byte copy or a promise to retain input compression. + +Every transform occurrence creates a separate block. Order and repetitions are +preserved, including pipelines containing both geometry encoders. The CLI does +not impose combination restrictions; any library failure is reported instead. +Custom blocks, prefab aliases, and per-block settings are not supported yet. + +Paths are local filesystem paths relative to the caller's working directory. +The input must be a regular file; referenced glTF buffers and images are loaded +by the library. Quote paths containing spaces and use `--` before positional +arguments that begin with a hyphen. + +The output's parent directory must exist. Existing destinations are never +overwritten, including when the input and output are the same file. There is no +`--force` flag. Pipeline failures do not create an output file. + +`--help` / `-h` lists syntax, extensions, and blocks. Running without arguments +also shows help. `--version` / `-v` reports the CLI package version. These commands +do not run a pipeline. Success exits with status 0; invalid arguments, input, +pipeline, and output errors are reported to stderr and exit with status 1. + +To use the CLI from this repository before installing a published package: + +```sh +pnpm install +pnpm build +pnpm cli pipeline input.gltf ktx2 draco output.glb +``` + # Example: Hello, pipeline! Connect a `GltfInputBlock` to a `GltfOutputBlock`, then execute the resulting `NodeAsset`. diff --git a/eslint.config.mjs b/eslint.config.mjs index d4d91f2..9eeb686 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -63,7 +63,7 @@ const internalNamingPlugin = { export default tseslint.config( { - ignores: ["dist/**", "node_modules/**", "test-results/**", "docs/**", "**/*.md"], + ignores: ["**/dist/**", "**/node_modules/**", "test-results/**", "docs/**", "**/*.md"], }, js.configs.recommended, diff --git a/package.json b/package.json index 3b3a02a..e88788c 100644 --- a/package.json +++ b/package.json @@ -43,12 +43,14 @@ "access": "public" }, "scripts": { - "build": "vite build", + "build": "pnpm build:library && pnpm --filter @babylonjs/node-assets-cli build", + "build:library": "vite build", + "cli": "node packages/cli/bin/node-assets.cjs", "typecheck": "tsc -p tsconfig.json --noEmit", "lint": "eslint . && pnpm typecheck", "lint:fix": "eslint . --fix", - "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"*.config.ts\" \"*.config.mjs\"", - "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"*.config.ts\" \"*.config.mjs\"", + "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"*.config.ts\" \"*.config.mjs\" \"packages/cli/**/*.{ts,cjs}\"", + "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"*.config.ts\" \"*.config.mjs\" \"packages/cli/**/*.{ts,cjs}\"", "test": "vitest run", "test:watch": "vitest", "typedocs": "typedoc" diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/packages/cli/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..eee2b3c --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,46 @@ +# Node Assets CLI + +An experimental command-line interface to `@babylonjs/node-assets`. +Package name: `@babylonjs/node-assets-cli`. Executable name: `node-assets`. + +## Usage + +```sh +node-assets pipeline input.gltf output.glb +node-assets pipeline input.glb ktx2 draco output.glb +node-assets --help +``` + +The first path selects the input block (`.gltf` or `.glb`); the last selects the +output block (`.glb`). Extensions are case-insensitive. + +Optional `draco`, `meshopt`, and `ktx2` blocks use library defaults and run in the +given order. Repeated blocks and mixed encoders are passed through to the +library, including any errors. Without transforms, the CLI performs a library +read/write round trip, not a byte-for-byte copy. + +Paths are local and relative to your working directory. Quote paths with spaces; +use `--` before positionals that start with a hyphen. The input must be a regular +file, the destination's parent must exist, and existing files are never +overwritten. There is no `--force`. + +`--help` / `-h` and no arguments show help. `--version` / `-v` prints the CLI +version. Success exits with status 0; errors go to stderr with status 1. + +## Working in this repository + +Run these commands from the repository root: + +```sh +pnpm install +pnpm build +pnpm cli pipeline input.gltf ktx2 draco output.glb +``` + +This workflow does not require a published CLI release. See the +[usage guide](https://github.com/BabylonJS/Node-Assets/blob/main/docs/usage.md) +for the complete contract. + +## License + +[Apache-2.0](LICENSE) diff --git a/packages/cli/bin/node-assets.cjs b/packages/cli/bin/node-assets.cjs new file mode 100755 index 0000000..5c4d1fe --- /dev/null +++ b/packages/cli/bin/node-assets.cjs @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +if (require.main === module) { + import("../dist/cli.js") + .then(({ runCliAsync }) => runCliAsync(process.argv.slice(2))) + .catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + }); +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..b4f191a --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,35 @@ +{ + "name": "@babylonjs/node-assets-cli", + "version": "0.1.0", + "description": "Build and run Node Assets pipelines from the command line.", + "license": "Apache-2.0", + "type": "module", + "sideEffects": false, + "bin": { + "node-assets": "./bin/node-assets.cjs" + }, + "files": [ + "bin", + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/BabylonJS/Node-Assets.git", + "directory": "packages/cli" + }, + "bugs": { + "url": "https://github.com/BabylonJS/Node-Assets/issues" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "vite build" + }, + "dependencies": { + "@babylonjs/node-assets": "workspace:*" + } +} diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 0000000..f99051e --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,77 @@ +import { stat, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { parseArgs } from "node:util"; + +import { version } from "../package.json"; +import { createPipelineAsync, getPipelineDefinitions } from "./pipeline"; + +export async function runCliAsync(args: string[]): Promise { + const { values, positionals } = parseArgs({ + args, + allowPositionals: true, + strict: true, + options: { + help: { type: "boolean", short: "h" }, + version: { type: "boolean", short: "v" }, + }, + }); + + if (values.help || args.length === 0) { + printHelp(); + return; + } + if (values.version) { + console.log(version); + return; + } + + const [command, input, ...remaining] = positionals; + if (command !== "pipeline") { + throw new Error(`Unknown command "${command ?? ""}". Run node-assets --help for usage.`); + } + const output = remaining.pop(); + if (input === undefined || output === undefined) { + throw new Error("A pipeline requires an input file and an output file."); + } + + const inputPath = resolve(input); + const outputPath = resolve(output); + if (!(await stat(inputPath)).isFile()) { + throw new Error(`Input is not a regular file: ${inputPath}`); + } + + const asset = await createPipelineAsync({ inputPath, outputPath, blockNames: remaining }); + try { + const file = await asset.executeAsync(); + await writeFile(outputPath, new Uint8Array(await file.arrayBuffer()), { flag: "wx" }); + console.log(`Wrote ${outputPath}`); + } finally { + asset.dispose(); + } +} + +function printHelp(): void { + const { inputs, outputs, transforms } = getPipelineDefinitions(); + console.log( + [ + "Usage: node-assets pipeline [...blocks] ", + "", + `Input extensions: ${inputs.flatMap(({ extensions }) => extensions).join(", ")}`, + `Output extensions: ${outputs.flatMap(({ extensions }) => extensions).join(", ")}`, + "Extensions are case-insensitive. Paths are local and relative to the working directory.", + "", + "Blocks (case-sensitive, applied in order, repetitions allowed):", + ...transforms.map(({ name, description }) => ` ${name.padEnd(9)}${description}`), + "", + "Options:", + " -h, --help Show this help", + " -v, --version Show the CLI version", + " -- End options before hyphen-prefixed paths", + "", + "With no blocks, the input connects directly to the output.", + "The output parent must exist. Existing files are never overwritten.", + "", + "Example: node-assets pipeline input.gltf ktx2 draco output.glb", + ].join("\n") + ); +} diff --git a/packages/cli/src/pipeline.ts b/packages/cli/src/pipeline.ts new file mode 100644 index 0000000..6e2325a --- /dev/null +++ b/packages/cli/src/pipeline.ts @@ -0,0 +1,76 @@ +import { extname } from "node:path"; + +import type * as NodeAssets from "@babylonjs/node-assets"; + +export function getPipelineDefinitions() { + return { + inputs: [ + { + extensions: [".gltf", ".glb"], + create: (library: typeof NodeAssets, path: string) => new library.GltfInputBlock({ input: path }), + }, + ], + outputs: [ + { + extensions: [".glb"], + create: (library: typeof NodeAssets) => new library.GltfOutputBlock(), + }, + ], + transforms: [ + { + name: "draco", + description: "Compress geometry with Draco", + create: (library: typeof NodeAssets) => new library.EncodeDracoBlock(), + }, + { + name: "meshopt", + description: "Compress geometry with Meshopt", + create: (library: typeof NodeAssets) => new library.EncodeMeshoptBlock(), + }, + { + name: "ktx2", + description: "Encode textures as KTX2", + create: (library: typeof NodeAssets) => new library.EncodeKTX2Block(), + }, + ], + }; +} + +interface PipelineOptions { + readonly inputPath: string; + readonly outputPath: string; + readonly blockNames: readonly string[]; +} + +export async function createPipelineAsync({ inputPath, outputPath, blockNames }: PipelineOptions) { + const definitions = getPipelineDefinitions(); + const inputExtension = extname(inputPath).toLowerCase(); + const outputExtension = extname(outputPath).toLowerCase(); + const inputDefinition = definitions.inputs.find(({ extensions }) => extensions.includes(inputExtension)); + const outputDefinition = definitions.outputs.find(({ extensions }) => extensions.includes(outputExtension)); + if (inputDefinition === undefined) { + throw new Error(`Unsupported input extension "${inputExtension}".`); + } + if (outputDefinition === undefined) { + throw new Error(`Unsupported output extension "${outputExtension}".`); + } + const transforms = blockNames.map((name) => { + const definition = definitions.transforms.find((block) => block.name === name); + if (definition === undefined) { + throw new Error(`Unknown block "${name}".`); + } + return definition; + }); + + const library = await import("@babylonjs/node-assets"); + const source = inputDefinition.create(library, inputPath); + let previous = source.output; + for (const transform of transforms) { + const block = transform.create(library); + previous.connectTo(block.input); + previous = block.output; + } + const destination = outputDefinition.create(library); + previous.connectTo(destination.input); + return new library.NodeAsset({ name: "pipeline", outputBlock: destination }); +} diff --git a/packages/cli/vite.config.ts b/packages/cli/vite.config.ts new file mode 100644 index 0000000..1ac31fc --- /dev/null +++ b/packages/cli/vite.config.ts @@ -0,0 +1,21 @@ +import { isBuiltin } from "node:module"; +import { fileURLToPath } from "node:url"; + +import { defineConfig } from "vite"; + +export default defineConfig({ + root: fileURLToPath(new URL(".", import.meta.url)), + build: { + target: "node20", + minify: false, + sourcemap: true, + lib: { + entry: "src/cli.ts", + formats: ["es"], + fileName: () => "cli.js", + }, + rollupOptions: { + external: (id) => isBuiltin(id) || id === "@babylonjs/node-assets", + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b108a24..b2d3b6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,12 @@ importers: specifier: ^4.1.10 version: 4.1.10(@types/node@26.2.0)(vite@6.4.3(@types/node@26.2.0)(yaml@2.9.0)) + packages/cli: + dependencies: + '@babylonjs/node-assets': + specifier: workspace:* + version: link:../.. + packages: '@babel/helper-string-parser@7.29.7': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0c7f1e7..9592292 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,5 +6,8 @@ # a platform binary. pnpm blocks dependency build scripts by default and treats # an unapproved script as an install failure. The allowBuilds entry below permits # this script. +packages: + - packages/cli + allowBuilds: esbuild: true diff --git a/tests/e2e/cli.test.ts b/tests/e2e/cli.test.ts new file mode 100644 index 0000000..bdc1cef --- /dev/null +++ b/tests/e2e/cli.test.ts @@ -0,0 +1,243 @@ +import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import { join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { NodeIO } from "@gltf-transform/core"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import cliPackage from "../../packages/cli/package.json"; +import { EncodeDracoBlock, EncodeKTX2Block, EncodeMeshoptBlock, GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../src/index"; +import { buildCliFixtureAsync, runNodeAsync } from "../helpers/cli"; +import { expectKtx2Image, parseGlbAsync } from "../helpers/glb"; +import { generateGlbDataUri, generateGltfJson, generateTexturedGltfJson } from "../helpers/gltf"; + +describe("Node Assets CLI", () => { + let directory: string; + let launcher: string; + let input: string; + let texturedInput: string; + + beforeAll(async () => { + directory = await mkdtemp(fileURLToPath(new URL("../../node_modules/.node-assets-cli-", import.meta.url))); + launcher = await buildCliFixtureAsync(directory); + input = join(directory, "input.gltf"); + texturedInput = join(directory, "textured.gltf"); + await writeFile(input, generateGltfJson()); + await writeFile(texturedInput, generateTexturedGltfJson()); + const glbDataUri = generateGlbDataUri(); + await writeFile(join(directory, "input.glb"), Buffer.from(glbDataUri.slice(glbDataUri.indexOf(",") + 1), "base64")); + }, 120_000); + + afterAll(async () => { + if (directory !== undefined) { + await rm(directory, { recursive: true, force: true }); + } + }); + + it.each([[], ["--help"], ["-h"], ["pipeline", "--help"]].map((args) => ({ args })))("shows help for $args", async ({ args }) => { + const result = await runNodeAsync([launcher, ...args], directory); + expect(result.code).toBe(0); + expect(result.stderr).toBe(""); + for (const term of ["pipeline", ".gltf", ".glb", "draco", "meshopt", "ktx2"]) { + expect(result.stdout).toContain(term); + } + }); + + it.each(["--version", "-v"])("reports the package version with %s", async (flag) => { + const result = await runNodeAsync([launcher, flag], directory); + expect(result.code).toBe(0); + expect(result.stdout.trim()).toBe(cliPackage.version); + expect(result.stderr).toBe(""); + }); + + it.each( + [ + ["unknown"], + ["pipeline"], + ["pipeline", "input.gltf"], + ["pipeline", "input.gltf", "unknown", "output.glb"], + ["pipeline", "input.gltf", "DRACO", "output.glb"], + ["pipeline", "--unknown", "input.gltf", "output.glb"], + ["pipeline", "input.gltf", "--force", "output.glb"], + ["pipeline", "input.obj", "output.glb"], + ["pipeline", "input", "output.glb"], + ["pipeline", "input.gltf", "output.gltf"], + ["pipeline", "input.gltf", "output"], + ].map((args) => ({ args })) + )("rejects invalid arguments $args", async ({ args }) => { + const cwd = await mkdtemp(join(directory, "invalid-")); + await cp(input, join(cwd, "input.gltf")); + await cp(input, join(cwd, "input.obj")); + await cp(input, join(cwd, "input")); + const before = await readdir(cwd); + const result = await runNodeAsync([launcher, ...args], cwd); + expect(result.code).toBe(1); + expect(result.stderr.trim()).not.toBe(""); + expect(await readdir(cwd)).toEqual(before); + }); + + it.each(["gltf", "glb"])("round trips a local %s without transforms", async (extension) => { + const output = join(directory, `roundtrip-${extension}.glb`); + const result = await runNodeAsync([launcher, "pipeline", `input.${extension}`, output], directory); + expect(result.code).toBe(0); + const document = await new NodeIO().read(output); + expect(document.getRoot().listMeshes()[0]?.listPrimitives()[0]?.getAttribute("POSITION")?.getArray()).toEqual(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0])); + const parsed = await readGlbAsync(output); + expect(parsed.json.extensionsUsed ?? []).not.toContain("KHR_draco_mesh_compression"); + expect(parsed.json.extensionsUsed ?? []).not.toContain("EXT_meshopt_compression"); + }); + + it("resolves sibling glTF resources from the input path", async () => { + const sourceDirectory = join(directory, "external"); + await mkdir(sourceDirectory); + const io = new NodeIO(); + await io.write(join(sourceDirectory, "model.gltf"), await io.read(input)); + expect(await readdir(sourceDirectory)).toContain("model.bin"); + + const output = join(directory, "external.glb"); + const result = await runNodeAsync([launcher, "pipeline", "external/model.gltf", output], directory); + expect(result.code).toBe(0); + expect((await io.read(output)).getRoot().listMeshes()).toHaveLength(1); + }); + + it("accepts uppercase extensions and relative paths with spaces from another directory", async () => { + const cwd = join(directory, "working directory"); + await mkdir(cwd); + await cp(input, join(cwd, "source file.GLTF")); + const result = await runNodeAsync([launcher, "pipeline", "source file.GLTF", "output file.GLB"], cwd); + expect(result.code).toBe(0); + expect((await new NodeIO().read(join(cwd, "output file.GLB"))).getRoot().listMeshes()).toHaveLength(1); + }); + + it("accepts hyphen-prefixed paths after the option terminator", async () => { + const cwd = await mkdtemp(join(directory, "hyphens-")); + await cp(input, join(cwd, "-input.gltf")); + const result = await runNodeAsync([launcher, "--", "pipeline", "-input.gltf", "-output.glb"], cwd); + expect(result.code).toBe(0); + expect((await new NodeIO().read(join(cwd, "-output.glb"))).getRoot().listMeshes()).toHaveLength(1); + }); + + it.each([ + { blocks: ["draco"], geometry: "KHR_draco_mesh_compression", ktx2: false }, + { blocks: ["meshopt"], geometry: "EXT_meshopt_compression", ktx2: false }, + { blocks: ["ktx2"], geometry: undefined, ktx2: true }, + { blocks: ["ktx2", "draco"], geometry: "KHR_draco_mesh_compression", ktx2: true }, + { blocks: ["ktx2", "meshopt"], geometry: "EXT_meshopt_compression", ktx2: true }, + ])( + "encodes $blocks through the built package", + async ({ blocks, geometry, ktx2 }) => { + const output = join(directory, `${blocks.join("-")}.glb`); + const result = await runNodeAsync([launcher, "pipeline", texturedInput, ...blocks, output], directory); + expect(result.code).toBe(0); + const parsed = await readGlbAsync(output); + if (geometry !== undefined) { + expect(parsed.json.extensionsUsed).toContain(geometry); + if (geometry === "KHR_draco_mesh_compression") { + expect(parsed.json.meshes?.[0]?.primitives[0]?.extensions?.KHR_draco_mesh_compression).toBeDefined(); + } else { + expect(parsed.json.bufferViews?.some(({ extensions }) => extensions?.EXT_meshopt_compression !== undefined)).toBe(true); + } + } + if (ktx2) { + expect(parsed.json.extensionsUsed).toContain("KHR_texture_basisu"); + expectKtx2Image(parsed); + } + }, + 60_000 + ); + + it.each([ + { blocks: ["draco", "draco"], first: EncodeDracoBlock, second: EncodeDracoBlock }, + { blocks: ["ktx2", "ktx2"], first: EncodeKTX2Block, second: EncodeKTX2Block }, + { blocks: ["draco", "meshopt"], first: EncodeDracoBlock, second: EncodeMeshoptBlock }, + { blocks: ["meshopt", "draco"], first: EncodeMeshoptBlock, second: EncodeDracoBlock }, + ])( + "matches library behavior for $blocks without imposing restrictions", + async ({ blocks, first: FirstBlock, second: SecondBlock }) => { + const source = new GltfInputBlock({ input: texturedInput }); + const first = new FirstBlock(); + const second = new SecondBlock(); + const destination = new GltfOutputBlock(); + source.output.connectTo(first.input); + first.output.connectTo(second.input); + second.output.connectTo(destination.input); + const asset = new NodeAsset({ name: "reference", outputBlock: destination }); + const output = join(directory, `sequence-${blocks.join("-")}.glb`); + + try { + const [reference] = await Promise.allSettled([asset.executeAsync()]); + const result = await runNodeAsync([launcher, "pipeline", texturedInput, ...blocks, output], directory); + if (reference?.status === "fulfilled") { + expect(result.code).toBe(0); + expect((await readGlbAsync(output)).json).toEqual((await parseGlbAsync(reference.value)).json); + } else { + expect(result.code).toBe(1); + expect(result.stderr.trim()).not.toBe(""); + await expect(readFile(output)).rejects.toThrow(); + } + } finally { + asset.dispose(); + } + }, + 60_000 + ); + + it("does not overwrite an existing destination", async () => { + const output = join(directory, "existing.glb"); + const original = Buffer.from("keep this file"); + await writeFile(output, original); + const result = await runNodeAsync([launcher, "pipeline", input, output], directory); + expect(result.code).toBe(1); + expect(result.stderr.trim()).not.toBe(""); + expect(await readFile(output)).toEqual(original); + }); + + it("does not overwrite the input through an equivalent relative output path", async () => { + const source = join(directory, "input.glb"); + const original = await readFile(source); + const result = await runNodeAsync([launcher, "pipeline", source, relative(directory, source)], directory); + expect(result.code).toBe(1); + expect(await readFile(source)).toEqual(original); + }); + + it.each(["missing", "malformed", "directory"])("surfaces a %s input without creating output", async (kind) => { + const source = join(directory, `${kind}.gltf`); + const output = join(directory, `${kind}.glb`); + if (kind === "malformed") { + await writeFile(source, "not glTF"); + } else if (kind === "directory") { + await mkdir(source); + } + const result = await runNodeAsync([launcher, "pipeline", source, output], directory); + expect(result.code).toBe(1); + expect(result.stderr.trim()).not.toBe(""); + await expect(readFile(output)).rejects.toThrow(); + }); + + it("surfaces a missing output parent without creating directories", async () => { + const parent = join(directory, "missing-parent"); + const result = await runNodeAsync([launcher, "pipeline", input, join(parent, "output.glb")], directory); + expect(result.code).toBe(1); + expect(result.stderr.trim()).not.toBe(""); + await expect(readdir(parent)).rejects.toThrow(); + }); + + it("refuses a directory destination", async () => { + const output = join(directory, "destination.glb"); + await mkdir(output); + const result = await runNodeAsync([launcher, "pipeline", input, output], directory); + expect(result.code).toBe(1); + expect(result.stderr.trim()).not.toBe(""); + expect(await readdir(output)).toEqual([]); + }); + + it("does not start the CLI when the launcher is imported", async () => { + const result = await runNodeAsync(["--require", launcher, "--eval", ""], directory); + expect(result).toEqual({ code: 0, stdout: "", stderr: "" }); + }); +}); + +async function readGlbAsync(path: string) { + const bytes = await readFile(path); + return parseGlbAsync(new File([bytes], "scene.glb", { type: "model/gltf-binary" })); +} diff --git a/tests/helpers/cli.ts b/tests/helpers/cli.ts new file mode 100644 index 0000000..364bdce --- /dev/null +++ b/tests/helpers/cli.ts @@ -0,0 +1,54 @@ +import { execFile } from "node:child_process"; +import { cp, mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { build } from "vite"; + +import cliPackage from "../../packages/cli/package.json"; +import libraryConfig from "../../vite.config"; + +export async function buildCliFixtureAsync(directory: string): Promise { + const libraryDirectory = join(directory, "node_modules", "@babylonjs", "node-assets"); + const cliDirectory = join(directory, "node_modules", "@babylonjs", "node-assets-cli"); + await mkdir(libraryDirectory, { recursive: true }); + await mkdir(cliDirectory, { recursive: true }); + await cp(new URL("../../package.json", import.meta.url), join(libraryDirectory, "package.json")); + await cp(new URL("../../packages/cli/package.json", import.meta.url), join(cliDirectory, "package.json")); + await cp(new URL("../../packages/cli/bin", import.meta.url), join(cliDirectory, "bin"), { recursive: true }); + + await build({ + ...libraryConfig, + configFile: false, + logLevel: "silent", + // Declaration bundling targets the real package's dist; this isolated fixture only executes JavaScript. + plugins: (libraryConfig.plugins ?? []).filter((plugin) => !plugin || !("name" in plugin) || plugin.name !== "vite:dts"), + build: { ...libraryConfig.build, outDir: join(libraryDirectory, "dist") }, + }); + await build({ + configFile: fileURLToPath(new URL("../../packages/cli/vite.config.ts", import.meta.url)), + logLevel: "silent", + build: { outDir: join(cliDirectory, "dist") }, + }); + + return join(cliDirectory, cliPackage.bin["node-assets"]); +} + +interface CommandResult { + readonly code: number; + readonly stdout: string; + readonly stderr: string; +} + +export function runNodeAsync(args: readonly string[], cwd: string): Promise { + return new Promise((resolve, reject) => { + execFile(process.execPath, args, { cwd, timeout: 30_000 }, (error, stdout, stderr) => { + const code = error?.code ?? 0; + if (typeof code !== "number" || error?.killed) { + reject(error); + return; + } + resolve({ code, stdout, stderr }); + }); + }); +} diff --git a/tsconfig.json b/tsconfig.json index 24342b8..084383b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,11 +18,14 @@ "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, + "paths": { + "@babylonjs/node-assets": ["./src/index.ts"] + }, "declaration": true, "declarationMap": true, "sourceMap": true, "noEmit": true }, - "include": ["src", "tests", "*.config.ts", "*.config.mjs", "examples/blockDefinition.ts"] + "include": ["src", "tests", "packages/cli/src", "packages/cli/*.config.ts", "*.config.ts", "*.config.mjs", "examples/blockDefinition.ts"] } From 72861eed87516b8bb12d867b1c55fe74a3a2659b Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Thu, 17 Sep 2026 00:12:18 -0400 Subject: [PATCH 2/6] docs: tighten CLI documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 5 +- docs/usage.md | 53 ----------- packages/cli/LICENSE | 201 ----------------------------------------- packages/cli/README.md | 52 ++++++----- 4 files changed, 29 insertions(+), 282 deletions(-) delete mode 100644 packages/cli/LICENSE diff --git a/README.md b/README.md index ec0f3ca..ad0639b 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@ A graph-based system for preparing 3D assets for the web. > **⚠️ Notice:** This package is experimental. API is subject to change and not intended for production use. -Use the [library or command-line pipelines](docs/usage.md) to process assets. -The separate [CLI package](packages/cli/README.md) builds a pipeline from file -extensions and optional transform names. +See the [usage guide](docs/usage.md) for the library API or the +[CLI package](packages/cli/README.md) for terminal usage. ## Contributing diff --git a/docs/usage.md b/docs/usage.md index 2458b9d..e2c0206 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,56 +1,3 @@ -# Command-line pipelines - -The separate `@babylonjs/node-assets-cli` package provides the `node-assets` command: - -```sh -node-assets pipeline input.gltf output.glb -node-assets pipeline input.glb ktx2 draco output.glb -``` - -The syntax is `node-assets pipeline [...blocks] `. -The first path selects the input block, the last selects the output block, and -the names between them select transform blocks in order. - -| Argument | Supported values | Behavior | -| --- | --- | --- | -| Input extension | `.gltf`, `.glb` | Read a local glTF or GLB with `GltfInputBlock`. | -| Output extension | `.glb` | Write GLB with `GltfOutputBlock`. | -| Transform | `draco` | Apply `EncodeDracoBlock` with library defaults. | -| Transform | `meshopt` | Apply `EncodeMeshoptBlock` with library defaults. | -| Transform | `ktx2` | Apply `EncodeKTX2Block` with library defaults. | - -Extensions are case-insensitive; block names are case-sensitive. Missing or -unsupported extensions are errors, including `.gltf` output. With no transforms, -the input connects directly to the output. This is a library read/write round -trip, not a byte-for-byte copy or a promise to retain input compression. - -Every transform occurrence creates a separate block. Order and repetitions are -preserved, including pipelines containing both geometry encoders. The CLI does -not impose combination restrictions; any library failure is reported instead. -Custom blocks, prefab aliases, and per-block settings are not supported yet. - -Paths are local filesystem paths relative to the caller's working directory. -The input must be a regular file; referenced glTF buffers and images are loaded -by the library. Quote paths containing spaces and use `--` before positional -arguments that begin with a hyphen. - -The output's parent directory must exist. Existing destinations are never -overwritten, including when the input and output are the same file. There is no -`--force` flag. Pipeline failures do not create an output file. - -`--help` / `-h` lists syntax, extensions, and blocks. Running without arguments -also shows help. `--version` / `-v` reports the CLI package version. These commands -do not run a pipeline. Success exits with status 0; invalid arguments, input, -pipeline, and output errors are reported to stderr and exit with status 1. - -To use the CLI from this repository before installing a published package: - -```sh -pnpm install -pnpm build -pnpm cli pipeline input.gltf ktx2 draco output.glb -``` - # Example: Hello, pipeline! Connect a `GltfInputBlock` to a `GltfOutputBlock`, then execute the resulting `NodeAsset`. diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/packages/cli/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. diff --git a/packages/cli/README.md b/packages/cli/README.md index eee2b3c..e093b49 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,46 +1,48 @@ # Node Assets CLI -An experimental command-line interface to `@babylonjs/node-assets`. -Package name: `@babylonjs/node-assets-cli`. Executable name: `node-assets`. +`@babylonjs/node-assets-cli` builds and runs linear +[`@babylonjs/node-assets`](https://www.npmjs.com/package/@babylonjs/node-assets) +pipelines. The package installs the `node-assets` command. ## Usage ```sh node-assets pipeline input.gltf output.glb node-assets pipeline input.glb ktx2 draco output.glb -node-assets --help ``` -The first path selects the input block (`.gltf` or `.glb`); the last selects the -output block (`.glb`). Extensions are case-insensitive. +The command syntax is: -Optional `draco`, `meshopt`, and `ktx2` blocks use library defaults and run in the -given order. Repeated blocks and mixed encoders are passed through to the -library, including any errors. Without transforms, the CLI performs a library -read/write round trip, not a byte-for-byte copy. +```text +node-assets pipeline [...blocks] +``` + +The input extension selects the input block. The output extension selects the +output block. + +| Kind | Supported values | +| --- | --- | +| Input | `.gltf`, `.glb` | +| Output | `.glb` | +| Block | `draco`, `meshopt`, `ktx2` | + +The CLI creates each named block and connects the pipeline from left to right. +It preserves repeated blocks and mixed encoders. Without a block name, the CLI +reads the input and writes it as GLB. -Paths are local and relative to your working directory. Quote paths with spaces; -use `--` before positionals that start with a hyphen. The input must be a regular -file, the destination's parent must exist, and existing files are never -overwritten. There is no `--force`. +Paths are relative to the current working directory. The input must be a file, +and the output directory must exist. The CLI refuses to overwrite an existing +file. -`--help` / `-h` and no arguments show help. `--version` / `-v` prints the CLI -version. Success exits with status 0; errors go to stderr with status 1. +Run `node-assets --help` for command help or `node-assets --version` for the +installed version. -## Working in this repository +## Develop locally -Run these commands from the repository root: +From the repository root: ```sh pnpm install pnpm build pnpm cli pipeline input.gltf ktx2 draco output.glb ``` - -This workflow does not require a published CLI release. See the -[usage guide](https://github.com/BabylonJS/Node-Assets/blob/main/docs/usage.md) -for the complete contract. - -## License - -[Apache-2.0](LICENSE) From ee980e5641901818a1be62ed87ac5634a6599d31 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Thu, 17 Sep 2026 00:18:56 -0400 Subject: [PATCH 3/6] refactor: move core package Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 8 +- package.json | 66 +++------------- packages/core/README.md | 21 +++++ packages/core/package.json | 62 +++++++++++++++ {src => packages/core/src}/blocks/block.ts | 0 .../core/src}/blocks/blockDefinition.ts | 0 .../core/src}/blocks/encodeDracoBlock.ts | 0 .../core/src}/blocks/encodeKtx2Block.ts | 0 .../core/src}/blocks/encodeMeshoptBlock.ts | 0 .../core/src}/blocks/fbxInputBlock.ts | 0 .../core/src}/blocks/gltfInputBlock.ts | 0 .../core/src}/blocks/gltfOutputBlock.ts | 0 .../core/src}/blocks/objInputBlock.ts | 0 .../core/src}/blocks/stlInputBlock.ts | 0 .../src}/connectionPoints/connectionPoint.ts | 0 .../core/src}/connectionPoints/file.ts | 0 .../src}/connectionPoints/gltfDocument.ts | 0 .../core/src}/connectionPoints/url.ts | 0 .../helpers/convertBabylonSceneToDocument.ts | 0 .../core/src}/helpers/isNodeRuntime.ts | 0 .../core/src}/helpers/loadNodePackageFile.ts | 0 .../core/src}/helpers/loadSceneWithPlugin.ts | 0 {src => packages/core/src}/index.ts | 0 {src => packages/core/src}/nodeAsset.ts | 0 .../core/src}/nodeAssetContext.ts | 0 .../src}/resources/dracoEncoderResource.ts | 0 .../src}/resources/gltfDecoderResource.ts | 0 .../src}/resources/meshoptEncoderResource.ts | 0 .../core/src}/resources/nullEngineResource.ts | 0 .../core/src}/resources/platformIOResource.ts | 0 .../core/src}/resources/resource.ts | 0 .../core/src}/resources/resourceScope.ts | 0 {src => packages/core/src}/types/assets.d.ts | 0 .../core/tsconfig.build.json | 2 +- typedoc.json => packages/core/typedoc.json | 2 +- .../core/vite.config.ts | 2 + pnpm-lock.yaml | 77 +++++++++++-------- pnpm-workspace.yaml | 2 +- tests/bundle/browserConsumerBundle.test.ts | 6 +- tests/e2e/cli.test.ts | 4 +- tests/helpers/cli.ts | 4 +- tests/helpers/numberBlocks.ts | 4 +- .../integration/compressedGlbPipeline.test.ts | 2 +- tests/integration/dracoCompression.test.ts | 2 +- tests/integration/encodeKtx2.test.ts | 2 +- tests/integration/fbxInput.test.ts | 2 +- tests/integration/gltfInput.test.ts | 2 +- tests/integration/gltfOutput.test.ts | 2 +- tests/integration/meshoptCompression.test.ts | 2 +- tests/integration/objInput.test.ts | 2 +- tests/integration/stlInput.test.ts | 2 +- tests/unit/block.test.ts | 6 +- tests/unit/nodeAsset.test.ts | 8 +- tsconfig.json | 4 +- 54 files changed, 173 insertions(+), 123 deletions(-) create mode 100644 packages/core/README.md create mode 100644 packages/core/package.json rename {src => packages/core/src}/blocks/block.ts (100%) rename {src => packages/core/src}/blocks/blockDefinition.ts (100%) rename {src => packages/core/src}/blocks/encodeDracoBlock.ts (100%) rename {src => packages/core/src}/blocks/encodeKtx2Block.ts (100%) rename {src => packages/core/src}/blocks/encodeMeshoptBlock.ts (100%) rename {src => packages/core/src}/blocks/fbxInputBlock.ts (100%) rename {src => packages/core/src}/blocks/gltfInputBlock.ts (100%) rename {src => packages/core/src}/blocks/gltfOutputBlock.ts (100%) rename {src => packages/core/src}/blocks/objInputBlock.ts (100%) rename {src => packages/core/src}/blocks/stlInputBlock.ts (100%) rename {src => packages/core/src}/connectionPoints/connectionPoint.ts (100%) rename {src => packages/core/src}/connectionPoints/file.ts (100%) rename {src => packages/core/src}/connectionPoints/gltfDocument.ts (100%) rename {src => packages/core/src}/connectionPoints/url.ts (100%) rename {src => packages/core/src}/helpers/convertBabylonSceneToDocument.ts (100%) rename {src => packages/core/src}/helpers/isNodeRuntime.ts (100%) rename {src => packages/core/src}/helpers/loadNodePackageFile.ts (100%) rename {src => packages/core/src}/helpers/loadSceneWithPlugin.ts (100%) rename {src => packages/core/src}/index.ts (100%) rename {src => packages/core/src}/nodeAsset.ts (100%) rename {src => packages/core/src}/nodeAssetContext.ts (100%) rename {src => packages/core/src}/resources/dracoEncoderResource.ts (100%) rename {src => packages/core/src}/resources/gltfDecoderResource.ts (100%) rename {src => packages/core/src}/resources/meshoptEncoderResource.ts (100%) rename {src => packages/core/src}/resources/nullEngineResource.ts (100%) rename {src => packages/core/src}/resources/platformIOResource.ts (100%) rename {src => packages/core/src}/resources/resource.ts (100%) rename {src => packages/core/src}/resources/resourceScope.ts (100%) rename {src => packages/core/src}/types/assets.d.ts (100%) rename tsconfig.build.json => packages/core/tsconfig.build.json (78%) rename typedoc.json => packages/core/typedoc.json (84%) rename vite.config.ts => packages/core/vite.config.ts (95%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf7a41f..f4863de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,9 +14,9 @@ pnpm install pnpm build ``` -The library remains at the repository root; `packages/cli` is a separate pnpm -workspace package that depends on it. Build both packages before running the -local CLI: +The `packages/core` workspace contains the `@babylonjs/node-assets` library. +`packages/cli` contains the command-line package and depends on the core package. +Build both before running the local CLI: ```sh pnpm cli pipeline input.gltf ktx2 draco output.glb @@ -32,7 +32,7 @@ pnpm format # Write Prettier formatting pnpm test # Run Vitest pnpm test:watch # Run Vitest in watch mode pnpm build # Build the library, then the CLI -pnpm build:library # Build only the library into dist/ +pnpm build:core # Build only the library into packages/core/dist/ pnpm cli # Run the built CLI (append pipeline arguments) pnpm typedocs # Generate the TypeDoc API reference ``` diff --git a/package.json b/package.json index e88788c..59c066f 100644 --- a/package.json +++ b/package.json @@ -1,75 +1,29 @@ { - "name": "@babylonjs/node-assets", - "version": "0.1.0", + "name": "node-assets-workspace", + "private": true, "license": "Apache-2.0", - "type": "module", - "sideEffects": false, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./package.json": "./package.json" - }, - "files": [ - "dist" - ], - "keywords": [ - "babylon", - "babylonjs", - "3d", - "gltf", - "glb", - "asset-pipeline", - "draco", - "meshopt", - "ktx2" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/BabylonJS/Node-Assets.git" - }, - "bugs": { - "url": "https://github.com/BabylonJS/Node-Assets/issues" - }, - "homepage": "https://github.com/BabylonJS/Node-Assets#readme", "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" }, "packageManager": "pnpm@11.9.0", - "publishConfig": { - "access": "public" - }, "scripts": { - "build": "pnpm build:library && pnpm --filter @babylonjs/node-assets-cli build", - "build:library": "vite build", + "build": "pnpm --filter @babylonjs/node-assets build && pnpm --filter @babylonjs/node-assets-cli build", + "build:core": "pnpm --filter @babylonjs/node-assets build", "cli": "node packages/cli/bin/node-assets.cjs", "typecheck": "tsc -p tsconfig.json --noEmit", "lint": "eslint . && pnpm typecheck", "lint:fix": "eslint . --fix", - "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"*.config.ts\" \"*.config.mjs\" \"packages/cli/**/*.{ts,cjs}\"", - "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"*.config.ts\" \"*.config.mjs\" \"packages/cli/**/*.{ts,cjs}\"", + "format": "prettier --write \"packages/**/*.{ts,cjs}\" \"tests/**/*.ts\" \"*.config.ts\" \"*.config.mjs\"", + "format:check": "prettier --check \"packages/**/*.{ts,cjs}\" \"tests/**/*.ts\" \"*.config.ts\" \"*.config.mjs\"", "test": "vitest run", "test:watch": "vitest", - "typedocs": "typedoc" - }, - "dependencies": { - "@babylonjs/core": "^9.21.2", - "@babylonjs/loaders": "^9.21.2", - "@babylonjs/serializers": "^9.21.2", - "@gltf-transform/core": "4.5.0", - "@gltf-transform/extensions": "4.5.0", - "@gltf-transform/functions": "4.5.0", - "@types/draco3dgltf": "1.4.3", - "babylonpress-ktx2-encoder": "0.6.0", - "draco3dgltf": "1.5.7", - "meshoptimizer": "1.2.0", - "sharp": "0.35.4" + "typedocs": "pnpm --filter @babylonjs/node-assets typedocs" }, "devDependencies": { + "@babylonjs/node-assets": "workspace:*", "@eslint/js": "^10.0.1", + "@gltf-transform/core": "4.5.0", + "@gltf-transform/extensions": "4.5.0", "@types/node": "^26.1.1", "eslint": "^10.7.0", "eslint-config-prettier": "^10.1.8", diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..2571e08 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,21 @@ +# Node Assets + +`@babylonjs/node-assets` is a graph-based library for preparing 3D assets for +the web. + +> **Warning:** This package is experimental. Its API may change and is not +> intended for production use. + +See the [usage guide](https://github.com/BabylonJS/Node-Assets/blob/main/docs/usage.md) +for examples and the +[architecture notes](https://github.com/BabylonJS/Node-Assets/blob/main/docs/architecture/index.md) +for implementation details. + +## Contributing + +See the +[contribution guide](https://github.com/BabylonJS/Node-Assets/blob/main/CONTRIBUTING.md). + +## License + +[Apache-2.0](https://github.com/BabylonJS/Node-Assets/blob/main/LICENSE) diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..b169ffc --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,62 @@ +{ + "name": "@babylonjs/node-assets", + "version": "0.1.0", + "license": "Apache-2.0", + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "keywords": [ + "babylon", + "babylonjs", + "3d", + "gltf", + "glb", + "asset-pipeline", + "draco", + "meshopt", + "ktx2" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/BabylonJS/Node-Assets.git", + "directory": "packages/core" + }, + "bugs": { + "url": "https://github.com/BabylonJS/Node-Assets/issues" + }, + "homepage": "https://github.com/BabylonJS/Node-Assets#readme", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "vite build", + "typedocs": "typedoc" + }, + "dependencies": { + "@babylonjs/core": "^9.21.2", + "@babylonjs/loaders": "^9.21.2", + "@babylonjs/serializers": "^9.21.2", + "@gltf-transform/core": "4.5.0", + "@gltf-transform/extensions": "4.5.0", + "@gltf-transform/functions": "4.5.0", + "@types/draco3dgltf": "1.4.3", + "babylonpress-ktx2-encoder": "0.6.0", + "draco3dgltf": "1.5.7", + "meshoptimizer": "1.2.0", + "sharp": "0.35.4" + } +} diff --git a/src/blocks/block.ts b/packages/core/src/blocks/block.ts similarity index 100% rename from src/blocks/block.ts rename to packages/core/src/blocks/block.ts diff --git a/src/blocks/blockDefinition.ts b/packages/core/src/blocks/blockDefinition.ts similarity index 100% rename from src/blocks/blockDefinition.ts rename to packages/core/src/blocks/blockDefinition.ts diff --git a/src/blocks/encodeDracoBlock.ts b/packages/core/src/blocks/encodeDracoBlock.ts similarity index 100% rename from src/blocks/encodeDracoBlock.ts rename to packages/core/src/blocks/encodeDracoBlock.ts diff --git a/src/blocks/encodeKtx2Block.ts b/packages/core/src/blocks/encodeKtx2Block.ts similarity index 100% rename from src/blocks/encodeKtx2Block.ts rename to packages/core/src/blocks/encodeKtx2Block.ts diff --git a/src/blocks/encodeMeshoptBlock.ts b/packages/core/src/blocks/encodeMeshoptBlock.ts similarity index 100% rename from src/blocks/encodeMeshoptBlock.ts rename to packages/core/src/blocks/encodeMeshoptBlock.ts diff --git a/src/blocks/fbxInputBlock.ts b/packages/core/src/blocks/fbxInputBlock.ts similarity index 100% rename from src/blocks/fbxInputBlock.ts rename to packages/core/src/blocks/fbxInputBlock.ts diff --git a/src/blocks/gltfInputBlock.ts b/packages/core/src/blocks/gltfInputBlock.ts similarity index 100% rename from src/blocks/gltfInputBlock.ts rename to packages/core/src/blocks/gltfInputBlock.ts diff --git a/src/blocks/gltfOutputBlock.ts b/packages/core/src/blocks/gltfOutputBlock.ts similarity index 100% rename from src/blocks/gltfOutputBlock.ts rename to packages/core/src/blocks/gltfOutputBlock.ts diff --git a/src/blocks/objInputBlock.ts b/packages/core/src/blocks/objInputBlock.ts similarity index 100% rename from src/blocks/objInputBlock.ts rename to packages/core/src/blocks/objInputBlock.ts diff --git a/src/blocks/stlInputBlock.ts b/packages/core/src/blocks/stlInputBlock.ts similarity index 100% rename from src/blocks/stlInputBlock.ts rename to packages/core/src/blocks/stlInputBlock.ts diff --git a/src/connectionPoints/connectionPoint.ts b/packages/core/src/connectionPoints/connectionPoint.ts similarity index 100% rename from src/connectionPoints/connectionPoint.ts rename to packages/core/src/connectionPoints/connectionPoint.ts diff --git a/src/connectionPoints/file.ts b/packages/core/src/connectionPoints/file.ts similarity index 100% rename from src/connectionPoints/file.ts rename to packages/core/src/connectionPoints/file.ts diff --git a/src/connectionPoints/gltfDocument.ts b/packages/core/src/connectionPoints/gltfDocument.ts similarity index 100% rename from src/connectionPoints/gltfDocument.ts rename to packages/core/src/connectionPoints/gltfDocument.ts diff --git a/src/connectionPoints/url.ts b/packages/core/src/connectionPoints/url.ts similarity index 100% rename from src/connectionPoints/url.ts rename to packages/core/src/connectionPoints/url.ts diff --git a/src/helpers/convertBabylonSceneToDocument.ts b/packages/core/src/helpers/convertBabylonSceneToDocument.ts similarity index 100% rename from src/helpers/convertBabylonSceneToDocument.ts rename to packages/core/src/helpers/convertBabylonSceneToDocument.ts diff --git a/src/helpers/isNodeRuntime.ts b/packages/core/src/helpers/isNodeRuntime.ts similarity index 100% rename from src/helpers/isNodeRuntime.ts rename to packages/core/src/helpers/isNodeRuntime.ts diff --git a/src/helpers/loadNodePackageFile.ts b/packages/core/src/helpers/loadNodePackageFile.ts similarity index 100% rename from src/helpers/loadNodePackageFile.ts rename to packages/core/src/helpers/loadNodePackageFile.ts diff --git a/src/helpers/loadSceneWithPlugin.ts b/packages/core/src/helpers/loadSceneWithPlugin.ts similarity index 100% rename from src/helpers/loadSceneWithPlugin.ts rename to packages/core/src/helpers/loadSceneWithPlugin.ts diff --git a/src/index.ts b/packages/core/src/index.ts similarity index 100% rename from src/index.ts rename to packages/core/src/index.ts diff --git a/src/nodeAsset.ts b/packages/core/src/nodeAsset.ts similarity index 100% rename from src/nodeAsset.ts rename to packages/core/src/nodeAsset.ts diff --git a/src/nodeAssetContext.ts b/packages/core/src/nodeAssetContext.ts similarity index 100% rename from src/nodeAssetContext.ts rename to packages/core/src/nodeAssetContext.ts diff --git a/src/resources/dracoEncoderResource.ts b/packages/core/src/resources/dracoEncoderResource.ts similarity index 100% rename from src/resources/dracoEncoderResource.ts rename to packages/core/src/resources/dracoEncoderResource.ts diff --git a/src/resources/gltfDecoderResource.ts b/packages/core/src/resources/gltfDecoderResource.ts similarity index 100% rename from src/resources/gltfDecoderResource.ts rename to packages/core/src/resources/gltfDecoderResource.ts diff --git a/src/resources/meshoptEncoderResource.ts b/packages/core/src/resources/meshoptEncoderResource.ts similarity index 100% rename from src/resources/meshoptEncoderResource.ts rename to packages/core/src/resources/meshoptEncoderResource.ts diff --git a/src/resources/nullEngineResource.ts b/packages/core/src/resources/nullEngineResource.ts similarity index 100% rename from src/resources/nullEngineResource.ts rename to packages/core/src/resources/nullEngineResource.ts diff --git a/src/resources/platformIOResource.ts b/packages/core/src/resources/platformIOResource.ts similarity index 100% rename from src/resources/platformIOResource.ts rename to packages/core/src/resources/platformIOResource.ts diff --git a/src/resources/resource.ts b/packages/core/src/resources/resource.ts similarity index 100% rename from src/resources/resource.ts rename to packages/core/src/resources/resource.ts diff --git a/src/resources/resourceScope.ts b/packages/core/src/resources/resourceScope.ts similarity index 100% rename from src/resources/resourceScope.ts rename to packages/core/src/resources/resourceScope.ts diff --git a/src/types/assets.d.ts b/packages/core/src/types/assets.d.ts similarity index 100% rename from src/types/assets.d.ts rename to packages/core/src/types/assets.d.ts diff --git a/tsconfig.build.json b/packages/core/tsconfig.build.json similarity index 78% rename from tsconfig.build.json rename to packages/core/tsconfig.build.json index 7fbdbf6..ef9ce4f 100644 --- a/tsconfig.build.json +++ b/packages/core/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": "./tsconfig.json", + "extends": "../../tsconfig.json", "compilerOptions": { "noEmit": false, "outDir": "./dist", diff --git a/typedoc.json b/packages/core/typedoc.json similarity index 84% rename from typedoc.json rename to packages/core/typedoc.json index b06f248..21b7a9c 100644 --- a/typedoc.json +++ b/packages/core/typedoc.json @@ -1,7 +1,7 @@ { "$schema": "https://typedoc.org/schema.json", "entryPoints": ["src/index.ts"], - "out": "docs/typedocs", + "out": "../../docs/typedocs", "tsconfig": "tsconfig.build.json", "excludeInternal": true, "readme": "none" diff --git a/vite.config.ts b/packages/core/vite.config.ts similarity index 95% rename from vite.config.ts rename to packages/core/vite.config.ts index 34efd10..ff3a332 100644 --- a/vite.config.ts +++ b/packages/core/vite.config.ts @@ -1,4 +1,5 @@ import { isBuiltin } from "node:module"; +import { fileURLToPath } from "node:url"; import { defineConfig, type Plugin } from "vite"; import dts from "vite-plugin-dts"; @@ -6,6 +7,7 @@ import dts from "vite-plugin-dts"; const EmptyNodeBuiltinModuleId = "\0node-assets-empty-node-builtin"; export default defineConfig({ + root: fileURLToPath(new URL(".", import.meta.url)), base: "./", build: { target: "es2022", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2d3b6b..5fb5133 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,44 +7,19 @@ settings: importers: .: - dependencies: - '@babylonjs/core': - specifier: ^9.21.2 - version: 9.21.2 - '@babylonjs/loaders': - specifier: ^9.21.2 - version: 9.21.2(@babylonjs/core@9.21.2)(babylonjs-gltf2interface@9.21.2) - '@babylonjs/serializers': - specifier: ^9.21.2 - version: 9.21.2(@babylonjs/core@9.21.2)(babylonjs-gltf2interface@9.21.2) + devDependencies: + '@babylonjs/node-assets': + specifier: workspace:* + version: link:packages/core + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.8.1) '@gltf-transform/core': specifier: 4.5.0 version: 4.5.0 '@gltf-transform/extensions': specifier: 4.5.0 version: 4.5.0 - '@gltf-transform/functions': - specifier: 4.5.0 - version: 4.5.0(@types/node@26.2.0) - '@types/draco3dgltf': - specifier: 1.4.3 - version: 1.4.3 - babylonpress-ktx2-encoder: - specifier: 0.6.0 - version: 0.6.0 - draco3dgltf: - specifier: 1.5.7 - version: 1.5.7 - meshoptimizer: - specifier: 1.2.0 - version: 1.2.0 - sharp: - specifier: 0.35.4 - version: 0.35.4(@types/node@26.2.0) - devDependencies: - '@eslint/js': - specifier: ^10.0.1 - version: 10.0.1(eslint@10.8.1) '@types/node': specifier: ^26.1.1 version: 26.2.0 @@ -86,7 +61,43 @@ importers: dependencies: '@babylonjs/node-assets': specifier: workspace:* - version: link:../.. + version: link:../core + + packages/core: + dependencies: + '@babylonjs/core': + specifier: ^9.21.2 + version: 9.21.2 + '@babylonjs/loaders': + specifier: ^9.21.2 + version: 9.21.2(@babylonjs/core@9.21.2)(babylonjs-gltf2interface@9.21.2) + '@babylonjs/serializers': + specifier: ^9.21.2 + version: 9.21.2(@babylonjs/core@9.21.2)(babylonjs-gltf2interface@9.21.2) + '@gltf-transform/core': + specifier: 4.5.0 + version: 4.5.0 + '@gltf-transform/extensions': + specifier: 4.5.0 + version: 4.5.0 + '@gltf-transform/functions': + specifier: 4.5.0 + version: 4.5.0(@types/node@26.2.0) + '@types/draco3dgltf': + specifier: 1.4.3 + version: 1.4.3 + babylonpress-ktx2-encoder: + specifier: 0.6.0 + version: 0.6.0 + draco3dgltf: + specifier: 1.5.7 + version: 1.5.7 + meshoptimizer: + specifier: 1.2.0 + version: 1.2.0 + sharp: + specifier: 0.35.4 + version: 0.35.4(@types/node@26.2.0) packages: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9592292..ca3ae88 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,7 +7,7 @@ # an unapproved script as an install failure. The allowBuilds entry below permits # this script. packages: - - packages/cli + - packages/* allowBuilds: esbuild: true diff --git a/tests/bundle/browserConsumerBundle.test.ts b/tests/bundle/browserConsumerBundle.test.ts index 7e26a03..6bd29d1 100644 --- a/tests/bundle/browserConsumerBundle.test.ts +++ b/tests/bundle/browserConsumerBundle.test.ts @@ -4,13 +4,13 @@ import { pathToFileURL } from "node:url"; import { build, type Plugin } from "vite"; import { beforeAll, describe, expect, it, vi } from "vitest"; -import type * as NodeAssets from "../../src/index"; +import type * as NodeAssets from "../../packages/core/src/index"; import { parseGlbAsync } from "../helpers/glb"; import { generateGltfJson } from "../helpers/gltf"; const ConsumerModuleId = "\0node-assets-browser-consumer"; const PublishedPackageName = "@babylonjs/node-assets"; -const PublishedEntryPath = fileURLToPath(new URL("../../dist/index.js", import.meta.url)); +const PublishedEntryPath = fileURLToPath(new URL("../../packages/core/dist/index.js", import.meta.url)); describe("browser consumer bundle", () => { beforeAll(buildLibrary, 120_000); @@ -66,7 +66,7 @@ describe("browser consumer bundle", () => { async function buildLibrary(): Promise { await build({ - configFile: fileURLToPath(new URL("../../vite.config.ts", import.meta.url)), + configFile: fileURLToPath(new URL("../../packages/core/vite.config.ts", import.meta.url)), logLevel: "silent", }); } diff --git a/tests/e2e/cli.test.ts b/tests/e2e/cli.test.ts index bdc1cef..e97edaf 100644 --- a/tests/e2e/cli.test.ts +++ b/tests/e2e/cli.test.ts @@ -6,7 +6,7 @@ import { NodeIO } from "@gltf-transform/core"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import cliPackage from "../../packages/cli/package.json"; -import { EncodeDracoBlock, EncodeKTX2Block, EncodeMeshoptBlock, GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../src/index"; +import { EncodeDracoBlock, EncodeKTX2Block, EncodeMeshoptBlock, GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../packages/core/src/index"; import { buildCliFixtureAsync, runNodeAsync } from "../helpers/cli"; import { expectKtx2Image, parseGlbAsync } from "../helpers/glb"; import { generateGlbDataUri, generateGltfJson, generateTexturedGltfJson } from "../helpers/gltf"; @@ -18,7 +18,7 @@ describe("Node Assets CLI", () => { let texturedInput: string; beforeAll(async () => { - directory = await mkdtemp(fileURLToPath(new URL("../../node_modules/.node-assets-cli-", import.meta.url))); + directory = await mkdtemp(fileURLToPath(new URL("../../packages/core/node_modules/.node-assets-cli-", import.meta.url))); launcher = await buildCliFixtureAsync(directory); input = join(directory, "input.gltf"); texturedInput = join(directory, "textured.gltf"); diff --git a/tests/helpers/cli.ts b/tests/helpers/cli.ts index 364bdce..53875a5 100644 --- a/tests/helpers/cli.ts +++ b/tests/helpers/cli.ts @@ -6,14 +6,14 @@ import { fileURLToPath } from "node:url"; import { build } from "vite"; import cliPackage from "../../packages/cli/package.json"; -import libraryConfig from "../../vite.config"; +import libraryConfig from "../../packages/core/vite.config"; export async function buildCliFixtureAsync(directory: string): Promise { const libraryDirectory = join(directory, "node_modules", "@babylonjs", "node-assets"); const cliDirectory = join(directory, "node_modules", "@babylonjs", "node-assets-cli"); await mkdir(libraryDirectory, { recursive: true }); await mkdir(cliDirectory, { recursive: true }); - await cp(new URL("../../package.json", import.meta.url), join(libraryDirectory, "package.json")); + await cp(new URL("../../packages/core/package.json", import.meta.url), join(libraryDirectory, "package.json")); await cp(new URL("../../packages/cli/package.json", import.meta.url), join(cliDirectory, "package.json")); await cp(new URL("../../packages/cli/bin", import.meta.url), join(cliDirectory, "bin"), { recursive: true }); diff --git a/tests/helpers/numberBlocks.ts b/tests/helpers/numberBlocks.ts index 1240c68..d1c8e71 100644 --- a/tests/helpers/numberBlocks.ts +++ b/tests/helpers/numberBlocks.ts @@ -1,5 +1,5 @@ -import { defineBlock, enumValue } from "../../src/blocks/blockDefinition"; -import { defineConnectionPointType } from "../../src/connectionPoints/connectionPoint"; +import { defineBlock, enumValue } from "../../packages/core/src/blocks/blockDefinition"; +import { defineConnectionPointType } from "../../packages/core/src/connectionPoints/connectionPoint"; export const NumberType = defineConnectionPointType("number", (value): value is number => typeof value === "number"); export const OtherNumberType = defineConnectionPointType("number", (value): value is number => typeof value === "number"); diff --git a/tests/integration/compressedGlbPipeline.test.ts b/tests/integration/compressedGlbPipeline.test.ts index c97155d..c24e34a 100644 --- a/tests/integration/compressedGlbPipeline.test.ts +++ b/tests/integration/compressedGlbPipeline.test.ts @@ -1,6 +1,6 @@ import { describe, expect, expectTypeOf, it, vi } from "vitest"; -import { EncodeDracoBlock, EncodeKTX2Block, EncodeMeshoptBlock, GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../src/index"; +import { EncodeDracoBlock, EncodeKTX2Block, EncodeMeshoptBlock, GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../packages/core/src/index"; import { expectKtx2Image, parseGlbAsync } from "../helpers/glb"; import { generateTexturedGltfJson } from "../helpers/gltf"; diff --git a/tests/integration/dracoCompression.test.ts b/tests/integration/dracoCompression.test.ts index 8fc3142..70807f1 100644 --- a/tests/integration/dracoCompression.test.ts +++ b/tests/integration/dracoCompression.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { EncodeDracoBlock, GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../src/index"; +import { EncodeDracoBlock, GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../packages/core/src/index"; import { parseGlbAsync } from "../helpers/glb"; import { generateGltfJson } from "../helpers/gltf"; diff --git a/tests/integration/encodeKtx2.test.ts b/tests/integration/encodeKtx2.test.ts index 7898fb1..c451c9f 100644 --- a/tests/integration/encodeKtx2.test.ts +++ b/tests/integration/encodeKtx2.test.ts @@ -2,7 +2,7 @@ import { Document } from "@gltf-transform/core"; import { EXTTextureWebP } from "@gltf-transform/extensions"; import { describe, expect, it, vi } from "vitest"; -import { EncodeKTX2Block, FbxInputBlock, GltfInputBlock, GltfOutputBlock, NodeAsset, ObjInputBlock } from "../../src/index"; +import { EncodeKTX2Block, FbxInputBlock, GltfInputBlock, GltfOutputBlock, NodeAsset, ObjInputBlock } from "../../packages/core/src/index"; import { generateTexturedFbxDataWithUvs } from "../helpers/fbx"; import { expectKtx2Image, getTextureImageIndex, parseGlbAsync } from "../helpers/glb"; import { generateTexturedGltfJson } from "../helpers/gltf"; diff --git a/tests/integration/fbxInput.test.ts b/tests/integration/fbxInput.test.ts index b5a1c55..249211a 100644 --- a/tests/integration/fbxInput.test.ts +++ b/tests/integration/fbxInput.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { FbxInputBlock, GltfOutputBlock, NodeAsset, NodeAssetContext } from "../../src/index"; +import { FbxInputBlock, GltfOutputBlock, NodeAsset, NodeAssetContext } from "../../packages/core/src/index"; import { generateBinaryFbxData, generateFbxData, generateFbxDataUri, generateTexturedFbxDataWithUvs, generateTgaTextureData } from "../helpers/fbx"; import { parseGlbAsync } from "../helpers/glb"; diff --git a/tests/integration/gltfInput.test.ts b/tests/integration/gltfInput.test.ts index 5f2b738..c2fd962 100644 --- a/tests/integration/gltfInput.test.ts +++ b/tests/integration/gltfInput.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { GltfInputBlock, NodeAsset, NodeAssetContext } from "../../src/index"; +import { GltfInputBlock, NodeAsset, NodeAssetContext } from "../../packages/core/src/index"; import { decodeDataUri, generateGlbDataUri, generateGltfJson } from "../helpers/gltf"; describe("glTF input", () => { diff --git a/tests/integration/gltfOutput.test.ts b/tests/integration/gltfOutput.test.ts index aac2134..baa504f 100644 --- a/tests/integration/gltfOutput.test.ts +++ b/tests/integration/gltfOutput.test.ts @@ -1,6 +1,6 @@ import { describe, it, vi } from "vitest"; -import { GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../src/index"; +import { GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../packages/core/src/index"; import { parseGlbAsync } from "../helpers/glb"; import { generateGltfJson } from "../helpers/gltf"; diff --git a/tests/integration/meshoptCompression.test.ts b/tests/integration/meshoptCompression.test.ts index 4b85e71..5686e75 100644 --- a/tests/integration/meshoptCompression.test.ts +++ b/tests/integration/meshoptCompression.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { EncodeMeshoptBlock, GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../src/index"; +import { EncodeMeshoptBlock, GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../packages/core/src/index"; import { parseGlbAsync } from "../helpers/glb"; import { generateGltfJson } from "../helpers/gltf"; diff --git a/tests/integration/objInput.test.ts b/tests/integration/objInput.test.ts index 87deee6..03a54c6 100644 --- a/tests/integration/objInput.test.ts +++ b/tests/integration/objInput.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { GltfOutputBlock, NodeAsset, ObjInputBlock } from "../../src/index"; +import { GltfOutputBlock, NodeAsset, ObjInputBlock } from "../../packages/core/src/index"; import { parseGlbAsync } from "../helpers/glb"; import { generateMtlData, generateObjDataUri, generateObjData, generateTexturedObjData, generateTextureData } from "../helpers/obj"; diff --git a/tests/integration/stlInput.test.ts b/tests/integration/stlInput.test.ts index f3f1a96..9deee11 100644 --- a/tests/integration/stlInput.test.ts +++ b/tests/integration/stlInput.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { GltfOutputBlock, NodeAsset, NodeAssetContext, StlInputBlock } from "../../src/index"; +import { GltfOutputBlock, NodeAsset, NodeAssetContext, StlInputBlock } from "../../packages/core/src/index"; import { parseGlbAsync } from "../helpers/glb"; import { generateBinaryStlData, generateStlData, generateStlDataUri } from "../helpers/stl"; diff --git a/tests/unit/block.test.ts b/tests/unit/block.test.ts index c554b97..d96915d 100644 --- a/tests/unit/block.test.ts +++ b/tests/unit/block.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; -import { Block } from "../../src/blocks/block"; -import { defineBlock, value } from "../../src/blocks/blockDefinition"; -import { NodeAsset } from "../../src/nodeAsset"; +import { Block } from "../../packages/core/src/blocks/block"; +import { defineBlock, value } from "../../packages/core/src/blocks/blockDefinition"; +import { NodeAsset } from "../../packages/core/src/nodeAsset"; import { ScaleDefinition, NumberDefinition, OtherNumberDefinition, NumberType } from "../helpers/numberBlocks"; describe("Block class", () => { diff --git a/tests/unit/nodeAsset.test.ts b/tests/unit/nodeAsset.test.ts index bb1af25..6743805 100644 --- a/tests/unit/nodeAsset.test.ts +++ b/tests/unit/nodeAsset.test.ts @@ -1,9 +1,9 @@ import { describe, expect, expectTypeOf, it } from "vitest"; -import { Block } from "../../src/blocks/block"; -import { defineBlock, defineSourceBlock } from "../../src/blocks/blockDefinition"; -import { NodeAsset, NodeAssetContext } from "../../src/index"; -import type { Resource } from "../../src/resources/resource"; +import { Block } from "../../packages/core/src/blocks/block"; +import { defineBlock, defineSourceBlock } from "../../packages/core/src/blocks/blockDefinition"; +import { NodeAsset, NodeAssetContext } from "../../packages/core/src/index"; +import type { Resource } from "../../packages/core/src/resources/resource"; import { ScaleDefinition, NumberDefinition } from "../helpers/numberBlocks"; describe("NodeAsset", () => { diff --git a/tsconfig.json b/tsconfig.json index 084383b..3e9c206 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "resolveJsonModule": true, "skipLibCheck": true, "paths": { - "@babylonjs/node-assets": ["./src/index.ts"] + "@babylonjs/node-assets": ["./packages/core/src/index.ts"] }, "declaration": true, @@ -27,5 +27,5 @@ "sourceMap": true, "noEmit": true }, - "include": ["src", "tests", "packages/cli/src", "packages/cli/*.config.ts", "*.config.ts", "*.config.mjs", "examples/blockDefinition.ts"] + "include": ["packages/**/*.ts", "tests", "*.config.ts", "*.config.mjs", "examples/blockDefinition.ts"] } From d4328d9aaf22fdc0256eec99a03f40de9d0945a6 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Thu, 17 Sep 2026 00:23:26 -0400 Subject: [PATCH 4/6] test: remove launcher implementation check Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/e2e/cli.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/e2e/cli.test.ts b/tests/e2e/cli.test.ts index e97edaf..694c9e3 100644 --- a/tests/e2e/cli.test.ts +++ b/tests/e2e/cli.test.ts @@ -230,11 +230,6 @@ describe("Node Assets CLI", () => { expect(result.stderr.trim()).not.toBe(""); expect(await readdir(output)).toEqual([]); }); - - it("does not start the CLI when the launcher is imported", async () => { - const result = await runNodeAsync(["--require", launcher, "--eval", ""], directory); - expect(result).toEqual({ code: 0, stdout: "", stderr: "" }); - }); }); async function readGlbAsync(path: string) { From ee5a533a6540d54a1fc3b7092975ca2b1fae0402 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Thu, 17 Sep 2026 00:30:28 -0400 Subject: [PATCH 5/6] docs: remove npm link Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- packages/cli/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index e093b49..c68963a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,7 +1,6 @@ # Node Assets CLI -`@babylonjs/node-assets-cli` builds and runs linear -[`@babylonjs/node-assets`](https://www.npmjs.com/package/@babylonjs/node-assets) +`@babylonjs/node-assets-cli` builds and runs linear `@babylonjs/node-assets` pipelines. The package installs the `node-assets` command. ## Usage From ddebc651b5929e0193303100b6c0029f0e5141c3 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Thu, 17 Sep 2026 02:10:45 -0400 Subject: [PATCH 6/6] chore: organize core documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .gitignore | 2 +- .prettierignore | 2 +- AGENTS.md | 10 +++- README.md | 5 +- eslint.config.mjs | 2 +- packages/cli/README.md | 32 +++++----- packages/cli/package.json | 6 +- packages/cli/src/cli.ts | 18 +++--- packages/cli/src/pipeline.ts | 6 +- packages/core/README.md | 12 ++-- packages/core/docs/basics.md | 57 ++++++++++++++++++ .../index.md => packages/core/docs/blocks.md | 58 +------------------ {docs => packages/core/docs}/usage.md | 0 packages/core/package.json | 6 +- packages/core/typedoc.json | 2 +- 15 files changed, 103 insertions(+), 115 deletions(-) create mode 100644 packages/core/docs/basics.md rename docs/architecture/index.md => packages/core/docs/blocks.md (52%) rename {docs => packages/core/docs}/usage.md (100%) diff --git a/.gitignore b/.gitignore index 86f13c5..d560b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ package-lock.json yarn.lock dist/ -docs/typedocs +packages/core/docs/typedocs test-results/ *.tsbuildinfo .vite/ diff --git a/.prettierignore b/.prettierignore index c907ed1..e8be54f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,5 @@ dist/ node_modules/ test-results/ -docs/typedocs +packages/core/docs/typedocs pnpm-lock.yaml diff --git a/AGENTS.md b/AGENTS.md index d2eac67..5691312 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,13 +4,17 @@ Guidance for coding agents working in this repository. ## What this is +This is a monorepo containing multiple packages related to 3D asset processing. + `@babylonjs/node-assets` is an experimental TypeScript library for reading many 3D source formats and producing web-ready formats. The package targets Node and browser environments and bundles all required converters and compressors. +`@babylonjs/node-assets-cli` is a command-line interface for building and running pipelines with `@babylonjs/node-assets`. + ## Getting started - Read [[CONTRIBUTING.md]] for setup and scripts. -- Read [[docs/usage.md]] for user-facing behavior contracts. -- Read [[docs/architecture/index.md]] for intended implementation details and more behavior contracts. +- Read [[packages/core/docs/usage.md]] for user-facing behavior contracts. +- Read [[packages/core/docs/basics.md]] and [[packages/core/docs/blocks.md]] for intended implementation details and more behavior contracts. ## Guidelines @@ -21,7 +25,7 @@ Guidance for coding agents working in this repository. ## Planning -- To propose a feature, first update or add the smallest task-focused guide, section, or note in [[docs/usage.md]]. +- To propose a feature, first update or add the smallest task-focused guide, section, or note in [[packages/core/docs/usage.md]]. ## Style diff --git a/README.md b/README.md index ad0639b..5625d40 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,9 @@ # Node Assets -A graph-based system for preparing 3D assets for the web. +A TypeScript library to help prepare 3D assets for the web. > **⚠️ Notice:** This package is experimental. API is subject to change and not intended for production use. -See the [usage guide](docs/usage.md) for the library API or the -[CLI package](packages/cli/README.md) for terminal usage. - ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/eslint.config.mjs b/eslint.config.mjs index 9eeb686..4a9cbca 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -63,7 +63,7 @@ const internalNamingPlugin = { export default tseslint.config( { - ignores: ["**/dist/**", "**/node_modules/**", "test-results/**", "docs/**", "**/*.md"], + ignores: ["**/dist/**", "**/node_modules/**", "test-results/**", "docs/**", "packages/core/docs/typedocs/**", "**/*.md"], }, js.configs.recommended, diff --git a/packages/cli/README.md b/packages/cli/README.md index c68963a..a6e5f75 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,10 +1,15 @@ # Node Assets CLI -`@babylonjs/node-assets-cli` builds and runs linear `@babylonjs/node-assets` -pipelines. The package installs the `node-assets` command. +A command-line interface for building and running pipelines with `@babylonjs/node-assets`. + +> **⚠️ Notice:** This package is experimental. API is subject to change and not intended for production use. ## Usage +### Pipelines + +A pipeline is a sequence of operations applied to a 3D asset. The CLI allows you to define and run pipelines using the `node-assets pipeline` command. + ```sh node-assets pipeline input.gltf output.glb node-assets pipeline input.glb ktx2 draco output.glb @@ -13,25 +18,16 @@ node-assets pipeline input.glb ktx2 draco output.glb The command syntax is: ```text -node-assets pipeline [...blocks] +node-assets pipeline [operation...] ``` -The input extension selects the input block. The output extension selects the -output block. - -| Kind | Supported values | -| --- | --- | -| Input | `.gltf`, `.glb` | -| Output | `.glb` | -| Block | `draco`, `meshopt`, `ktx2` | - -The CLI creates each named block and connects the pipeline from left to right. -It preserves repeated blocks and mixed encoders. Without a block name, the CLI -reads the input and writes it as GLB. +| Element | Supported values | +| --------- | -------------------------- | +| Input | `.gltf`, `.glb` | +| Output | `.glb` | +| Operation | `draco`, `meshopt`, `ktx2` | -Paths are relative to the current working directory. The input must be a file, -and the output directory must exist. The CLI refuses to overwrite an existing -file. +Without specifying operations, the CLI reads the input and writes it back out as the target output format. Run `node-assets --help` for command help or `node-assets --version` for the installed version. diff --git a/packages/cli/package.json b/packages/cli/package.json index b4f191a..1813e5b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,7 @@ { "name": "@babylonjs/node-assets-cli", - "version": "0.1.0", + "private": true, + "version": "0.0.0", "description": "Build and run Node Assets pipelines from the command line.", "license": "Apache-2.0", "type": "module", @@ -23,9 +24,6 @@ "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" }, - "publishConfig": { - "access": "public" - }, "scripts": { "build": "vite build" }, diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index f99051e..812b5b7 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -51,26 +51,24 @@ export async function runCliAsync(args: string[]): Promise { } function printHelp(): void { - const { inputs, outputs, transforms } = getPipelineDefinitions(); + const { inputs, outputs, operations } = getPipelineDefinitions(); console.log( [ - "Usage: node-assets pipeline [...blocks] ", + "Usage: node-assets pipeline [operations...] ", "", - `Input extensions: ${inputs.flatMap(({ extensions }) => extensions).join(", ")}`, - `Output extensions: ${outputs.flatMap(({ extensions }) => extensions).join(", ")}`, - "Extensions are case-insensitive. Paths are local and relative to the working directory.", + `Supported file types:`, + `Input: ${inputs.flatMap(({ extensions }) => extensions).join(", ")}`, + `Output: ${outputs.flatMap(({ extensions }) => extensions).join(", ")}`, + "Paths are local and relative to the working directory.", "", - "Blocks (case-sensitive, applied in order, repetitions allowed):", - ...transforms.map(({ name, description }) => ` ${name.padEnd(9)}${description}`), + "Operations:", + ...operations.map(({ name, description }) => ` ${name.padEnd(9)}${description}`), "", "Options:", " -h, --help Show this help", " -v, --version Show the CLI version", " -- End options before hyphen-prefixed paths", "", - "With no blocks, the input connects directly to the output.", - "The output parent must exist. Existing files are never overwritten.", - "", "Example: node-assets pipeline input.gltf ktx2 draco output.glb", ].join("\n") ); diff --git a/packages/cli/src/pipeline.ts b/packages/cli/src/pipeline.ts index 6e2325a..c4da63f 100644 --- a/packages/cli/src/pipeline.ts +++ b/packages/cli/src/pipeline.ts @@ -16,7 +16,7 @@ export function getPipelineDefinitions() { create: (library: typeof NodeAssets) => new library.GltfOutputBlock(), }, ], - transforms: [ + operations: [ { name: "draco", description: "Compress geometry with Draco", @@ -55,9 +55,9 @@ export async function createPipelineAsync({ inputPath, outputPath, blockNames }: throw new Error(`Unsupported output extension "${outputExtension}".`); } const transforms = blockNames.map((name) => { - const definition = definitions.transforms.find((block) => block.name === name); + const definition = definitions.operations.find((block) => block.name === name); if (definition === undefined) { - throw new Error(`Unknown block "${name}".`); + throw new Error(`Unknown operation "${name}".`); } return definition; }); diff --git a/packages/core/README.md b/packages/core/README.md index 2571e08..a651642 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,15 +1,11 @@ # Node Assets -`@babylonjs/node-assets` is a graph-based library for preparing 3D assets for -the web. +A TypeScript library to help prepare 3D assets for the web, with converters & compressors included. -> **Warning:** This package is experimental. Its API may change and is not -> intended for production use. +> **⚠️ Notice:** This package is experimental. API is subject to change and not intended for production use. -See the [usage guide](https://github.com/BabylonJS/Node-Assets/blob/main/docs/usage.md) -for examples and the -[architecture notes](https://github.com/BabylonJS/Node-Assets/blob/main/docs/architecture/index.md) -for implementation details. +See the [usage guide](docs/usage.md) for examples. [Basics](docs/basics.md) and +[blocks](docs/blocks.md) describe the implementation. ## Contributing diff --git a/packages/core/docs/basics.md b/packages/core/docs/basics.md new file mode 100644 index 0000000..5dd7ada --- /dev/null +++ b/packages/core/docs/basics.md @@ -0,0 +1,57 @@ +# Graph Terminology + +Below is a rough translation guide for common graph terminology to Babylon node editor terminology. + +A graph is a **NodeAsset**. +Nodes are referred to as **blocks** in code contexts and **nodes** in UI contexts. +Sink nodes, or terminal nodes, are **output blocks**. +Edges are **connections**. +End points of an edge are **connection points** in code contexts and **ports** in UI contexts. +The payload carried along an edge is **runtime data**: what a block processes. +The type of payload accepted by an end points is defined by its **connection point type**. +Edges can only be drawn between compatible **connection point types**. +Inbound end points are block **inputs**. +Outbound end points are block **outputs**. + +# Connection Point Types + +Connection points types, in general, come in two forms. + +- File: for format- or byte-level operations. Examples: platform I/O (future) +- Content: for content-level operations. Examples: removing vertices, updating texture pixels + +## Content + +- `Babylon` (future) + - Runtime data: `Scene` (@babylonjs/core) (future) +- `glTF` + - Runtime data: `Document` (@gltf-transform/core) + +Runtime data is passed by reference. + +## File + +(Future) + +# Resources + +Resources are reusable values owned by a pipeline execution's resource scope, such as a shared `PlatformIO` instance. They are created on demand and shared by blocks within that execution. Blocks borrow resources; the scope retains them until execution completes or fails, then performs any required cleanup and releases its references. + +Worker-backed encoding is future work. + +# Blocks + +Blocks are broadly categorized as follows. + +1. Inputs: data. User supplies a value; then, graph-managed data flows out. +2. Mutators: functions. Data is managed by graph in both directions, in and out. +3. Outputs: data. Graph-managed data in, user-facing data out. + +Some other categories: + +- Files +- Selectors +- Transforms: N -> N +- Transcoders: N -> U + +Blocks have input and output connection points. Some might also have additional, optional input connection points. diff --git a/docs/architecture/index.md b/packages/core/docs/blocks.md similarity index 52% rename from docs/architecture/index.md rename to packages/core/docs/blocks.md index 2960dc8..5ba394f 100644 --- a/docs/architecture/index.md +++ b/packages/core/docs/blocks.md @@ -1,60 +1,4 @@ -# Graph Terminology - -Below is a rough translation guide for common graph terminology to Babylon node editor terminology. - -A graph is a **NodeAsset**. -Nodes are referred to as **blocks** in code contexts and **nodes** in UI contexts. -Sink nodes, or terminal nodes, are **output blocks**. -Edges are **connections**. -End points of an edge are **connection points** in code contexts and **ports** in UI contexts. -The payload carried along an edge is **runtime data**: what a block processes. -The type of payload accepted by an end points is defined by its **connection point type**. -Edges can only be drawn between compatible **connection point types**. -Inbound end points are block **inputs**. -Outbound end points are block **outputs**. - -# Connection Point Types - -Connection points types, in general, come in two forms. - -- File: for format- or byte-level operations. Examples: platform I/O (future) -- Content: for content-level operations. Examples: removing vertices, updating texture pixels - -## Content - -- `Babylon` (future) - - Runtime data: `Scene` (@babylonjs/core) -- `glTF` - - Runtime data: `Document` (@gltf-transform/core) - -Runtime data is passed by reference. - -## File - -(Future) - -# Resources - -Resources are reusable values owned by a pipeline execution's resource scope, such as a shared `PlatformIO` instance. They are created on demand and shared by blocks within that execution. Blocks borrow resources; the scope retains them until execution completes or fails, then performs any required cleanup and releases its references. - -Worker-backed encoding is future work. - -# Blocks - -Blocks are broadly categorized as follows. - -1. Inputs: data. User supplies a value; then, graph-managed data flows out. -2. Mutators: functions. Data is managed by graph in both directions, in and out. -3. Outputs: data. Graph-managed data in, user-facing data out. - -Some other categories: - -- Files -- Selectors -- Transforms: N -> N -- Transcoders: N -> U - -Blocks have input and output connection points. Some might also have additional, optional input connection points. +# List of Blocks > **Uses** lists a block's implementation dependencies: libraries, functions, modules, and instances. These may include execution-scoped resources. diff --git a/docs/usage.md b/packages/core/docs/usage.md similarity index 100% rename from docs/usage.md rename to packages/core/docs/usage.md diff --git a/packages/core/package.json b/packages/core/package.json index b169ffc..8b3edb8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,7 @@ { "name": "@babylonjs/node-assets", - "version": "0.1.0", + "private": true, + "version": "0.0.0", "license": "Apache-2.0", "type": "module", "sideEffects": false, @@ -39,9 +40,6 @@ "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" }, - "publishConfig": { - "access": "public" - }, "scripts": { "build": "vite build", "typedocs": "typedoc" diff --git a/packages/core/typedoc.json b/packages/core/typedoc.json index 21b7a9c..b06f248 100644 --- a/packages/core/typedoc.json +++ b/packages/core/typedoc.json @@ -1,7 +1,7 @@ { "$schema": "https://typedoc.org/schema.json", "entryPoints": ["src/index.ts"], - "out": "../../docs/typedocs", + "out": "docs/typedocs", "tsconfig": "tsconfig.build.json", "excludeInternal": true, "readme": "none"