diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74ba3ce..7ef110d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: node-version: 20 cache: 'npm' - - run: npm ci --legacy-peer-deps + - run: npm ci - uses: nrwl/nx-set-shas@v4 - run: npx nx format:check diff --git a/CONTRIBUTING.MD b/CONTRIBUTING.MD index f3a4a54..2547e6b 100644 --- a/CONTRIBUTING.MD +++ b/CONTRIBUTING.MD @@ -13,6 +13,7 @@ Thank you for your interest in contributing to the Angular Toolkit MCP! This doc - [Submitting Changes](#submitting-changes) - [Documentation](#documentation) - [Debugging](#debugging) +- [Release Process](#release-process) ## 🚀 Getting Started @@ -38,7 +39,7 @@ Thank you for your interest in contributing to the Angular Toolkit MCP! This doc 2. **Build the project:** ```bash - npx nx build angular-mcp + npx nx build angular-toolkit-mcp ``` ### Nx Workspace Commands @@ -209,7 +210,7 @@ Before committing, ensure: Start the MCP server in debug mode: ```bash -npx nx run angular-mcp:debug +npx nx run angular-toolkit-mcp:debug ``` This starts the server with the MCP Inspector for debugging. @@ -222,6 +223,78 @@ This starts the server with the MCP Inspector for debugging. - Use `console.log` or debugger statements in development - Test with the minimal-repo examples +## 📦 Release Process + +### Publishing to npm + +The Angular Toolkit MCP is published to npm as `@push-based/angular-toolkit-mcp`. Only maintainers with appropriate permissions can publish new versions. + +### Release Steps + +1. **Update Version** + + Update the version in `packages/angular-mcp/package.json` following semantic versioning: + - **Patch** (0.1.0 → 0.1.1): Bug fixes + - **Minor** (0.1.0 → 0.2.0): New features (backwards compatible) + - **Major** (0.1.0 → 1.0.0): Breaking changes + +2. **Build the Package** + ```bash + npx nx build angular-toolkit-mcp + ``` + +3. **Test the Package** + ```bash + cd packages/angular-mcp/dist + npm pack + # Test the generated .tgz file + node main.js --help + ``` + +4. **Authenticate with npm** + ```bash + npm login + ``` + Ensure you have access to the `@push-based` scope. + +5. **Publish to npm** + ```bash + npm run publish:mcp + ``` + Or manually: + ```bash + npx nx build angular-toolkit-mcp + cd packages/angular-mcp/dist + npm publish + ``` + +6. **Verify Publication** + ```bash + npm view @push-based/angular-toolkit-mcp + npx @push-based/angular-toolkit-mcp@latest --help + ``` + +7. **Tag the Release** + ```bash + git tag v0.1.0 + git push origin v0.1.0 + ``` + +8. **Update Documentation** + - Update CHANGELOG.md with release notes + - Update any version references in documentation + +### Pre-release Checklist + +Before publishing a new version: +- [ ] All tests pass (`npx nx run-many --target=test --all`) +- [ ] No linting errors (`npx nx run-many --target=lint --all`) +- [ ] Build succeeds (`npx nx build angular-toolkit-mcp`) +- [ ] Version number updated in package.json +- [ ] CHANGELOG.md updated with changes +- [ ] Documentation updated as needed +- [ ] Local npm pack test successful + ## 📄 License By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/README.md b/README.md index 44b641f..7ae2e25 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,27 @@ A Model Context Protocol (MCP) server that provides Angular project analysis and - Breaking change detection during component updates - Code quality analysis and improvement +## Quick Start + +Install and run via npx (no manual build required): + +```json +{ + "mcpServers": { + "angular-toolkit": { + "command": "npx", + "args": [ + "@push-based/angular-toolkit-mcp@latest", + "--workspaceRoot=/absolute/path/to/your/angular/workspace", + "--ds.uiRoot=packages/ui" + ] + } + } +} +``` + +**Required Node.js version:** 18 or higher + ## Configuration ### Prerequisites @@ -28,16 +49,22 @@ A Model Context Protocol (MCP) server that provides Angular project analysis and ### Installation & Setup +#### For Users + +Simply use npx as shown in the Quick Start section above. No installation or build required. + +#### For Contributors (Local Development) + 1. Clone the repository -2. Build the MCP +2. Install dependencies and build the MCP - ```bash + ```bash npm install npx nx build angular-mcp - ``` + ``` -2. Locate the built server +3. Locate the built server After building, the server will be available at `packages/angular-mcp/dist/main.js` @@ -45,22 +72,40 @@ A Model Context Protocol (MCP) server that provides Angular project analysis and Add the server to your MCP client configuration (e.g., Claude Desktop, Cursor, Copilot, Windsurf or other MCP-compatible clients): -#### For Cursor (`.cursor/mcp.json` or MCP settings): +#### For Users (npx - Recommended) + +```json +{ + "mcpServers": { + "angular-toolkit": { + "command": "npx", + "args": [ + "@push-based/angular-toolkit-mcp@latest", + "--workspaceRoot=/absolute/path/to/your/angular/workspace", + "--ds.uiRoot=relative/path/to/ui/components", + "--ds.storybookDocsRoot=relative/path/to/storybook/docs", + "--ds.deprecatedCssClassesPath=relative/path/to/component-options.mjs" + ] + } + } +} +``` + +#### For Contributors (Local Development) -Copy `.cursor/mcp.json.example` to the project you're working on. Copied file should be: `.cursor/mcp.json` and update `angular-toolkit-mcp` values accordingly: +When developing locally, point to the built server: ```json { "mcpServers": { - ...(other servers)... "angular-toolkit-mcp": { "command": "node", "args": [ - "/absolute/path/to/angular-mcp-server/packages/angular-mcp-server/dist/index.js", + "/absolute/path/to/angular-toolkit-mcp/packages/angular-mcp/dist/main.js", "--workspaceRoot=/absolute/path/to/your/angular/workspace", + "--ds.uiRoot=relative/path/to/ui/components", "--ds.storybookDocsRoot=relative/path/to/storybook/docs", - "--ds.deprecatedCssClassesPath=relative/path/to/component-options.js", - "--ds.uiRoot=relative/path/to/ui/components" + "--ds.deprecatedCssClassesPath=relative/path/to/component-options.mjs" ] } } @@ -85,7 +130,7 @@ Copy `.cursor/mcp.json.example` to the project you're working on. Copied file sh | Parameter | Type | Description | Example | |-----------|------|-------------|---------| | `ds.storybookDocsRoot` | Relative path | Root directory containing Storybook documentation used by documentation-related tools | `storybook/docs` | -| `ds.deprecatedCssClassesPath` | Relative path | JavaScript file mapping deprecated CSS classes used by violation and deprecated CSS tools | `design-system/component-options.js` | +| `ds.deprecatedCssClassesPath` | Relative path | JavaScript file mapping deprecated CSS classes used by violation and deprecated CSS tools | `design-system/component-options.mjs` | When optional parameters are omitted: @@ -94,7 +139,7 @@ When optional parameters are omitted: #### Deprecated CSS Classes File Format -The `component-options.js` file should export an array of component configurations: +The `component-options.mjs` file should export an array of component configurations: ```javascript const dsComponents = [ @@ -121,7 +166,7 @@ my-angular-workspace/ │ │ ├── modal/ │ │ └── ... │ └── design-system/ -│ └── component-options.js # ds.deprecatedCssClassesPath +│ └── component-options.mjs # ds.deprecatedCssClassesPath ├── storybook/ │ └── docs/ # ds.storybookDocsRoot └── apps/ @@ -132,7 +177,7 @@ my-angular-workspace/ - **Server not starting**: Ensure all paths are correct and the server is built - **Permission errors**: Check that the Node.js process has read access to all specified directories -- **Component not found**: Verify that component names in `component-options.js` match your actual component class names +- **Component not found**: Verify that component names in `component-options.mjs` match your actual component class names - **Path resolution issues**: Use absolute paths for `workspaceRoot` and relative paths (from workspace root) for other parameters ## Available Tools diff --git a/docs/getting-started.md b/docs/getting-started.md index 1ec6c08..8881476 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -44,7 +44,7 @@ Instead of the palette-based flow, copy the manual configuration from your works "./packages/angular-mcp/dist/main.js", "--workspaceRoot=/absolute/path/to/angular-toolkit-mcp", "--ds.storybookDocsRoot=packages/minimal-repo/packages/design-system/storybook-host-app/src/components", - "--ds.deprecatedCssClassesPath=packages/minimal-repo/packages/design-system/component-options.js", + "--ds.deprecatedCssClassesPath=packages/minimal-repo/packages/design-system/component-options.mjs", "--ds.uiRoot=packages/minimal-repo/packages/design-system/ui" ] } diff --git a/package-lock.json b/package-lock.json index 8b2f48b..a71ff10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,6 +68,7 @@ "eslint-config-prettier": "10.1.5", "eslint-plugin-functional": "^9.0.2", "eslint-plugin-unicorn": "^59.0.1", + "ignore-loader": "^0.1.2", "jest": "^29.7.0", "jest-environment-node": "^29.7.0", "jiti": "2.4.2", @@ -10734,14 +10735,14 @@ "resolved": "packages/shared/angular-ast-utils", "link": true }, - "node_modules/@push-based/angular-mcp": { - "resolved": "packages/angular-mcp", - "link": true - }, "node_modules/@push-based/angular-mcp-server": { "resolved": "packages/angular-mcp-server", "link": true }, + "node_modules/@push-based/angular-toolkit-mcp": { + "resolved": "packages/angular-mcp", + "link": true + }, "node_modules/@push-based/ds-component-coverage": { "resolved": "packages/shared/ds-component-coverage", "link": true @@ -20622,6 +20623,12 @@ "node": ">= 4" } }, + "node_modules/ignore-loader": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ignore-loader/-/ignore-loader-0.1.2.tgz", + "integrity": "sha512-yOJQEKrNwoYqrWLS4DcnzM7SEQhRKis5mB+LdKKh4cPmGYlLPR0ozRzHV5jmEk2IxptqJNQA5Cc0gw8Fj12bXA==", + "dev": true + }, "node_modules/ignore-walk": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", @@ -31625,10 +31632,14 @@ } }, "packages/angular-mcp": { - "name": "@push-based/angular-mcp", - "version": "0.0.1", + "name": "@push-based/angular-toolkit-mcp", + "version": "0.2.0", + "license": "MIT", "bin": { - "angular-mcp": "main.js" + "angular-toolkit-mcp": "main.js" + }, + "engines": { + "node": ">=18" } }, "packages/angular-mcp-server": { diff --git a/package.json b/package.json index b045d46..ffaa46c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "0.0.0", "type": "module", "license": "MIT", - "scripts": {}, + "scripts": { + "publish:mcp": "nx build @push-based/angular-toolkit-mcp && cd packages/angular-mcp/dist && npm publish" + }, "private": true, "devDependencies": { "@eslint/js": "^9.28.0", @@ -34,6 +36,7 @@ "eslint-config-prettier": "10.1.5", "eslint-plugin-functional": "^9.0.2", "eslint-plugin-unicorn": "^59.0.1", + "ignore-loader": "^0.1.2", "jest": "^29.7.0", "jest-environment-node": "^29.7.0", "jiti": "2.4.2", diff --git a/packages/angular-mcp-server/src/lib/angular-mcp-server.ts b/packages/angular-mcp-server/src/lib/angular-mcp-server.ts index ff5590e..d03b7b2 100644 --- a/packages/angular-mcp-server/src/lib/angular-mcp-server.ts +++ b/packages/angular-mcp-server/src/lib/angular-mcp-server.ts @@ -90,64 +90,71 @@ export class AngularMcpServerWrapper { this.mcpServer.server.setRequestHandler( ListResourcesRequestSchema, async (): Promise => { + const resources = []; + + // Try to read the llms.txt file from the package root (optional) try { - // Read the llms.txt file from the package root const filePath = path.resolve(__dirname, '../../llms.txt'); - console.log('Attempting to read file from:', filePath); - const content = fs.readFileSync(filePath, 'utf-8'); - const lines = content.split('\n'); - const resources = []; - let currentSection = ''; + // Only attempt to read if file exists + if (fs.existsSync(filePath)) { + console.log('Reading llms.txt from:', filePath); + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); + let currentSection = ''; - // Skip empty lines and comments that don't start with # - if (!line || (line.startsWith('#') && !line.includes(':'))) { - continue; - } + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); - // Update section if line starts with # - if (line.startsWith('# ')) { - currentSection = line.substring(2).replace(':', '').trim(); - continue; - } + // Skip empty lines and comments that don't start with # + if (!line || (line.startsWith('#') && !line.includes(':'))) { + continue; + } - // Parse markdown links: [name](url) - const linkMatch = line.match(/- \[(.*?)\]\((.*?)\):(.*)/); - if (linkMatch) { - const [, name, uri, description = ''] = linkMatch; - resources.push({ - uri, - name: name.trim(), - type: currentSection.toLowerCase(), - content: description.trim() || name.trim(), - }); - continue; - } + // Update section if line starts with # + if (line.startsWith('# ')) { + currentSection = line.substring(2).replace(':', '').trim(); + continue; + } - // Parse simple links: - [name](url) - const simpleLinkMatch = line.match(/- \[(.*?)\]\((.*?)\)/); - if (simpleLinkMatch) { - const [, name, uri] = simpleLinkMatch; - resources.push({ - uri, - name: name.trim(), - type: currentSection.toLowerCase(), - content: name.trim(), - }); - } - } + // Parse markdown links: [name](url) + const linkMatch = line.match(/- \[(.*?)\]\((.*?)\):(.*)/); + if (linkMatch) { + const [, name, uri, description = ''] = linkMatch; + resources.push({ + uri, + name: name.trim(), + type: currentSection.toLowerCase(), + content: description.trim() || name.trim(), + }); + continue; + } - // Scan available design system components to add them as discoverable resources - try { - if (!this.storybookDocsRoot) { - return { - resources, - }; + // Parse simple links: - [name](url) + const simpleLinkMatch = line.match(/- \[(.*?)\]\((.*?)\)/); + if (simpleLinkMatch) { + const [, name, uri] = simpleLinkMatch; + resources.push({ + uri, + name: name.trim(), + type: currentSection.toLowerCase(), + content: name.trim(), + }); + } } + } else { + console.log('llms.txt not found at:', filePath, '(skipping)'); + } + } catch (ctx: unknown) { + if (ctx instanceof Error) { + console.error('Error reading llms.txt (non-fatal):', ctx.message); + } + } + // Scan available design system components to add them as discoverable resources + try { + if (this.storybookDocsRoot) { const dsUiPath = path.resolve( process.cwd(), this.storybookDocsRoot, @@ -175,46 +182,19 @@ export class AngularMcpServerWrapper { }); } } - } catch (ctx: unknown) { - if (ctx instanceof Error) { - console.error('Error scanning DS components:', ctx); - } } - - return { - resources, - }; } catch (ctx: unknown) { if (ctx instanceof Error) { - console.error('Error reading llms.txt:', ctx); - // Return a more informative error message - return { - resources: [ - { - uri: 'error://file-not-found', - name: 'Error Reading Resources', - type: 'error', - content: `Failed to read llms.txt: ${ - ctx.message - }. Attempted path: ${path.resolve( - __dirname, - '../../llms.txt', - )}`, - }, - ], - }; + console.error( + 'Error scanning DS components (non-fatal):', + ctx.message, + ); } - return { - resources: [ - { - uri: 'error://unknown', - name: 'Unknown Error', - type: 'error', - content: 'An unknown error occurred while reading resources', - }, - ], - }; } + + return { + resources, + }; }, ); } diff --git a/packages/angular-mcp/README.md b/packages/angular-mcp/README.md index f1ee2cc..fe3b950 100644 --- a/packages/angular-mcp/README.md +++ b/packages/angular-mcp/README.md @@ -1,7 +1,94 @@ -# angular-mcp +# Angular Toolkit MCP -This library was generated with [Nx](https://nx.dev). +A Model Context Protocol (MCP) server that provides Angular project analysis and refactoring capabilities. This server enables LLMs to analyze Angular projects for component usage patterns, dependency analysis, code quality issues, and provides automated refactoring assistance. -## Building +## Installation -Run `nx build angular-mcp-server` to build the library. +No installation required! Run directly with npx: + +```bash +npx @push-based/angular-toolkit-mcp@latest --workspaceRoot=/path/to/workspace --ds.uiRoot=packages/ui +``` + +## Configuration + +Add the server to your MCP client configuration (e.g., Claude Desktop, Cursor, Copilot, Windsurf): + +```json +{ + "mcpServers": { + "angular-toolkit": { + "command": "npx", + "args": [ + "@push-based/angular-toolkit-mcp@latest", + "--workspaceRoot=/absolute/path/to/your/angular/workspace", + "--ds.uiRoot=packages/ui", + "--ds.storybookDocsRoot=storybook/docs", + "--ds.deprecatedCssClassesPath=design-system/component-options.mjs" + ] + } + } +} +``` + +### Configuration Parameters + +#### Required Parameters + +| Parameter | Type | Description | Example | +|-----------|------|-------------|---------| +| `workspaceRoot` | Absolute path | Root directory of your Angular workspace | `/Users/dev/my-angular-app` | +| `ds.uiRoot` | Relative path | Directory containing UI components | `packages/ui` | + +#### Optional Parameters + +| Parameter | Type | Description | Example | +|-----------|------|-------------|---------| +| `ds.storybookDocsRoot` | Relative path | Root directory containing Storybook documentation | `storybook/docs` | +| `ds.deprecatedCssClassesPath` | Relative path | JavaScript file mapping deprecated CSS classes | `design-system/component-options.mjs` | + +## Key Features + +- **Component Analysis**: Detect deprecated CSS classes and component usage violations +- **Safe Refactoring**: Generate contracts for safe component refactoring with breaking change detection +- **Dependency Mapping**: Map component dependencies across modules, templates, and styles +- **ESLint Integration**: Lint Angular files with automatic ESLint configuration discovery +- **Project Analysis**: Analyze buildable/publishable libraries and validate import paths +- **Component Documentation**: Retrieve component data and documentation, list available components + +## Available Tools + +### Component Analysis +- `report-violations` - Report deprecated CSS usage in a directory +- `report-deprecated-css` - Report deprecated CSS classes found in styling files +- `get-deprecated-css-classes` - List deprecated CSS classes for a component +- `list-ds-components` - List all available Design System components +- `get-ds-component-data` - Get component data including implementation and documentation +- `build-component-usage-graph` - Map component imports across the project + +### Component Contracts +- `build_component_contract` - Generate a static surface contract for a component +- `diff_component_contract` - Compare before/after contracts for breaking changes +- `list_component_contracts` - List all available component contracts + +### Project Analysis +- `get-project-dependencies` - Analyze project dependencies and library configuration +- `lint-changes` - Lint changed Angular files using ESLint rules + +## Requirements + +- Node.js version 18 or higher + +## Documentation + +For comprehensive documentation, guides, and workflows, see the [full documentation](https://github.com/push-based/angular-toolkit-mcp). + +## License + +MIT License - see the [LICENSE](https://github.com/push-based/angular-toolkit-mcp/blob/main/LICENSE) file for details. + +--- + +
+

Sponsored by Entain

+
diff --git a/packages/angular-mcp/package.json b/packages/angular-mcp/package.json index 365156e..ec6b167 100644 --- a/packages/angular-mcp/package.json +++ b/packages/angular-mcp/package.json @@ -1,10 +1,34 @@ { - "name": "@push-based/angular-mcp", - "version": "0.0.1", - "private": true, - "bin": "main.js", + "name": "@push-based/angular-toolkit-mcp", + "version": "0.2.0", + "description": "A Model Context Protocol server for Angular project analysis and refactoring", + "keywords": [ + "mcp", + "angular", + "refactoring", + "analysis", + "model-context-protocol" + ], + "author": "Push-Based", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/push-based/angular-toolkit-mcp.git" + }, + "homepage": "https://github.com/push-based/angular-toolkit-mcp", + "bugs": "https://github.com/push-based/angular-toolkit-mcp/issues", + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + }, + "bin": { + "angular-toolkit-mcp": "main.js" + }, "files": [ "main.js", + "*.js", "README.md" ], "nx": { @@ -41,7 +65,7 @@ "dependsOn": [ "build" ], - "command": "npx @modelcontextprotocol/inspector node packages/angular-mcp/dist/main.js --workspaceRoot=/root/path/to/workspace --ds.uiRoot=packages/minimal-repo/packages/design-system/ui --ds.storybookDocsRoot=packages/minimal-repo/packages/design-system/storybook-host-app/src/components --ds.deprecatedCssClassesPath=packages/minimal-repo/packages/design-system/component-options.js" + "command": "npx @modelcontextprotocol/inspector node packages/angular-mcp/dist/main.js --workspaceRoot=/root/path/to/workspace --ds.uiRoot=packages/minimal-repo/packages/design-system/ui --ds.storybookDocsRoot=packages/minimal-repo/packages/design-system/storybook-host-app/src/components --ds.deprecatedCssClassesPath=packages/minimal-repo/packages/design-system/component-options.mjs" } } } diff --git a/packages/angular-mcp/src/main.ts b/packages/angular-mcp/src/main.ts index 07a9065..dd919f2 100644 --- a/packages/angular-mcp/src/main.ts +++ b/packages/angular-mcp/src/main.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node import express from 'express'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; diff --git a/packages/angular-mcp/webpack.config.cjs b/packages/angular-mcp/webpack.config.cjs index 8304034..d825e58 100644 --- a/packages/angular-mcp/webpack.config.cjs +++ b/packages/angular-mcp/webpack.config.cjs @@ -1,20 +1,54 @@ const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); const { join } = require('path'); +const webpack = require('webpack'); module.exports = { output: { path: join(__dirname, 'dist'), }, + resolve: { + extensions: ['.ts', '.js', '.mjs', '.cjs', '.json'], + }, + externals: [ + // Keep Node.js built-ins external + function ({ request }, callback) { + if (/^node:/.test(request)) { + return callback(null, 'commonjs ' + request); + } + callback(); + }, + ], + module: { + rules: [ + { + test: /\.d\.ts$/, + loader: 'ignore-loader', + }, + ], + }, plugins: [ new NxAppWebpackPlugin({ target: 'node', compiler: 'tsc', main: './src/main.ts', tsConfig: './tsconfig.app.json', - assets: ['./src/assets'], + assets: [ + './src/assets', + { + input: '.', + glob: 'README.md', + output: '.', + }, + ], optimization: false, outputHashing: 'none', generatePackageJson: true, + externalDependencies: 'none', + }), + new webpack.BannerPlugin({ + banner: '#!/usr/bin/env node', + raw: true, + entryOnly: true, }), ], }; diff --git a/packages/minimal-repo/packages/design-system/component-options.js b/packages/minimal-repo/packages/design-system/component-options.mjs similarity index 100% rename from packages/minimal-repo/packages/design-system/component-options.js rename to packages/minimal-repo/packages/design-system/component-options.mjs diff --git a/packages/shared/utils/src/lib/file/default-export-loader.ts b/packages/shared/utils/src/lib/file/default-export-loader.ts index 73ec399..9485c93 100644 --- a/packages/shared/utils/src/lib/file/default-export-loader.ts +++ b/packages/shared/utils/src/lib/file/default-export-loader.ts @@ -17,7 +17,18 @@ export async function loadDefaultExport( ): Promise { try { const fileUrl = pathToFileURL(filePath).toString(); - const module = await import(fileUrl); + + // In test environments (Vitest), use native import to avoid transformation issues + // In production (webpack/bundled), use Function constructor to preserve dynamic import + const isTestEnv = + typeof process !== 'undefined' && + (process.env.NODE_ENV === 'test' || + process.env.VITEST === 'true' || + typeof (globalThis as Record).vitest !== 'undefined'); + + const module = isTestEnv + ? await import(fileUrl) + : await new Function('url', 'return import(url)')(fileUrl); if (!('default' in module)) { throw new Error(