From a9771ac9bda10fe5f50832ee56baa39f288adda8 Mon Sep 17 00:00:00 2001 From: Adnane Belmadiaf Date: Thu, 21 May 2026 13:00:38 -0400 Subject: [PATCH 1/2] feat(build)!: migrate build/test tooling to Vite, Vitest, oxlint, oxfmt Replace the webpack + rollup build, Karma test harness, and ESLint + Prettier with Vite, Vitest browser mode, oxlint, and oxfmt. BREAKING CHANGE: build, test, and lint tooling overhauled. See BREAKING_CHANGES.md for migration notes. Co-authored-by: Paul Elliott --- .babelrc.json | 14 - .eslintrc.js | 78 - .github/actions/setup-node-project/action.yml | 18 + .github/workflows/build-test.yml | 59 +- .github/workflows/pr-checks.yml | 11 +- .github/workflows/publish.yml | 40 +- .github/workflows/test-only-check.sh | 2 +- .gitignore | 6 + .oxfmtrc.json | 9 + .oxlintrc.json | 40 + BREAKING_CHANGES.md | 5 + Documentation/.vitepress/config.mts | 4 +- Documentation/docs/develop_build.md | 48 +- Documentation/docs/develop_example.md | 2 +- Documentation/docs/develop_test.md | 144 +- Documentation/docs/develop_webxr.md | 28 +- Documentation/docs/vtk_react.md | 2 +- Documentation/scripts/build-examples.mjs | 372 +- Documentation/scripts/generate-examples.mjs | 13 +- .../scripts/generate-sidebar-config.mjs | 11 +- .../ImageResliceMapperLabelOutline/index.js | 5 +- .../Core/CellArray/test/testCellArray.js | 58 +- .../Core/DataArray/test/testDataArray.js | 416 +- .../LookupTable/test/testCategoricalColors.js | 7 +- .../Core/LookupTable/test/testScalarBar.js | 7 +- .../Core/LookupTable/test/testSetTable.js | 9 +- Sources/Common/Core/Math/test/testMath.js | 474 +- .../MatrixBuilder/test/testMatrixBuilder.js | 13 +- Sources/Common/Core/Points/test/testPoints.js | 16 +- .../DataModel/AbstractPointLocator/index.d.ts | 3 +- .../AbstractPointLocator/test/testLocator.js | 14 +- .../BoundingBox/test/testBoundingBox.js | 50 +- Sources/Common/DataModel/Box/test/testBox.js | 27 +- .../DataModel/CardinalSpline1D/index.d.ts | 5 +- .../Common/DataModel/Cell/test/testCell.js | 53 +- .../Collection/test/testCollection.js | 45 +- .../Cone/test/testConeImplicitFunction.js | 7 +- .../DataModel/DataSetAttributes/index.d.ts | 3 +- .../test/testDataSetAttributes.js | 52 +- .../EdgeLocator/test/testEdgeLocator.js | 41 +- .../DataModel/ImageData/test/testImageData.js | 45 +- .../test/testIncrementalOctreeNode.js | 12 +- .../IncrementalOctreePointLocator/index.d.ts | 6 +- .../test/testIncrementalOctreePointLocator.js | 11 +- .../IncrementalPointLocator/index.d.ts | 3 +- .../DataModel/KochanekSpline1D/index.d.ts | 2 +- .../Common/DataModel/Line/test/testLine.js | 76 +- .../DataModel/Locator/test/testLocator.js | 12 +- .../MergePoints/test/testMergePoints.js | 93 +- .../Common/DataModel/Plane/test/testPlane.js | 103 +- .../Common/DataModel/PointLocator/index.d.ts | 3 +- .../PointLocator/test/testPointLocator.js | 106 +- .../DataModel/PolyData/test/testPolyData.js | 23 +- .../Common/DataModel/Quad/test/testQuad.js | 42 +- .../DataModel/Sphere/test/testSphere.js | 36 +- Sources/Common/DataModel/Spline1D/index.d.ts | 2 +- .../DataModel/Triangle/test/testTriangle.js | 127 +- .../TriangleStrip/test/testTriangleStrip.js | 210 +- .../test/testLandmarkTransform.js | 13 +- .../CleanPolyData/test/testCleanPolyData.js | 201 +- .../ClipPolyData/test/testClipPolyData.js | 27 +- .../Filters/Core/Cutter/test/testCutter.js | 12 +- .../test/testPolyDataNormals.js | 36 +- .../ReverseSense/test/testReverseSense.js | 62 +- .../AppendPolyData/test/testAppendPolyData.js | 54 +- .../General/Calculator/test/testCalculator.js | 69 +- .../test/testClipClosedSurface.js | 24 +- .../test/testClosedPolyLineToSurfaceFilter.js | 20 +- .../test/testContourTriangulator.js | 23 +- .../General/ImageDataOutlineFilter/index.d.ts | 3 +- .../test/testImageDataOutlineFilter.js | 14 +- .../ImageStreamline/test/testStreamline.js | 45 +- .../test/testMultipleBonds.js | 10 +- .../General/OBBTree/test/testHelpers.js | 13 +- .../General/OBBTree/test/testOBBTree.js | 55 +- .../General/PaintFilter/PaintFilter.worker.js | 5 +- .../PaintFilter/test/testPaintEllipse.js | 7 +- .../Filters/General/ShrinkPolyData/index.d.ts | 4 +- .../ShrinkPolyData/test/testShrinkPolyData.js | 7 +- .../TransformPolyDataFilter/index.d.ts | 3 +- .../test/testTransformPolyDataFilter.js | 149 +- .../General/TubeFilter/test/testTubeColors.js | 133 +- .../General/TubeFilter/test/testTubeFilter.js | 77 +- .../General/WarpScalar/test/testWarp.js | 46 +- .../Filters/Sources/ArcSource/test/testArc.js | 7 +- .../test/testConcentricCylinder.js | 112 +- .../Sources/ConeSource/test/testCone.js | 7 +- .../Sources/CubeSource/test/testCube.js | 7 +- .../CylinderSource/test/testCylinder.js | 7 +- .../Sources/DiskSource/test/testDisk.js | 7 +- .../EllipseArcSource/test/testEllipseArc.js | 7 +- .../Sources/LineSource/test/testLine.js | 7 +- .../Sources/PlaneSource/test/testPlane.js | 7 +- .../test/testPlatonicSolid.js | 536 +- .../PointSource/test/testPointSource.js | 7 +- .../test/testRegularPolygon.js | 88 +- .../test/testTextureMapToPlane.js | 24 +- .../test/testTextureMapToSphere.js | 18 +- .../testHttpDataAccessHelperFetchArray.js | 14 +- Sources/IO/Core/HttpDataSetReader/index.d.ts | 2 +- .../test/testHttpDataSetReader.js | 90 +- .../IO/Geometry/GLTFImporter/example/index.js | 5 +- .../Geometry/PLYReader/test/testPLYReader.js | 59 +- .../Geometry/STLReader/test/testSTLReader.js | 53 +- .../IO/Misc/OBJReader/test/testOBJReader.js | 56 +- .../IO/Misc/OBJWriter/test/testOBJWriter.js | 17 +- .../IO/Misc/PDBReader/test/testMolecule.js | 12 +- Sources/IO/XML/XMLImageDataReader/index.d.ts | 3 +- .../test/testXMLImageDataReader.js | 30 +- .../test/testXMLImageDataWriter.js | 10 +- Sources/IO/XML/XMLPolyDataReader/index.d.ts | 3 +- .../ImageReslice/test/testImageReslice.js | 173 +- .../index.d.ts | 6 +- .../GestureCameraManipulator/index.d.ts | 6 +- .../KeyboardCameraManipulator/index.d.ts | 3 +- .../MouseBoxSelectorManipulator/index.d.ts | 6 +- .../index.d.ts | 6 +- .../index.d.ts | 6 +- .../index.d.ts | 6 +- .../index.d.ts | 6 +- .../index.d.ts | 6 +- .../MouseRangeManipulator/index.d.ts | 6 +- .../test/testMouseRangeManipulator.js | 45 +- .../Style/InteractorStyleHMDXR/index.d.ts | 6 +- .../Style/InteractorStyleImage/index.d.ts | 3 +- .../test/testInteractorStyleImage.js | 21 +- .../example/index.js | 8 +- .../test/testDollyToPosition.js | 127 +- .../Animation/AnimationProxyManager/index.js | 11 +- .../test/testPiecewiseFunctionProxy.js | 27 +- Sources/Proxy/Core/ProxyManager/index.d.ts | 2 +- .../Core/View2DProxy/test/testView2DProxy.js | 9 +- Sources/Proxy/Core/ViewProxy/index.d.ts | 8 +- .../GeometryRepresentationProxy/index.d.ts | 3 +- .../ResliceRepresentationProxy/index.d.ts | 3 +- .../SliceRepresentationProxy/index.d.ts | 3 +- .../VolumeRepresentationProxy/index.d.ts | 3 +- .../Core/AbstractImageMapper/index.d.ts | 3 +- .../test/testAbstractImageMapper.js | 10 +- .../AbstractMapper/test/testAbstractMapper.js | 22 +- .../Core/AbstractMapper3D/index.d.ts | 3 +- .../Rendering/Core/Actor/test/testRotate.js | 7 +- .../Core/Actor2D/test/testActor2D.js | 7 +- .../Actor2D/test/testActor2DMultiViewports.js | 7 +- .../Actor2D/test/testRenderer3DAnd2DActors.js | 375 +- .../Core/CellPicker/test/testCellPicker.js | 60 +- .../test/testColorTransferFunction.js | 39 +- .../testColorTransferFunctionInterpolation.js | 94 +- .../test/testColorTransferFunctionPresets.js | 7 +- .../test/testCssFilters.js | 18 +- .../Core/Coordinate/test/testCoordinate.js | 14 +- .../Core/CutterMapper/example/index.js | 14 +- .../Core/Follower/test/testFollower.js | 7 +- .../Glyph3DMapper/test/testGlyph3DMapper.js | 7 +- .../test/testGlyph3DMapperClip.js | 7 +- .../test/testGlyph3DMapperShiftScale.js | 21 +- .../test/testHardwareSelector.js | 12 +- .../test/testHardwareSelectorGlyph.js | 20 +- .../test/testHardwareSelectorManyCells.js | 115 +- .../test/testHardwareSelectorPoints.js | 47 +- .../test/testHardwareSelectorSpeed.js | 10 +- .../Core/ImageArrayMapper/index.d.ts | 6 +- .../test/testImageArrayMapper.js | 7 +- .../test/testImageArrayMapperColorTF.js | 10 +- .../test/testImageArrayMapperFunctions.js | 109 +- .../test/testImageArrayMapperNN.js | 9 +- .../Rendering/Core/ImageCPRMapper/index.d.ts | 6 +- Sources/Rendering/Core/ImageMapper/index.d.ts | 6 +- .../Core/ImageMapper/test/testImage.js | 7 +- .../test/testImageColorTransferFunction.js | 124 +- .../ImageMapper/test/testImageLabelOutline.js | 9 +- .../test/testImageNearestNeighbor.js | 9 +- .../test/testLabelOutlineCardinal.js | 342 +- .../test/testSlicingModePosition.js | 19 +- .../Core/ImageResliceMapper/example/index.js | 5 +- .../Core/ImageResliceMapper/index.d.ts | 6 +- .../test/testImageResliceMapper.js | 10 +- .../testImageResliceMapperBorderRendering.js | 9 +- .../testImageResliceMapperDynamicInputs.js | 547 +- .../testImageResliceMapperLabelOutline.js | 311 +- .../test/testImageResliceMapperPolyData.js | 10 +- ...estImageResliceMapperShareOpenGLTexture.js | 308 +- .../test/testImageResliceMapperSlabTypes.js | 373 +- .../Core/Mapper/CoincidentTopologyHelper.d.ts | 3 +- Sources/Rendering/Core/Mapper/index.d.ts | 3 +- .../Core/Mapper/test/testCellData.js | 294 +- .../test/testCoincidentTopologyHelper.js | 16 +- .../Core/Mapper/test/testEdgeVisibility.js | 7 +- .../testInterpolateScalarsBeforeMapping.js | 207 +- .../Core/Mapper/test/testVectorComponent.js | 7 +- .../Rendering/Core/Picker/test/testPicker.js | 92 +- .../Core/PixelSpaceCallbackMapper/index.d.ts | 3 +- .../Core/PointPicker/test/testPointPicker.js | 46 +- .../Core/Prop3D/test/testUserMatrix.js | 7 +- .../test/testMultipleRenderers.js | 5 +- .../Core/RenderWindowInteractor/index.js | 5 +- .../test/testChordedButtons.js | 47 +- .../Rendering/Core/ScalarBarActor/index.d.ts | 12 +- .../Core/ScalarBarActor/test/testScalarBar.js | 7 +- .../test/testScalarBarNoExtras.js | 9 +- .../test/testScalarBarSetGenerateTicks.js | 126 +- .../test/testDisableScalarColoring.js | 10 +- .../Core/SphereMapper/test/testSphere.js | 7 +- .../test/testSphereMapperShiftScale.js | 18 +- .../Core/StickMapper/test/testStick.js | 7 +- .../Core/TextActor/test/testTextActor.js | 11 +- Sources/Rendering/Core/Volume/index.d.ts | 2 +- .../Rendering/Core/VolumeMapper/index.d.ts | 3 +- .../test/testAverageIntensityProjection.js | 9 +- .../Core/VolumeMapper/test/testColorMix.js | 418 +- .../Core/VolumeMapper/test/testComposite.js | 9 +- .../VolumeMapper/test/testComposite16Bit.js | 9 +- .../test/testCompositeParallelProjection.js | 12 +- .../Core/VolumeMapper/test/testIntermixed.js | 9 +- .../VolumeMapper/test/testIntermixedImage.js | 9 +- .../test/testMaximumIntensityProjection.js | 9 +- .../test/testMinimumIntensityProjection.js | 9 +- .../testGenericRenderWindowCreateDelete.js | 55 +- .../RenderWindowWithControlBar/index.d.ts | 3 +- .../Misc/RenderWindowWithControlBar/index.js | 5 +- .../SynchronizableRenderWindow/index.d.ts | 3 +- .../OpenGL/HardwareSelector/index.d.ts | 3 +- .../test/testImageCroppingPlanes.js | 73 +- .../test/testImageIntermediateZSlice.js | 7 +- .../ImageMapper/test/testImageWindowLevel.js | 16 +- .../ImageMapper/test/testLargeScalarImage.js | 202 +- .../ImageSlice/test/testForceRenderPass.js | 16 +- .../test/testAddShaderReplacements.js | 10 +- .../test/testClearShaderReplacements.js | 10 +- .../PolyDataMapper/test/testClippingPlanes.js | 8 +- .../test/testMoreClippingPlanes.js | 232 +- .../PolyDataMapper2D/test/testCullFaceLeak.js | 22 +- .../Rendering/OpenGL/RenderWindow/index.d.ts | 2 +- .../ShaderProgram/test/testSubstitute.js | 46 +- .../OpenGL/Skybox/test/testSkybox.js | 139 +- .../Skybox/test/testSkyboxBackground.js | 118 +- .../SphereMapper/test/testLargeCoordinates.js | 123 +- .../test/testCreateCubeFromRawTexture.js | 162 +- .../test/testLabelmapOutlineManyRenderer.js | 9 +- .../test/testLargeScalarVolume.js | 237 +- .../OpenGL/VolumeMapper/test/testLighting.js | 7 +- .../test/testProportionalComponents.js | 331 +- .../VolumeMapper/test/testUpdatedExtents.js | 212 +- .../test/testVolumeMapperBounds.js | 7 +- .../VolumeMapper/test/testVolumeMapperClip.js | 7 +- .../test/testVolumeMapperShadowClip.js | 7 +- Sources/Rendering/WebGPU/Pipeline/index.js | 12 +- .../Texture/test/testWriteSubImageData.js | 138 +- .../WebXR/RenderWindowHelper/index.d.ts | 2 +- Sources/Testing/index.js | 28 - Sources/Testing/setupTestEnv.js | 70 +- Sources/Testing/testAlgorithm.js | 44 +- Sources/Testing/testIf.js | 12 +- Sources/Testing/testMacro.js | 597 +- Sources/Testing/testProxy.js | 46 +- Sources/Testing/testSerialization.js | 51 +- Sources/Testing/testUtils.js | 39 +- .../Core/AbstractWidgetFactory/index.d.ts | 8 +- Sources/Widgets/Core/WidgetManager/index.d.ts | 2 +- .../WidgetManager/test/testWidgetManager.js | 20 +- .../Manipulators/LineManipulator/index.d.ts | 3 +- .../Manipulators/PickerManipulator/index.d.ts | 3 +- .../Manipulators/PlaneManipulator/index.d.ts | 3 +- .../TrackballManipulator/index.d.ts | 3 +- .../InteractiveOrientationWidget/index.d.ts | 6 +- .../Widgets3D/LineWidget/example/index.js | 2 +- .../ResliceCursorWidget/Constants.d.ts | 4 +- .../ResliceCursorWidget/behavior.d.ts | 5 +- .../Widgets3D/ResliceCursorWidget/index.d.ts | 8 +- .../test/testBoundPlane.js | 51 +- .../ResliceCursorWidget/test/testHelper.js | 15 +- .../test/testMultipleRotationPlanes.js | 21 +- .../test/testRotateVector.js | 15 +- .../SplineWidget/test/testSplineWidget.js | 230 +- Sources/macros.d.ts | 10 +- Sources/types.d.ts | 2 +- Utilities/ExampleRunner/.gitignore | 1 - Utilities/ExampleRunner/example-runner-cli.js | 101 - .../ExampleRunner/example-runner-cli.mjs | 149 + Utilities/ExampleRunner/index.html | 19 + Utilities/ExampleRunner/template-config.js | 87 - Utilities/ExampleRunner/template.html | 10 - .../ExampleRunner/vite.example.config.mjs | 88 + Utilities/Karma/reporting-template.html | 214 - Utilities/Karma/tape-html-reporter.js | 125 - Utilities/Karma/tape-object-stream/adapter.js | 46 - Utilities/Karma/tape-object-stream/index.js | 15 - Utilities/build/plugins.mjs | 166 + Utilities/build/rewrite-imports.js | 4 +- Utilities/build/umd-entry.js | 3 + Utilities/build/vtk-plugins.mjs | 380 + .../rollup/plugin-generate-references.js | 36 - Utilities/rollup/plugin-rewrite-filenames.js | 52 - karma.conf.js | 112 - package-lock.json | 46700 ++++------------ package.json | 162 +- patches/rollup-plugin-copy+3.4.0.patch | 22 - ...ollup-plugin-web-worker-loader+1.6.1.patch | 27 - prettier.config.js | 6 - rollup.config.js | 264 - tsconfig.umd-check.json | 2 +- vite.config.js | 148 + vitest.config.js | 87 + webpack.common.js | 215 - webpack.dev.js | 39 - webpack.examples.config.js | 137 - webpack.prod.js | 81 - webpack.settings.js | 38 - 308 files changed, 18019 insertions(+), 45593 deletions(-) delete mode 100644 .babelrc.json delete mode 100644 .eslintrc.js create mode 100644 .github/actions/setup-node-project/action.yml create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json delete mode 100644 Sources/Testing/index.js delete mode 100644 Utilities/ExampleRunner/.gitignore delete mode 100755 Utilities/ExampleRunner/example-runner-cli.js create mode 100755 Utilities/ExampleRunner/example-runner-cli.mjs create mode 100644 Utilities/ExampleRunner/index.html delete mode 100644 Utilities/ExampleRunner/template-config.js delete mode 100644 Utilities/ExampleRunner/template.html create mode 100644 Utilities/ExampleRunner/vite.example.config.mjs delete mode 100644 Utilities/Karma/reporting-template.html delete mode 100644 Utilities/Karma/tape-html-reporter.js delete mode 100644 Utilities/Karma/tape-object-stream/adapter.js delete mode 100644 Utilities/Karma/tape-object-stream/index.js create mode 100644 Utilities/build/plugins.mjs create mode 100644 Utilities/build/umd-entry.js create mode 100644 Utilities/build/vtk-plugins.mjs delete mode 100644 Utilities/rollup/plugin-generate-references.js delete mode 100644 Utilities/rollup/plugin-rewrite-filenames.js delete mode 100644 karma.conf.js delete mode 100644 patches/rollup-plugin-copy+3.4.0.patch delete mode 100644 patches/rollup-plugin-web-worker-loader+1.6.1.patch delete mode 100644 prettier.config.js delete mode 100644 rollup.config.js create mode 100644 vite.config.js create mode 100644 vitest.config.js delete mode 100644 webpack.common.js delete mode 100644 webpack.dev.js delete mode 100644 webpack.examples.config.js delete mode 100644 webpack.prod.js delete mode 100644 webpack.settings.js diff --git a/.babelrc.json b/.babelrc.json deleted file mode 100644 index fda3e98774d..00000000000 --- a/.babelrc.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "presets": [ - ["@babel/preset-env", { - "debug": false, - "useBuiltIns": false - }] - ], - "env": { - "test": { - "plugins": ["istanbul"] - } - }, - "plugins": ["@babel/plugin-transform-runtime"] -} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 49977a30419..00000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,78 +0,0 @@ -const path = require('path'); -const prettierConf = require('./prettier.config.js'); - -module.exports = { - parser: '@babel/eslint-parser', - parserOptions: { - babelOptions: { - configFile: path.resolve(__dirname, '.babelrc.json'), - }, - }, - extends: ['airbnb/base', 'prettier'], - rules: { - 'prettier/prettier': ['error', prettierConf], - - // But we want the following - 'no-multi-spaces': ['error', { exceptions: { ImportDeclaration: true } }], - 'no-param-reassign': ['error', { props: false }], - 'no-unused-vars': ['error', { args: 'none' }], - 'prefer-destructuring': 0, - 'import/no-extraneous-dependencies': 0, // Needed for tests - // 'no-mixed-operators': 'error', // Wish we can put it back with prettier - - // Not for us - 'jsx-a11y/label-has-for': 0, - 'no-console': 0, - 'no-plusplus': 0, - 'no-underscore-dangle': 0, - 'import/no-named-as-default': 0, - 'import/no-named-as-default-member': 0, - - // Introduced with new eslint - // and no time to fix them... - // [...] - 'linebreak-style': 0, - }, - plugins: ['prettier'], - globals: { - __BASE_PATH__: false, - VRFrameData: true, - }, - settings: { - 'import/resolver': { - webpack: { - config: { - resolve: { - alias: { - // Since vtk.js examples are written as if the vtk.js package is a dependency, - // we need to resolve example imports as if they were referencing vtk.js/Sources. - // the Examples/Utilities hack allows for imports from those folders, since our - // last alias overrides vtk.js/* paths to point to vtk.js/Sources/*. - 'vtk.js/Data': path.resolve(__dirname, 'Data'), - 'vtk.js/Examples': path.resolve(__dirname, 'Examples'), - 'vtk.js/Utilities': path.resolve(__dirname, 'Utilities'), - 'vtk.js/Sources': path.resolve(__dirname, 'Sources'), - 'vtk.js': path.resolve(__dirname, 'Sources'), - '@kitware/vtk.js': path.resolve(__dirname, 'Sources'), - }, - }, - }, - }, - }, - }, - env: { - es2020: true, - es6: true, - browser: true, - }, - ignorePatterns: [ - // ignore old Actor example - '**/example_/*.js', - // ignore js files in utilities - 'Utilities/**/*.js', - // ignore configs - 'karma.conf.js', - 'webpack.*.js', - '.eslintrc.js', - ], -}; diff --git a/.github/actions/setup-node-project/action.yml b/.github/actions/setup-node-project/action.yml new file mode 100644 index 00000000000..c4463acf954 --- /dev/null +++ b/.github/actions/setup-node-project/action.yml @@ -0,0 +1,18 @@ +name: Setup Node Project +description: Setup Node and install dependencies with npm ci +inputs: + node-version: + description: Node.js version + required: false + default: '22' +runs: + using: composite + steps: + - name: Setup node + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + - name: Install dependencies + shell: bash + run: npm ci diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 29b24c615c5..1558b77ddf7 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -7,40 +7,65 @@ jobs: build-test: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-24.04] node: [22] - name: ${{ matrix.os }} and node ${{ matrix.node }} + browser: [chromium, firefox] + name: ${{ matrix.os }} / node ${{ matrix.node }} / ${{ matrix.browser }} steps: - - uses: actions/checkout@v2 - - name: Setup node - uses: actions/setup-node@v1 + - uses: actions/checkout@v6 + - name: Setup project + uses: ./.github/actions/setup-node-project with: - node-version: ${{ matrix.node }} - - name: Install dependencies - run: | - npm ci - sudo apt-get install xvfb + node-version: '${{ matrix.node }}' + - name: Resolve Playwright version + id: playwright + run: echo "version=$(node -p "require('playwright/package.json').version")" >> "$GITHUB_OUTPUT" + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@v5 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ matrix.os }}-${{ matrix.browser }}-${{ steps.playwright.outputs.version }} + - name: Install Playwright browser + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install ${{ matrix.browser }} + - name: Install Playwright system deps + run: sudo npx playwright install-deps ${{ matrix.browser }} - name: Build run: npm run build:release - name: Archive build output - if: github.event_name != 'merge_group' - uses: actions/upload-artifact@v4 + if: github.event_name != 'merge_group' && matrix.browser == 'chromium' + uses: actions/upload-artifact@v7 with: - name: build-results-${{ matrix.runs_on }}-node_${{ matrix.node }} + name: build-results-${{ matrix.os }}-node_${{ matrix.node }} path: dist retention-days: 15 - name: Validate generated typescript definitions run: | npx tsc -p tsconfig.esm-check.json npx tsc -p tsconfig.umd-check.json - - name: Chrome and Firefox tests - run: xvfb-run --auto-servernum npm run test -- --browsers ChromeSwiftShader,Firefox + - name: Smoke-test packed ESM tarball + if: matrix.browser == 'chromium' + run: | + tarball=$(npm pack ./dist/esm --silent) + mkdir -p /tmp/vtk-smoke + cd /tmp/vtk-smoke + npm init -y >/dev/null + npm install --no-audit --no-fund "$GITHUB_WORKSPACE/$tarball" + node -e "require('@kitware/vtk.js/Utilities/config/rules-vtk')" + node -e "require('@kitware/vtk.js/Utilities/config/chainWebpack')" + npx --no-install vtkDataConverter --help + - name: Tests + env: + TEST_BROWSER: ${{ matrix.browser }} + run: xvfb-run --auto-servernum npm test - name: Archive test results if: github.event_name != 'merge_group' && (success() || failure()) - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 continue-on-error: true with: - name: test-results-${{ matrix.runs_on }}-node_${{ matrix.node }} - path: Utilities/TestResults/Test-Report.html + name: test-results-${{ matrix.os }}-node_${{ matrix.node }}-${{ matrix.browser }} + path: Utilities/TestResults/ retention-days: 15 diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index a572448c21a..32056559391 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -8,16 +8,17 @@ jobs: runs-on: ubuntu-24.04 name: Check and lint PR steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 with: fetch-depth: 0 # needed so commitlint can lint the commits - - name: Setup node - uses: actions/setup-node@v1 + - name: Setup project + uses: ./.github/actions/setup-node-project with: - node-version: 20 - - run: npm ci + node-version: '22' - name: Enforce code style run: npm run validate + - name: Lint + run: npm run lint - name: Lint commits if: github.event_name != 'merge_group' run: npx commitlint --from=${{ github.event.pull_request.base.sha }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 960e4ece898..f8f4fa22ac1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,17 +22,15 @@ jobs: name: npm steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Setup node - uses: actions/setup-node@v4 + - name: Setup project + uses: ./.github/actions/setup-node-project with: - node-version: 22 - - name: Install dependencies - run: | - npm ci - sudo apt-get install xvfb + node-version: '22' + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium firefox - name: Build run: npm run build:release - name: Validate code style @@ -42,13 +40,13 @@ jobs: npx tsc -p tsconfig.esm-check.json npx tsc -p tsconfig.umd-check.json - name: Chrome and Firefox tests - run: xvfb-run --auto-servernum npm run test -- --browsers ChromeSwiftShader,Firefox + run: xvfb-run --auto-servernum npm test - name: Archive test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: test-results - path: Utilities/TestResults/Test-Report.html + path: Utilities/TestResults/ retention-days: 15 - name: Release env: @@ -70,25 +68,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v6 with: fetch-depth: 1 - - name: Setup node - uses: actions/setup-node@v1 + - name: Setup project + uses: ./.github/actions/setup-node-project with: - node-version: 22 - - name: Install dependencies - run: npm ci + node-version: '22' - name: Build run: npm run build:release - - name: Generate API docs - run: npm run docs:generate-api - - name: Generate docs examples - run: npm run docs:generate-examples - - name: Generate docs sidebar - run: npm run docs:generate-sidebar - - name: Generate docs gallery - run: npm run docs:generate-gallery + - name: Generate docs content + run: npm run docs:generate - name: Build docs run: npm run docs:build - name: Build docs examples diff --git a/.github/workflows/test-only-check.sh b/.github/workflows/test-only-check.sh index 8f42227beb5..39f14ea6a29 100644 --- a/.github/workflows/test-only-check.sh +++ b/.github/workflows/test-only-check.sh @@ -1,3 +1,3 @@ #!/bin/bash cd $(dirname $(readlink -e $0))/../../ -! git grep 'test\.only(' **/test*.js +! git grep -E '\b(test|it|describe)\.only\(' -- '**/test*.js' diff --git a/.gitignore b/.gitignore index bcbf9d1cbbd..1aed33fdf44 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,15 @@ dist/ coverage/ .env Utilities/TestResults +.vitest-attachments/ .idea Documentation/.vitepress/cache/ Documentation/.vitepress/dist/ Documentation/build-tmp/ Documentation/content/ Documentation/Examples/ +Documentation/api/*.md +!Documentation/api/index.md +Documentation/examples/*.md +!Documentation/examples/index.md +Documentation/examples/gallery.js diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000000..c956ecb44ca --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,9 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 80, + "singleQuote": true, + "trailingComma": "es5", + "arrowParens": "always", + "sortPackageJson": false, + "ignorePatterns": [] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000000..e55300841e4 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,40 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["import"], + "env": { + "es2020": true, + "es6": true, + "browser": true + }, + "globals": { + "__BASE_PATH__": "readonly", + "__VTK_TEST_NO_WEBGL__": "readonly", + "__VTK_TEST_WEBGPU__": "readonly", + "VRFrameData": "readonly" + }, + "rules": { + "no-console": "off", + "no-param-reassign": [ + "warn", + { + "props": false + } + ], + "no-plusplus": "off", + "no-underscore-dangle": "off", + "no-unused-vars": [ + "warn", + { + "args": "none" + } + ], + "prefer-destructuring": "off" + }, + "ignorePatterns": [ + "**/example_/*.js", + "Utilities/**/*.js", + "vite.config.js", + "vitest.config.js", + ".eslintrc.js" + ] +} diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index b182da2d088..13f41120d90 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,3 +1,8 @@ +## From 35.x to 36 + +- **`vtk-lite.js` deprecated.** The UMD build previously produced a slimmed-down companion bundle `dist/umd/vtk-lite.js` (curated ColorMaps subset; `PDBReader`, `MoleculeToRepresentation`, `MobileVR`, and `webvr-polyfill` stubbed out). The Vite build pipeline no longer produces a distinct lite bundle; `vtk-lite.js` now ships as a byte-identical alias of `vtk.js` so existing ``; } if (inlineScript) { - exampleScript = ``; + exampleScript = isModule + ? `` + : ``; } return ` @@ -90,7 +79,9 @@ async function walkExamples(config, dir = config.root, results = []) { entries.map(async (entry) => { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { - const relDir = path.relative(config.root, fullPath).replace(/\\/g, '/'); + const relDir = path + .relative(config.root, fullPath) + .replace(/\\/g, '/'); if (relDir && config.shouldSkipDir(relDir)) { return; } @@ -122,118 +113,39 @@ async function collectEntries() { return entries; } -function getMimeType(filePath) { - const ext = path.extname(filePath).toLowerCase(); - const mimeByExt = { - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.gif': 'image/gif', - '.svg': 'image/svg+xml', - '.webp': 'image/webp', - }; - return mimeByExt[ext] || 'application/octet-stream'; -} - -function toDataUri(filePath, source) { - return `data:${getMimeType(filePath)};base64,${source.toString('base64')}`; -} - -function assetLoader({ inline = false } = {}) { - const assetRegex = /\.(png|jpe?g)$/i; - - return { - name: inline ? 'asset-loader-inline' : 'asset-loader', - async load(id) { - if (!assetRegex.test(id)) { - return null; - } - - const source = await fs.readFile(id); - if (inline) { - return `export default ${JSON.stringify(toDataUri(id, source))};`; - } - const refId = this.emitFile({ - type: 'asset', - name: path.basename(id), - source, - }); - - return `export default import.meta.ROLLUP_FILE_URL_${refId};`; - }, - }; -} - -function replaceBasePath(basePath) { +// Standalone example HTML files have a single inline script (no +// to a separate CSS asset), so any CSS Vite extracts must be inlined into +// the JS chunk. This plugin takes Vite's normal CSS Modules output and +// prepends a - - - -

vtk.js Test Results

-
- - -
- {{#each browsers}} -
-
-
- {{name}} - - {{#if summary.failed}} - Some tests failed - {{else}} - All passed - {{/if}} - - -
-
    -
  • Failed: {{summary.failed}}
  • -
  • Passed: {{summary.passed}}
  • -
  • Skipped: {{summary.skipped}}
  • -
  • Total: {{summary.total}}
  • -
-
- -
- {{/each}} - - diff --git a/Utilities/Karma/tape-html-reporter.js b/Utilities/Karma/tape-html-reporter.js deleted file mode 100644 index e8e76261686..00000000000 --- a/Utilities/Karma/tape-html-reporter.js +++ /dev/null @@ -1,125 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const Handlebars = require('handlebars'); - -Handlebars.registerHelper('equals', function(a, b) { - return a === b; -}); - -Handlebars.registerHelper('json', function(obj) { - return JSON.stringify(obj); -}); - -var TapeHTMLReporter = function(baseReporterDecorator, rootConfig, logger) { - const log = logger.create('tape-html-reporter'); - const config = rootConfig.tapeHTMLReporter || {}; - - // config - const templateFile = config.templateFile; - const outputFile = config.outputFile; - - // state - let testData = { - browsers: [], - }; - let browserIndex = {}; - let currentSuite = null; - - const createSuite = (name) => ({ name, success: false, specs: [] }); - - baseReporterDecorator(this); - - this.onBrowserStart = function(browser) { - const info = { - id: browser.id, - name: browser.name, - tests: [], - summary: { - failed: 0, - passed: 0, - skipped: 0, - total: 0, - }, - }; - browserIndex[browser.id] = info; - testData.browsers.push(info); - }; - - const addSpec = (browser, result) => { - const suiteName = result.suite.join(' ').trim() || '(untitled suite)'; - const browserInfo = browserIndex[browser.id]; - - if (!currentSuite || (currentSuite && currentSuite.name !== suiteName)) { - currentSuite = createSuite(suiteName); - browserInfo.tests.push(currentSuite); - } - - // result has a shape defined in tape-object-stream/adapter.js - currentSuite.specs.push(result); - - if (result.skipped) { - browserInfo.summary.skipped += 1; - } else if (result.success) { - browserInfo.summary.passed += 1; - } else { - browserInfo.summary.failed += 1; - } - browserInfo.summary.total += 1; - }; - - this.specSuccess = addSpec; - this.specFailure = addSpec; - this.specSkipped = addSpec; - - this.onRunComplete = function(browsers, results) { - // fill in test success flag - testData.browsers.forEach((browser) => { - browser.tests.forEach((test) => { - test.success = test.specs.reduce((yn, spec) => yn && spec.success, true); - }); - - console.info(`Browser: ${browser.name}`); - console.info(`\tSuccess:\t${browser.summary.passed}`); - console.info(`\tSkipped:\t${browser.summary.skipped}`); - console.info(`\tFailed:\t${browser.summary.failed}`); - console.info(`\tTotal:\t${browser.summary.total}`); - }); - - // render out html - let templateHTML = ''; - try { - templateHTML = fs.readFileSync(templateFile, { encoding: 'utf8' }); - } catch (e) { - return log.error(`error reading template file: ${e}`); - } - const template = Handlebars.compile(templateHTML); - - if (outputFile) { - try { - fs.mkdirSync(path.dirname(outputFile), { recursive: true }); - } catch (e) { - if (e.code !== 'EEXIST') { - log.error(`error creating test results folder: ${e}`); - return; - } - } - - try { - fs.writeFileSync(outputFile, template(testData), { encoding: 'utf8' }); - log.info(`report written to file ${outputFile}`); - } catch (e) { - log.error(`error writing report to file ${outputFile}: ${e}`); - } - } - }; -}; - -TapeHTMLReporter.$inject = [ - 'baseReporterDecorator', - 'config', - 'logger', -]; - -module.exports = { - 'reporter:tape-html': ['type', TapeHTMLReporter], -}; diff --git a/Utilities/Karma/tape-object-stream/adapter.js b/Utilities/Karma/tape-object-stream/adapter.js deleted file mode 100644 index 9312d6f14a2..00000000000 --- a/Utilities/Karma/tape-object-stream/adapter.js +++ /dev/null @@ -1,46 +0,0 @@ -function createStartFn(tc) { - return function() { - var currentTest = {}; - var results = []; - - function readTapeObject(obj) { - if (obj.type === 'test') { - currentTest = obj; - console.info(`Test Suite: ${obj.name}`); - } else if (obj.type === 'assert') { - const status = obj.skip ? 'SKIP' : obj.ok ? 'PASS' : 'FAIL'; - console.info(`\t[${status}] ${obj.name}`); - - results.push({ - id: `${currentTest.id || ''}.${obj.id}`, - description: obj.name, - suite: [currentTest.name || '(untitled suite)'], - log: [], - success: obj.ok, - // cast to bool just to be explicit. - // skip is not always on obj. - skipped: !!obj.skip, - details: obj, - }); - } - } - - function complete() { - tc.info({ total: results.length }); - while (results.length) { - tc.result(results.shift()); - } - tc.complete({ - coverage: window.__coverage__, - }); - } - - var tapeEnv = window.__TapeEnv__; - tapeEnv.pipe.setReader({ - onData: readTapeObject, - onClose: complete, - }); - }; -} - -window.__karma__.start = createStartFn(window.__karma__); diff --git a/Utilities/Karma/tape-object-stream/index.js b/Utilities/Karma/tape-object-stream/index.js deleted file mode 100644 index 7d461f20db3..00000000000 --- a/Utilities/Karma/tape-object-stream/index.js +++ /dev/null @@ -1,15 +0,0 @@ -const path = require('path'); - -function createPattern(pathPattern) { - return { pattern: pathPattern, included: true, served: true, watched: false }; -} - -function initTapeObjectStream(files) { - files.unshift(createPattern(path.join(__dirname, 'adapter.js'))); -} - -initTapeObjectStream.$inject = ['config.files']; - -module.exports = { - 'framework:tape-object-stream': ['factory', initTapeObjectStream], -}; diff --git a/Utilities/build/plugins.mjs b/Utilities/build/plugins.mjs new file mode 100644 index 00000000000..0d78f7a472f --- /dev/null +++ b/Utilities/build/plugins.mjs @@ -0,0 +1,166 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +function stripQueryAndHash(id) { + return id.replace(/[?#].*$/, ''); +} + +/** + * Load .glsl files as raw strings. + */ +export function glslPlugin() { + return { + name: 'vtk-glsl', + load(id) { + const fileId = stripQueryAndHash(id); + if (fileId.endsWith('.glsl')) { + const content = fs.readFileSync(fileId, 'utf-8'); + return { + code: `export default ${JSON.stringify(content)};`, + moduleType: 'js', + }; + } + }, + }; +} + +/** + * Load .svg files as raw strings. + */ +export function svgRawPlugin() { + const VIRTUAL_PREFIX = '\0vtk-svg-raw:'; + + return { + name: 'vtk-svg-raw', + enforce: 'pre', + async resolveId(source, importer, options) { + if (!source.endsWith('.svg') || source.includes('?')) { + return null; + } + + const resolved = await this.resolve(source, importer, { + ...options, + skipSelf: true, + }); + if (!resolved) { + return null; + } + + return `${VIRTUAL_PREFIX}${resolved.id}`; + }, + load(id) { + if (!id.startsWith(VIRTUAL_PREFIX)) { + return null; + } + + const fileId = stripQueryAndHash(id.slice(VIRTUAL_PREFIX.length)); + if (fileId.endsWith('.svg')) { + const content = fs.readFileSync(fileId, 'utf-8'); + return { + code: `export default ${JSON.stringify(content)};`, + moduleType: 'js', + }; + } + }, + }; +} + +/** + * Load .cjson files as JSON modules. + */ +export function cjsonPlugin() { + return { + name: 'vtk-cjson', + load(id) { + const fileId = stripQueryAndHash(id); + if (fileId.endsWith('.cjson')) { + const content = fs.readFileSync(fileId, 'utf-8'); + return { + code: `export default ${content};`, + moduleType: 'js', + }; + } + }, + }; +} + +/** + * Resolve .worker imports using Vite native inline workers. + */ +export function workerInlinePlugin() { + return { + name: 'vtk-worker-inline', + enforce: 'pre', + async resolveId(source, importer, options) { + if (/\.worker(?:\.js)?$/.test(source) && !source.includes('?')) { + const actualSource = source.endsWith('.js') ? source : `${source}.js`; + const resolved = await this.resolve(actualSource, importer, { + ...options, + skipSelf: true, + }); + if (resolved) { + return `${resolved.id}?worker&inline`; + } + } + }, + }; +} + +/** + * Ignore specific modules (e.g. crypto) by replacing them with empty exports. + */ +export function ignorePlugin(modules) { + return { + name: 'vtk-ignore', + resolveId(source) { + if (modules.includes(source)) { + return { id: `\0ignore:${source}`, moduleSideEffects: false }; + } + }, + load(id) { + if (id.startsWith('\0ignore:')) { + return { code: 'export default {};', moduleType: 'js' }; + } + }, + }; +} + +/** + * Serve a directory as static files via the Vite dev server. + */ +export function serveStaticDataPlugin(rootDir) { + return { + name: 'vtk-serve-data', + configureServer(server) { + server.middlewares.use('/Data', (req, res, next) => { + const filePath = path.join(rootDir, 'Data', req.url); + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { + res.writeHead(200); + res.end(fs.readFileSync(filePath)); + } else { + next(); + } + }); + }, + }; +} + +export function createVtkPlugins({ + includeCjson = false, + includeStaticData = false, + staticDataRootDir, +} = {}) { + const plugins = [workerInlinePlugin(), glslPlugin(), svgRawPlugin()]; + + if (includeCjson) { + plugins.push(cjsonPlugin()); + } + + plugins.push(ignorePlugin(['crypto'])); + + if (includeStaticData && staticDataRootDir) { + plugins.push(serveStaticDataPlugin(staticDataRootDir)); + } + + return plugins; +} diff --git a/Utilities/build/rewrite-imports.js b/Utilities/build/rewrite-imports.js index 404365c5c2f..d6045f4f00b 100644 --- a/Utilities/build/rewrite-imports.js +++ b/Utilities/build/rewrite-imports.js @@ -1,4 +1,4 @@ -module.exports = (code, replaceFunc) => { +export default function rewriteImports(code, replaceFunc) { const importRegex = /(?:import|from)\s+['"]([^'"]*)['"]/g; let m; while ((m = importRegex.exec(code)) != null) { @@ -16,4 +16,4 @@ module.exports = (code, replaceFunc) => { } } return code; -}; +} diff --git a/Utilities/build/umd-entry.js b/Utilities/build/umd-entry.js new file mode 100644 index 00000000000..26a8d5a0cfa --- /dev/null +++ b/Utilities/build/umd-entry.js @@ -0,0 +1,3 @@ +import '../../Sources/index.js'; + +export default globalThis.vtk; diff --git a/Utilities/build/vtk-plugins.mjs b/Utilities/build/vtk-plugins.mjs new file mode 100644 index 00000000000..ad672f3acde --- /dev/null +++ b/Utilities/build/vtk-plugins.mjs @@ -0,0 +1,380 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import glob from 'glob'; +import rewriteImports from './rewrite-imports.js'; + +export const SOURCE_IGNORE_LIST = [ + /[/\\]example_?[/\\]/, + /[/\\]test/, + /^Sources[/\\](Testing|ThirdParty)/, +]; + +export function ignoreSourceFile(name, ignoreList = SOURCE_IGNORE_LIST) { + return ignoreList.some((toMatch) => { + if (toMatch instanceof RegExp) return toMatch.test(name); + if (typeof toMatch === 'string') return toMatch === name; + return false; + }); +} + +/** + * If `name` looks like `/Foo/index`, return `/Foo` (flattening the + * idiomatic vtk.js single-file class layout). Otherwise return `name`. + */ +export function flattenIndexEntry(name) { + const match = /^((?:.*\/)?)([A-Z]\w+)\/index$/.exec(name); + return match ? `${match[1]}${match[2]}` : name; +} + +function copyDir(src, dest) { + fs.cpSync(src, dest, { recursive: true }); +} + +function copyRootFiles(outputDir, patterns = ['*.txt', '*.md']) { + patterns.forEach((pattern) => { + for (const file of glob.sync(pattern)) { + fs.copyFileSync(file, path.join(outputDir, file)); + } + }); +} + +function copyCommonPackageAssets(outputDir) { + copyRootFiles(outputDir); + if (fs.existsSync('.npmignore')) { + fs.copyFileSync('.npmignore', path.join(outputDir, '.npmignore')); + } + fs.copyFileSync('LICENSE', path.join(outputDir, 'LICENSE')); +} + +function writePackageManifest(outputDir, transformPkg) { + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8')); + transformPkg(pkg); + fs.writeFileSync( + path.join(outputDir, 'package.json'), + JSON.stringify(pkg, null, 2) + ); +} + +function rewriteDtsContent(file, rewriter) { + return rewriteImports(fs.readFileSync(file, 'utf-8'), rewriter); +} + +/** + * Walks Sources/**\/*.d.ts, copies them next to the matching .js output, + * rewriting relative imports for the flattened layout. Also copies static + * Utilities, the macro shim, root assets, tsconfig, and writes a tailored + * package.json into the ESM output directory. + */ +export function copyEsmAssetsPlugin({ esmOutputDir }) { + return { + name: 'vtk-copy-esm-assets', + closeBundle() { + const dtsFiles = glob.sync('Sources/**/*.d.ts'); + for (const file of dtsFiles) { + if (ignoreSourceFile(file)) continue; + + const filename = path.basename(file); + const dirname = path.dirname(file); + + if (filename === 'index.d.ts' && dirname !== 'Sources') { + const moduleName = path.basename(dirname); + const destPath = path.join( + esmOutputDir, + path.relative('Sources', path.dirname(dirname)), + `${moduleName}.d.ts` + ); + + const content = rewriteDtsContent(file, (relImport) => { + const baseDir = dirname; + + if (relImport === '..') { + return `../${path.basename(path.dirname(baseDir))}`; + } + + if (relImport.startsWith('../') || relImport.startsWith('./')) { + const resolvedImportPath = path.resolve( + `${baseDir}/${relImport}` + ); + return `./${path + .relative(`${baseDir}/..`, resolvedImportPath) + .replace(/\\/g, '/')}`; + } + + return relImport; + }); + + fs.mkdirSync(path.dirname(destPath), { recursive: true }); + fs.writeFileSync(destPath, content); + } else { + const destPath = path.join( + esmOutputDir, + path.relative('Sources', file) + ); + fs.mkdirSync(path.dirname(destPath), { recursive: true }); + fs.copyFileSync(file, destPath); + } + } + + copyDir( + 'Utilities/XMLConverter', + `${esmOutputDir}/Utilities/XMLConverter` + ); + copyDir( + 'Utilities/DataGenerator', + `${esmOutputDir}/Utilities/DataGenerator` + ); + copyDir('Utilities/config', `${esmOutputDir}/Utilities/config`); + fs.mkdirSync(`${esmOutputDir}/Utilities`, { recursive: true }); + fs.copyFileSync( + 'Utilities/prepare.js', + `${esmOutputDir}/Utilities/prepare.js` + ); + + // Flip these CJS subdirs back to CommonJS scope (root is type: module). + for (const dir of [ + 'Utilities/config', + 'Utilities/XMLConverter', + 'Utilities/DataGenerator', + ]) { + fs.writeFileSync( + `${esmOutputDir}/${dir}/package.json`, + `${JSON.stringify({ type: 'commonjs' }, null, 2)}\n` + ); + } + + fs.copyFileSync( + 'Utilities/build/macro-shim.d.ts', + `${esmOutputDir}/macro.d.ts` + ); + fs.copyFileSync( + 'Utilities/build/macro-shim.js', + `${esmOutputDir}/macro.js` + ); + + copyCommonPackageAssets(esmOutputDir); + fs.copyFileSync('tsconfig.json', `${esmOutputDir}/tsconfig.json`); + writePackageManifest(esmOutputDir, (pkg) => { + pkg.name = '@kitware/vtk.js'; + pkg.type = 'module'; + pkg.main = './index.js'; + pkg.module = './index.js'; + pkg.types = './index.d.ts'; + }); + }, + }; +} + +/** + * For the UMD build, ship Sources/**\/* unbundled alongside vtk.js so + * consumers can deep-import individual modules. .d.ts imports get rewritten + * from relative paths to absolute `vtk.js/Sources/...` paths. + */ +export function copyUmdAssetsPlugin({ umdOutputDir, projectRoot }) { + return { + name: 'vtk-copy-umd-assets', + closeBundle() { + const sourceFiles = glob + .sync('Sources/**/*', { nodir: true }) + .filter((file) => !ignoreSourceFile(file) && !file.endsWith('.md')); + + for (const file of sourceFiles) { + const destPath = path.join(umdOutputDir, file); + fs.mkdirSync(path.dirname(destPath), { recursive: true }); + + if (file.endsWith('.d.ts')) { + const content = rewriteDtsContent(file, (relImport) => { + const importPath = path.join(path.dirname(file), relImport); + return path + .join('vtk.js', path.relative(projectRoot, importPath)) + .replace(/\\/g, '/'); + }); + fs.writeFileSync(destPath, content); + } else { + fs.copyFileSync(file, destPath); + } + } + + fs.mkdirSync(path.join(umdOutputDir, 'Utilities'), { recursive: true }); + copyDir( + 'Utilities/XMLConverter', + `${umdOutputDir}/Utilities/XMLConverter` + ); + copyDir( + 'Utilities/DataGenerator', + `${umdOutputDir}/Utilities/DataGenerator` + ); + copyDir('Utilities/config', `${umdOutputDir}/Utilities/config`); + fs.copyFileSync( + 'Utilities/prepare.js', + `${umdOutputDir}/Utilities/prepare.js` + ); + + fs.copyFileSync( + 'Utilities/build/macro-shim.d.ts', + `${umdOutputDir}/Sources/macro.d.ts` + ); + fs.copyFileSync( + 'Utilities/build/macro-shim.js', + `${umdOutputDir}/Sources/macro.js` + ); + + copyCommonPackageAssets(umdOutputDir); + writePackageManifest(umdOutputDir, (pkg) => { + pkg.name = 'vtk.js'; + pkg.main = './vtk.js'; + delete pkg.module; + delete pkg.type; + delete pkg.types; + }); + + // vtk-lite.js is a deprecated alias of vtk.js; see BREAKING_CHANGES.md. + const vtkBundle = path.join(umdOutputDir, 'vtk.js'); + if (fs.existsSync(vtkBundle)) { + fs.copyFileSync(vtkBundle, path.join(umdOutputDir, 'vtk-lite.js')); + } + }, + }; +} + +/** + * Writes dist/esm/index.d.ts with triple-slash refs to every emitted .d.ts. + */ +export function generateDtsReferencesPlugin({ esmOutputDir }) { + return { + name: 'vtk-generate-dts-references', + closeBundle() { + const dtsReferences = [ + '/// ', + '/// ', + ]; + + const jsFiles = glob.sync('**/*.js', { cwd: esmOutputDir }); + for (const file of jsFiles) { + const dtsFile = file.replace(/\.js$/, '.d.ts'); + const dtsPath = path.join(esmOutputDir, dtsFile); + + const flat = flattenIndexEntry(file.replace(/\.js$/, '')); + const flatDtsFile = + flat !== file.replace(/\.js$/, '') ? `${flat}.d.ts` : null; + const flatDtsPath = flatDtsFile + ? path.join(esmOutputDir, flatDtsFile) + : null; + + if (fs.existsSync(dtsPath) && dtsFile !== 'index.d.ts') { + dtsReferences.push( + `/// ` + ); + } else if (flatDtsPath && fs.existsSync(flatDtsPath)) { + dtsReferences.push( + `/// ` + ); + } + } + + fs.writeFileSync( + path.join(esmOutputDir, 'index.d.ts'), + dtsReferences.join('\r\n') + ); + }, + }; +} + +/** + * Removes Vite's `/assets` directory if empty/unwanted. + */ +export function cleanupAssetsPlugin(outputDir) { + return { + name: `vtk-cleanup-${path.basename(outputDir)}`, + closeBundle() { + const assetsDir = path.join(outputDir, 'assets'); + if (fs.existsSync(assetsDir)) { + fs.rmSync(assetsDir, { recursive: true, force: true }); + } + }, + }; +} + +/** + * Prepends a