diff --git a/.circleci/config.yml b/.circleci/config.yml index 2fcd48297951c5..da73d5ec046da5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -43,6 +43,7 @@ default-job: &default-job AWS_REGION_ARTIFACTS: eu-central-1 COREPACK_ENABLE_DOWNLOAD_PROMPT: '0' DANGER_DISABLE_TRANSPILATION: 'true' + MUI_EXPERIMENTAL_MJS: 1 working_directory: /tmp/material-ui docker: - image: cimg/node:20.17 @@ -239,14 +240,6 @@ jobs: pnpm --filter @mui/material typescript:module-augmentation pnpm --filter @mui/joy typescript:module-augmentation pnpm --filter @mui/system typescript:module-augmentation - - run: - name: Diff declaration files - command: | - git add -f packages/mui-material/build || echo '/material declarations do not exist' - git add -f packages/mui-lab/build || echo '/lab declarations do not exist' - git add -f packages/mui-utils/build || echo '/utils declarations do not exist' - pnpm -r build:stable && pnpm -r build:types - git --no-pager diff - run: name: Any defect declaration files? command: node scripts/testBuiltTypes.mjs @@ -595,7 +588,7 @@ jobs: - run: name: build @mui packages - command: pnpm lerna run --ignore @mui/icons-material --concurrency 6 --scope "@mui/*" build + command: pnpm release:build --scope "@mui/*" --ignore @mui/icons-material - aws-cli/setup: aws_access_key_id: $AWS_ACCESS_KEY_ID_ARTIFACTS aws_secret_access_key: $AWS_SECRET_ACCESS_KEY_ARTIFACTS diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3268d38b70091..38cdab6f924384 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,8 @@ jobs: cache: 'pnpm' # https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#caching-packages-dependencies - run: pnpm install:codesandbox - run: pnpm build:codesandbox + env: + MUI_EXPERIMENTAL_MJS: 1 - run: pnpm pkg-pr-new-release # Tests dev-only scripts across all supported dev environments diff --git a/docs/src/components/typography/foo.ts b/docs/src/components/typography/foo.ts new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/netlify.toml b/netlify.toml index 7ceaf30e657d2f..fb94526627ec2d 100644 --- a/netlify.toml +++ b/netlify.toml @@ -8,6 +8,7 @@ [build.environment] NODE_VERSION = "20" PNPM_FLAGS = "--frozen-lockfile" + MUI_EXPERIMENTAL_MJS = "1" [[plugins]] package = "./packages/netlify-plugin-cache-docs" diff --git a/package.json b/package.json index 8e587d637ebcd0..852904fb083bf2 100644 --- a/package.json +++ b/package.json @@ -189,6 +189,8 @@ "prettier": "^3.6.2", "pretty-quick": "^4.2.2", "process": "^0.11.10", + "regexp.escape": "^2.0.1", + "resolve-pkg-maps": "^1.0.0", "rimraf": "^6.0.1", "serve": "^14.2.4", "stylelint": "^16.22.0", @@ -209,6 +211,15 @@ "engines": { "pnpm": "10.13.1" }, + "pnpm": { + "packageExtensions": { + "@pigment-css/react": { + "peerDependencies": { + "@mui/types": "7" + } + } + } + }, "resolutions": { "@babel/core": "^7.28.0", "@babel/plugin-transform-runtime": "^7.28.0", diff --git a/packages-internal/docs-utils/src/createTypeScriptProject.ts b/packages-internal/docs-utils/src/createTypeScriptProject.ts index ba3b64e54df3c2..ef0473c943e39f 100644 --- a/packages-internal/docs-utils/src/createTypeScriptProject.ts +++ b/packages-internal/docs-utils/src/createTypeScriptProject.ts @@ -38,7 +38,7 @@ export const createTypeScriptProject = ( const { name, rootPath, - tsConfigPath: inputTsConfigPath = 'tsconfig.build.json', + tsConfigPath: inputTsConfigPath = 'tsconfig.json', entryPointPath: inputEntryPointPath, files, } = options; diff --git a/packages-internal/docs-utils/tsconfig.build.json b/packages-internal/docs-utils/tsconfig.build.json index 8992c3d55a7be0..8b4a1cfa3e8c77 100644 --- a/packages-internal/docs-utils/tsconfig.build.json +++ b/packages-internal/docs-utils/tsconfig.build.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "paths": {}, "rootDir": "./src", "outDir": "./build", "declaration": true, diff --git a/packages-internal/test-utils/tsconfig.build.json b/packages-internal/test-utils/tsconfig.build.json index 62bf0d03440fe5..2b531ff5aa6a63 100644 --- a/packages-internal/test-utils/tsconfig.build.json +++ b/packages-internal/test-utils/tsconfig.build.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "paths": {}, "rootDir": "./src", "outDir": "./build", "declaration": true, diff --git a/packages/api-docs-builder/utils/createTypeScriptProject.ts b/packages/api-docs-builder/utils/createTypeScriptProject.ts index 2965e6a9b063aa..87046eff9cdc40 100644 --- a/packages/api-docs-builder/utils/createTypeScriptProject.ts +++ b/packages/api-docs-builder/utils/createTypeScriptProject.ts @@ -43,7 +43,7 @@ export const createTypeScriptProject = ( const { name, rootPath, - tsConfigPath: inputTsConfigPath = 'tsconfig.build.json', + tsConfigPath: inputTsConfigPath = 'tsconfig.json', entryPointPath: inputEntryPointPath, files, } = options; diff --git a/packages/mui-docs/src/Ad/ad.styles.ts b/packages/mui-docs/src/Ad/ad.styles.ts index 822fc29b18885b..8f5c7ae2b50204 100644 --- a/packages/mui-docs/src/Ad/ad.styles.ts +++ b/packages/mui-docs/src/Ad/ad.styles.ts @@ -1,6 +1,6 @@ -import { alpha, Theme } from '@mui/material/styles'; +import { alpha, CSSProperties, Theme } from '@mui/material/styles'; -export const adBodyImageStyles = (theme: Theme) => ({ +export const adBodyImageStyles = (theme: Theme): Record => ({ root: { display: 'block', overflow: 'hidden', @@ -45,7 +45,7 @@ export const adBodyImageStyles = (theme: Theme) => ({ }, }); -export const adBodyInlineStyles = (theme: Theme) => { +export const adBodyInlineStyles = (theme: Theme): Record => { const baseline = adBodyImageStyles(theme); return { diff --git a/packages/mui-docs/tsconfig.build.json b/packages/mui-docs/tsconfig.build.json index 8141d5e5a49e9f..d4a8f67bd78a91 100644 --- a/packages/mui-docs/tsconfig.build.json +++ b/packages/mui-docs/tsconfig.build.json @@ -3,7 +3,7 @@ // Actual .ts source files are transpiled via babel "extends": "./tsconfig.json", "compilerOptions": { - "composite": true, + "paths": {}, "declaration": true, "noEmit": false, "emitDeclarationOnly": true, @@ -12,10 +12,5 @@ "tsBuildInfoFile": "build/tsconfig.build.tsbuildinfo" }, "include": ["./types.d.ts", "src/**/*.ts*", "src/**/*.json"], - "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"], - "references": [ - { "path": "../mui-material/tsconfig.build.json" }, - { "path": "../mui-system/tsconfig.build.json" }, - { "path": "../mui-icons-material/tsconfig.build.json" } - ] + "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"] } diff --git a/packages/mui-icons-material/tsconfig.build.json b/packages/mui-icons-material/tsconfig.build.json index ccc01046d18965..916ea70606566c 100644 --- a/packages/mui-icons-material/tsconfig.build.json +++ b/packages/mui-icons-material/tsconfig.build.json @@ -3,7 +3,7 @@ // Actual .ts source files are transpiled via babel "extends": "./tsconfig.json", "compilerOptions": { - "composite": true, + "paths": {}, "declaration": false, "noEmit": false, "emitDeclarationOnly": false, diff --git a/packages/mui-joy/src/styles/variantColorInheritance.tsx b/packages/mui-joy/src/styles/variantColorInheritance.tsx index f15ea4c85ec58c..2b80cf177d1e5c 100644 --- a/packages/mui-joy/src/styles/variantColorInheritance.tsx +++ b/packages/mui-joy/src/styles/variantColorInheritance.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { ColorPaletteProp, VariantProp } from '@mui/joy/styles/types'; +import { ColorPaletteProp, VariantProp } from './types'; const VariantColorContext = React.createContext(undefined); diff --git a/packages/mui-joy/tsconfig.build.json b/packages/mui-joy/tsconfig.build.json index 3da937fe8e1890..81f8518e0be753 100644 --- a/packages/mui-joy/tsconfig.build.json +++ b/packages/mui-joy/tsconfig.build.json @@ -3,7 +3,7 @@ // Actual .ts source files are transpiled via babel "extends": "./tsconfig.json", "compilerOptions": { - "composite": true, + "paths": {}, "declaration": true, "noEmit": false, "emitDeclarationOnly": true, @@ -11,6 +11,5 @@ "rootDir": "./src" }, "include": ["./src/**/*.ts*"], - "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"], - "references": [{ "path": "../mui-system/tsconfig.build.json" }] + "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"] } diff --git a/packages/mui-lab/tsconfig.build.json b/packages/mui-lab/tsconfig.build.json index d4a77daf9abbab..dcc908471b8fde 100644 --- a/packages/mui-lab/tsconfig.build.json +++ b/packages/mui-lab/tsconfig.build.json @@ -3,6 +3,7 @@ // Actual .ts source files are transpiled via babel "extends": "./tsconfig.json", "compilerOptions": { + "paths": {}, "noEmit": false, "declaration": true, "rootDir": "./src", @@ -10,9 +11,5 @@ "emitDeclarationOnly": true }, "include": ["src/**/*.ts*"], - "exclude": ["src/**/*.d.ts", "src/**/*.test.*", "./**/*.spec.*"], - "references": [ - { "path": "../mui-material/tsconfig.build.json" }, - { "path": "../mui-system/tsconfig.build.json" } - ] + "exclude": ["src/**/*.d.ts", "src/**/*.test.*", "./**/*.spec.*"] } diff --git a/packages/mui-material-nextjs/tsconfig.build.json b/packages/mui-material-nextjs/tsconfig.build.json index 26c0289c7f7928..9bfd7261aed2a3 100644 --- a/packages/mui-material-nextjs/tsconfig.build.json +++ b/packages/mui-material-nextjs/tsconfig.build.json @@ -3,7 +3,7 @@ // Actual .ts source files are transpiled via babel "extends": "./tsconfig.json", "compilerOptions": { - "composite": true, + "paths": {}, "declaration": true, "noEmit": false, "emitDeclarationOnly": true, diff --git a/packages/mui-material-pigment-css/tsconfig.build.json b/packages/mui-material-pigment-css/tsconfig.build.json index e0c0a5a8aa68af..1b866619436535 100644 --- a/packages/mui-material-pigment-css/tsconfig.build.json +++ b/packages/mui-material-pigment-css/tsconfig.build.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "composite": true, + "paths": {}, "declaration": true, "noEmit": false, "emitDeclarationOnly": true, diff --git a/packages/mui-material/src/themeCssVarsAugmentation/index.d.ts b/packages/mui-material/src/themeCssVarsAugmentation/index.ts similarity index 99% rename from packages/mui-material/src/themeCssVarsAugmentation/index.d.ts rename to packages/mui-material/src/themeCssVarsAugmentation/index.ts index 5db7c7a9419006..f5f2f884649f3b 100644 --- a/packages/mui-material/src/themeCssVarsAugmentation/index.d.ts +++ b/packages/mui-material/src/themeCssVarsAugmentation/index.ts @@ -3,6 +3,7 @@ export {}; * Enhance the theme types to include new properties from the CssVarsProvider. * The theme is typed with CSS variables in `styled`, `sx`, `useTheme`, etc. */ + declare module '@mui/material/styles' { interface CssThemeVariables { enabled: true; diff --git a/packages/mui-material/tsconfig.build.json b/packages/mui-material/tsconfig.build.json index 3da937fe8e1890..323e3ef6173f57 100644 --- a/packages/mui-material/tsconfig.build.json +++ b/packages/mui-material/tsconfig.build.json @@ -3,7 +3,13 @@ // Actual .ts source files are transpiled via babel "extends": "./tsconfig.json", "compilerOptions": { - "composite": true, + "paths": { + // reset paths, but keep the one for @mui/material. Necessary to be able to build + // packages/mui-material/src/types/OverridableComponentAugmentation.ts + // packages/mui-material/src/themeCssVarsAugmentation/index.ts + // as the types to be overriden are otherwise not found + "@mui/material/*": ["./src/*"] + }, "declaration": true, "noEmit": false, "emitDeclarationOnly": true, @@ -11,6 +17,5 @@ "rootDir": "./src" }, "include": ["./src/**/*.ts*"], - "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"], - "references": [{ "path": "../mui-system/tsconfig.build.json" }] + "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"] } diff --git a/packages/mui-stylis-plugin-rtl/tsconfig.build.json b/packages/mui-stylis-plugin-rtl/tsconfig.build.json index 26edb3c2b1ea91..cd9a7f5a02a452 100644 --- a/packages/mui-stylis-plugin-rtl/tsconfig.build.json +++ b/packages/mui-stylis-plugin-rtl/tsconfig.build.json @@ -3,7 +3,7 @@ // Actual .ts source files are transpiled via babel "extends": "./tsconfig.json", "compilerOptions": { - "composite": true, + "paths": {}, "declaration": true, "noEmit": false, "emitDeclarationOnly": true, diff --git a/packages/mui-system/tsconfig.build.json b/packages/mui-system/tsconfig.build.json index 3ffd11e1d6ac57..95e1058b14bb09 100644 --- a/packages/mui-system/tsconfig.build.json +++ b/packages/mui-system/tsconfig.build.json @@ -3,7 +3,7 @@ // Actual .ts source files are transpiled via babel "extends": "./tsconfig.json", "compilerOptions": { - "composite": true, + "paths": {}, "declaration": true, "noEmit": false, "emitDeclarationOnly": true, @@ -11,6 +11,5 @@ "rootDir": "./src" }, "include": ["src/**/*.ts*"], - "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"], - "references": [{ "path": "../mui-utils/tsconfig.build.json" }] + "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"] } diff --git a/packages/mui-types/tsconfig.build.json b/packages/mui-types/tsconfig.build.json index 53b2bcb1e5a7b6..c7a60e77923fbd 100644 --- a/packages/mui-types/tsconfig.build.json +++ b/packages/mui-types/tsconfig.build.json @@ -3,7 +3,12 @@ // Actual .ts source files are transpiled via babel "extends": "./tsconfig.json", "compilerOptions": { - "composite": true, + "paths": { + // reset paths, but keep the one for @mui/types. Necessary to be able to build + // packages/mui-types/src/OverridableComponentAugmentation.ts + // as the types to be overriden are otherwise not found + "@mui/types": ["./src"] + }, "declaration": true, "noEmit": false, "emitDeclarationOnly": true, diff --git a/packages/mui-utils/package.json b/packages/mui-utils/package.json index fb92d04e6ad94d..16e77e81a7f1a5 100644 --- a/packages/mui-utils/package.json +++ b/packages/mui-utils/package.json @@ -27,7 +27,7 @@ "scripts": { "build": "pnpm build:node && pnpm build:stable && pnpm build:types && pnpm build:copy-files", "build:node": "node ../../scripts/build.mjs node", - "build:stable": "node ../../scripts/build.mjs stable", + "build:stable": "node ../../scripts/build.mjs stable --verbose", "build:copy-files": "node ../../scripts/copyFiles.mjs", "build:types": "tsx ../../scripts/buildTypes.mts", "prebuild": "rimraf build tsconfig.build.tsbuildinfo", diff --git a/packages/mui-utils/tsconfig.build.json b/packages/mui-utils/tsconfig.build.json index 915404fe310d87..cd9a7f5a02a452 100644 --- a/packages/mui-utils/tsconfig.build.json +++ b/packages/mui-utils/tsconfig.build.json @@ -3,7 +3,7 @@ // Actual .ts source files are transpiled via babel "extends": "./tsconfig.json", "compilerOptions": { - "composite": true, + "paths": {}, "declaration": true, "noEmit": false, "emitDeclarationOnly": true, @@ -12,6 +12,5 @@ "types": ["react", "node"] }, "include": ["src/**/*.ts"], - "exclude": ["src/**/*.test.ts*", "src/**/*.spec.ts*"], - "references": [{ "path": "../mui-types/tsconfig.build.json" }] + "exclude": ["src/**/*.test.ts*", "src/**/*.spec.ts*"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20a4766d00c84b..f154deac416122 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,8 @@ overrides: '@pigment-css/nextjs-plugin': 0.0.30 '@pigment-css/vite-plugin': 0.0.30 +packageExtensionsChecksum: sha256-xTBNl6mGQcaqoVbDkrOKAeqxB9KeBPa4Z33lG3mhLOo= + patchedDependencies: styled-components: hash: 383c648dfdb5dfc82fbe414d54027d8c982a01c6320370f0ecfdb387e753c09f @@ -123,7 +125,7 @@ importers: version: 22.0.0 '@pigment-css/react': specifier: 0.0.30 - version: 0.0.30(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3) + version: 0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3) '@playwright/test': specifier: 1.54.1 version: 1.54.1 @@ -286,6 +288,12 @@ importers: process: specifier: ^0.11.10 version: 0.11.10 + regexp.escape: + specifier: ^2.0.1 + version: 2.0.1 + resolve-pkg-maps: + specifier: ^1.0.0 + version: 1.0.0 rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -373,7 +381,7 @@ importers: devDependencies: '@pigment-css/nextjs-plugin': specifier: 0.0.30 - version: 0.0.30(@types/react@19.1.8)(next@15.4.4(@babel/core@7.28.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(webpack-sources@3.3.3) + version: 0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(next@15.4.4(@babel/core@7.28.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(webpack-sources@3.3.3) '@types/node': specifier: ^20.19.9 version: 20.19.9 @@ -428,7 +436,7 @@ importers: devDependencies: '@pigment-css/nextjs-plugin': specifier: 0.0.30 - version: 0.0.30(@types/react@19.1.8)(next@15.4.4(@babel/core@7.28.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(webpack-sources@3.3.3) + version: 0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(next@15.4.4(@babel/core@7.28.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(webpack-sources@3.3.3) '@types/node': specifier: ^20.19.9 version: 20.19.9 @@ -498,7 +506,7 @@ importers: version: 7.27.1(@babel/core@7.28.0) '@pigment-css/vite-plugin': specifier: 0.0.30 - version: 0.0.30(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3)(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)(terser@5.39.0)) + version: 0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3)(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)(terser@5.39.0)) '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1708,7 +1716,7 @@ importers: version: link:../mui-system/build '@pigment-css/react': specifier: 0.0.30 - version: 0.0.30(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3) + version: 0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3) publishDirectory: build packages/mui-private-theming: @@ -4980,6 +4988,7 @@ packages: resolution: {integrity: sha512-aNvpOgbv+M9+YV2wKk3CIyiiiF+8S6KJJKDKGzhFWOVWeQFZBgTOjBHhL/0SyAnCOVjDg2sSXOEElIdEQywXKQ==} engines: {node: '>=14.0.0'} peerDependencies: + '@mui/types': '7' react: ^17.0.0 || ^18.0.0 || ^19.0.0 '@pigment-css/unplugin@0.0.30': @@ -12524,6 +12533,10 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.escape@2.0.1: + resolution: {integrity: sha512-JItRb4rmyTzmERBkAf6J87LjDPy/RscIwmaJQ3gsFlAzrmZbZU8LwBw5IydFZXW9hqpgbPlGbMhtpqtuAhMgtg==} + engines: {node: '>= 0.4'} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -18026,18 +18039,19 @@ snapshots: '@opentelemetry/api@1.8.0': optional: true - '@pigment-css/nextjs-plugin@0.0.30(@types/react@19.1.8)(next@15.4.4(@babel/core@7.28.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(webpack-sources@3.3.3)': + '@pigment-css/nextjs-plugin@0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(next@15.4.4(@babel/core@7.28.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(webpack-sources@3.3.3)': dependencies: - '@pigment-css/unplugin': 0.0.30(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3)(webpack-sources@3.3.3) + '@pigment-css/unplugin': 0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3)(webpack-sources@3.3.3) next: 15.4.4(@babel/core@7.28.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) transitivePeerDependencies: + - '@mui/types' - '@types/react' - react - supports-color - typescript - webpack-sources - '@pigment-css/react@0.0.30(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3)': + '@pigment-css/react@0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3)': dependencies: '@babel/core': 7.28.0 '@babel/helper-module-imports': 7.27.1 @@ -18050,6 +18064,7 @@ snapshots: '@emotion/serialize': 1.3.3 '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@mui/system': 6.4.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + '@mui/types': 7.4.4(@types/react@19.1.8) '@mui/utils': 6.4.8(@types/react@19.1.8)(react@19.1.0) '@wyw-in-js/processor-utils': 0.5.5 '@wyw-in-js/shared': 0.5.5 @@ -18067,31 +18082,33 @@ snapshots: - supports-color - typescript - '@pigment-css/unplugin@0.0.30(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3)(webpack-sources@3.3.3)': + '@pigment-css/unplugin@0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3)(webpack-sources@3.3.3)': dependencies: '@babel/core': 7.28.0 - '@pigment-css/react': 0.0.30(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3) + '@pigment-css/react': 0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3) '@wyw-in-js/shared': 0.5.5 '@wyw-in-js/transform': 0.5.5(typescript@5.8.3) babel-plugin-define-var: 0.1.0 unplugin: 1.15.0(webpack-sources@3.3.3) transitivePeerDependencies: + - '@mui/types' - '@types/react' - react - supports-color - typescript - webpack-sources - '@pigment-css/vite-plugin@0.0.30(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3)(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)(terser@5.39.0))': + '@pigment-css/vite-plugin@0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3)(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)(terser@5.39.0))': dependencies: '@babel/core': 7.28.0 '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) - '@pigment-css/react': 0.0.30(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3) + '@pigment-css/react': 0.0.30(@mui/types@7.4.4(@types/react@19.1.8))(@types/react@19.1.8)(react@19.1.0)(typescript@5.8.3) '@wyw-in-js/shared': 0.5.5 '@wyw-in-js/transform': 0.5.5(typescript@5.8.3) babel-plugin-define-var: 0.1.0 vite: 5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)(terser@5.39.0) transitivePeerDependencies: + - '@mui/types' - '@types/react' - react - supports-color @@ -27377,6 +27394,15 @@ snapshots: regenerator-runtime@0.14.1: {} + regexp.escape@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + for-each: 0.3.5 + safe-regex-test: 1.1.0 + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 diff --git a/scripts/build.mjs b/scripts/build.mjs index a520e0e0dc2dc0..75c2122d82655b 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import childProcess from 'child_process'; import path from 'path'; import { promisify } from 'util'; @@ -6,6 +7,9 @@ import * as fs from 'fs/promises'; import { cjsCopy } from './copyFilesUtils.mjs'; import { getVersionEnvVariables, getWorkspaceRoot } from './utils.mjs'; +// Use .mjs extension for ESM output files if the MUI_EXPERIMENTAL_MJS environment variable is set. +const EXPERIMENTAL_MJS = !!process.env.MUI_EXPERIMENTAL_MJS; + const exec = promisify(childProcess.exec); const validBundles = [ @@ -71,7 +75,13 @@ async function run(argv) { ...babelIgnore, ]; - const outFileExtension = '.js'; + let outFileExtension = '.js'; + + if (EXPERIMENTAL_MJS && bundle === 'stable') { + outFileExtension = '.mjs'; + } + + console.log(`Building ${bundle} bundle with outFileExtension: ${outFileExtension}`); const relativeOutDir = { node: cjsDir, @@ -114,7 +124,6 @@ async function run(argv) { const command = ['pnpm babel', ...babelArgs].join(' '); if (verbose) { - // eslint-disable-next-line no-console console.log(`running '${command}' with ${JSON.stringify(env)}`); } @@ -131,7 +140,7 @@ async function run(argv) { // Write a package.json file in the output directory if we are building the stable bundle // or if the output directory is not the root of the package. const shouldWriteBundlePackageJson = bundle === 'stable' || relativeOutDir !== './'; - if (shouldWriteBundlePackageJson && !argv.skipEsmPkg) { + if (!EXPERIMENTAL_MJS && shouldWriteBundlePackageJson && !argv.skipEsmPkg) { const rootBundlePackageJson = path.join(outDir, 'package.json'); await fs.writeFile( rootBundlePackageJson, @@ -140,7 +149,6 @@ async function run(argv) { } if (verbose) { - // eslint-disable-next-line no-console console.log(stdout); } } diff --git a/scripts/buildTypes.mts b/scripts/buildTypes.mts index 78ab604506f735..fee5ede419ce1c 100644 --- a/scripts/buildTypes.mts +++ b/scripts/buildTypes.mts @@ -4,33 +4,35 @@ import path from 'path'; import yargs from 'yargs'; import { $ } from 'execa'; import * as babel from '@babel/core'; -import { parse } from 'jsonc-parser'; const $$ = $({ stdio: 'inherit' }); +// Use .mjs extension for ESM output files if the MUI_EXPERIMENTAL_MJS environment variable is set. +const EXPERIMENTAL_MJS = !!process.env.MUI_EXPERIMENTAL_MJS; + async function emitDeclarations(tsconfig: string, outDir: string) { // eslint-disable-next-line no-console console.log(`Building types for ${path.resolve(tsconfig)}`); await $$`tsc -p ${tsconfig} --outDir ${outDir} --declaration --emitDeclarationOnly`; } -async function postProcessImports(folder: string, removeCss: boolean) { +async function postProcessImports(folder: string, removeCss: boolean, filter = '.d.ts') { // eslint-disable-next-line no-console console.log(`Adding import extensions`); - const dtsFiles = await glob('**/*.d.ts', { absolute: true, cwd: folder }); + const dtsFiles = await glob(`**/*${filter}`, { absolute: true, cwd: folder }); if (dtsFiles.length === 0) { - throw new Error(`Unable to find declaration files in '${folder}'`); + return; } - const babelPlugins: babel.PluginItem[] = [ - ['@babel/plugin-syntax-typescript', { dts: true }], - ['@mui/internal-babel-plugin-resolve-imports'], - ]; + const babelPlugins: babel.PluginItem[] = [['@babel/plugin-syntax-typescript', { dts: true }]]; if (removeCss) { babelPlugins.push(['babel-plugin-transform-remove-imports', { test: /\.css$/ }]); } + // this plugin needs to come after remove-imports so that css imports are already removed + babelPlugins.push(['@mui/internal-babel-plugin-resolve-imports']); + await Promise.all( dtsFiles.map(async (dtsFile) => { const result = await babel.transformFileAsync(dtsFile, { @@ -70,9 +72,28 @@ async function copyDeclarations(sourceDirectory: string, destinationDirectory: s }); } +async function renameDtsFilesToDmts(sourceDirectory: string) { + const dtsFiles = await glob('**/*.d.ts', { absolute: true, cwd: sourceDirectory }); + if (dtsFiles.length === 0) { + console.warn('No .d.ts files found in the directory. Skipping renaming to .d.mts'); + return; + } + + console.log('Renaming .d.ts files to .d.mts files in', sourceDirectory); + await Promise.all( + dtsFiles.map(async (dtsFile) => { + // Rename the file from .d.ts to .d.mts + const basename = path.basename(dtsFile, '.d.ts'); + const dirname = path.dirname(dtsFile); + const newFilePath = path.join(dirname, `${basename}.d.mts`); + await fs.rename(dtsFile, newFilePath); + }), + ); +} + interface HandlerArgv { skipTsc: boolean; - copy: string[]; + cjsDir: string; removeCss: boolean; } @@ -84,17 +105,12 @@ async function main(argv: HandlerArgv) { () => false, ); - const tsConfig = tsconfigExists - ? (parse(await fs.readFile(tsconfigPath, 'utf-8')) as { compilerOptions: { outDir: string } }) - : null; - const srcPath = path.join(packageRoot, 'src'); const buildFolder = path.join(packageRoot, 'build'); - const esmOrOutDir = tsConfig?.compilerOptions.outDir - ? path.join(packageRoot, tsConfig.compilerOptions.outDir) - : path.join(buildFolder, 'esm'); + const esmDir = path.join(buildFolder, 'esm'); + const cjsDir = path.join(buildFolder, argv.cjsDir); - await copyDeclarations(srcPath, esmOrOutDir); + await copyDeclarations(srcPath, esmDir); if (!argv.skipTsc) { if (!tsconfigExists) { @@ -105,14 +121,17 @@ async function main(argv: HandlerArgv) { ); } - await emitDeclarations(tsconfigPath, esmOrOutDir); + await emitDeclarations(tsconfigPath, esmDir); } - await postProcessImports(esmOrOutDir, argv.removeCss); + await copyDeclarations(esmDir, cjsDir); - await Promise.all( - argv.copy.map((copy) => copyDeclarations(esmOrOutDir, path.join(packageRoot, copy))), - ); + if (EXPERIMENTAL_MJS) { + await renameDtsFilesToDmts(esmDir); + } + + await postProcessImports(esmDir, argv.removeCss, '.d.mts'); + await postProcessImports(cjsDir, argv.removeCss, '.d.ts'); const tsbuildinfo = await glob('**/*.tsbuildinfo', { absolute: true, cwd: buildFolder }); await Promise.all(tsbuildinfo.map(async (file) => fs.rm(file))); @@ -129,11 +148,10 @@ yargs(process.argv.slice(2)) default: false, describe: 'Set to `true` if you want the legacy behavior of just copying .d.ts files.', }) - .option('copy', { - alias: 'c', - type: 'array', - description: 'Directories where the type definition files should be copied', - default: ['build'], + .option('cjsDir', { + type: 'string', + description: 'Directory under the build folder where the cjs build lives', + default: '.', }) .option('removeCss', { type: 'boolean', diff --git a/scripts/copyFiles.mjs b/scripts/copyFiles.mjs index ad6de871f8e982..3276a4ff2e2c54 100644 --- a/scripts/copyFiles.mjs +++ b/scripts/copyFiles.mjs @@ -16,7 +16,7 @@ async function addLicense(packageData) { */ `; await Promise.all( - ['./index.js', './esm/index.js', './modern/index.js', './node/index.js'].map(async (file) => { + ['./index.js', './esm/index.js', './esm/index.mjs', './cjs/index.js'].map(async (file) => { try { await prepend(path.resolve(buildPath, file), license); } catch (err) { diff --git a/scripts/copyFilesUtils.mjs b/scripts/copyFilesUtils.mjs index 14069eb338b70e..b1433d014cdc0b 100644 --- a/scripts/copyFilesUtils.mjs +++ b/scripts/copyFilesUtils.mjs @@ -2,6 +2,10 @@ import path from 'path'; import fse from 'fs-extra'; import glob from 'fast-glob'; +import { shimPackageExports } from './exportsUtils.mjs'; + +// Use .mjs extension for ESM output files if the MUI_EXPERIMENTAL_MJS environment variable is set. +const EXPERIMENTAL_MJS = !!process.env.MUI_EXPERIMENTAL_MJS; const packagePath = process.cwd(); const buildPath = path.join(packagePath, './build'); @@ -21,66 +25,6 @@ export async function includeFileInBuild(file, target = path.basename(file)) { console.log(`Copied ${sourcePath} to ${targetPath}`); } -/** - * Puts a package.json into every immediate child directory of rootDir. - * That package.json contains information about esm for bundlers so that imports - * like import Typography from '@mui/material/Typography' are tree-shakeable. - * - * It also tests that an this import can be used in TypeScript by checking - * if an index.d.ts is present at that path. - * TODO: kept around for backwards compatibility, remove once X is on ESM-exports package layout - * @param {object} param0 - * @param {string} param0.from - * @param {string} param0.to - */ -export async function createModulePackages({ from, to }) { - const directoryPackages = glob.sync('*/index.{js,ts,tsx}', { cwd: from }).map(path.dirname); - - await Promise.all( - directoryPackages.map(async (directoryPackage) => { - const packageJsonPath = path.join(to, directoryPackage, 'package.json'); - const topLevelPathImportsAreCommonJSModules = await fse.pathExists( - path.resolve(path.dirname(packageJsonPath), '../esm'), - ); - - const packageJson = { - sideEffects: false, - module: topLevelPathImportsAreCommonJSModules - ? path.posix.join('../esm', directoryPackage, 'index.js') - : './index.js', - main: topLevelPathImportsAreCommonJSModules - ? './index.js' - : path.posix.join('../node', directoryPackage, 'index.js'), - types: './index.d.ts', - }; - - const [typingsEntryExist, moduleEntryExists, mainEntryExists] = await Promise.all([ - fse.pathExists(path.resolve(path.dirname(packageJsonPath), packageJson.types)), - fse.pathExists(path.resolve(path.dirname(packageJsonPath), packageJson.module)), - fse.pathExists(path.resolve(path.dirname(packageJsonPath), packageJson.main)), - fse.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)), - ]); - - const manifestErrorMessages = []; - if (!typingsEntryExist) { - manifestErrorMessages.push(`'types' entry '${packageJson.types}' does not exist`); - } - if (!moduleEntryExists) { - manifestErrorMessages.push(`'module' entry '${packageJson.module}' does not exist`); - } - if (!mainEntryExists) { - manifestErrorMessages.push(`'main' entry '${packageJson.main}' does not exist`); - } - if (manifestErrorMessages.length > 0) { - // TODO: AggregateError - throw new Error(`${packageJsonPath}:\n${manifestErrorMessages.join('\n')}`); - } - - return packageJsonPath; - }), - ); -} - export async function typescriptCopy({ from, to }) { if (!(await fse.pathExists(to))) { console.warn(`path ${to} does not exists`); @@ -120,8 +64,8 @@ function createExportFor(exportName, conditions) { default: `./${baseName}.js`, }, import: { - types: `./esm/${baseName}.d.ts`, - default: `./esm/${baseName}.js`, + types: `./esm/${baseName}.d.${EXPERIMENTAL_MJS ? 'mts' : 'ts'}`, + default: `./esm/${baseName}.${EXPERIMENTAL_MJS ? 'mjs' : 'js'}`, }, ...rest, }, @@ -160,7 +104,7 @@ export async function createPackageFile(useEsmExports = false) { Object.assign(packageExports, { ...createExportFor('./*', { [srcCondition]: './src/*/index.ts' }), ...createExportFor('./esm', null), - ...createExportFor('./modern', null), + ...createExportFor('./esm/*', null), }); } @@ -177,7 +121,7 @@ export async function createPackageFile(useEsmExports = false) { ...(packageDataOther.main ? { main: './index.js', - module: './esm/index.js', + module: `./esm/index.${EXPERIMENTAL_MJS ? 'mjs' : 'js'}`, } : {}), exports: packageExports, @@ -187,12 +131,8 @@ export async function createPackageFile(useEsmExports = false) { private: false, ...(packageDataOther.main ? { - main: fse.existsSync(path.resolve(buildPath, './node/index.js')) - ? './node/index.js' - : './index.js', - module: fse.existsSync(path.resolve(buildPath, './esm/index.js')) - ? './esm/index.js' - : './index.js', + main: './index.js', + module: `./esm/index.${EXPERIMENTAL_MJS ? 'mjs' : 'js'}`, } : {}), }; @@ -211,6 +151,14 @@ export async function createPackageFile(useEsmExports = false) { await fse.writeFile(targetPath, JSON.stringify(newPackageData, null, 2), 'utf8'); console.log(`Created package.json in ${targetPath}`); + // Create shim structure for package exports + if (newPackageData.exports) { + await shimPackageExports(buildPath, newPackageData.exports, { + sideEffects: newPackageData.sideEffects, + }); + console.log(`Created shim structure in ${buildPath}`); + } + return newPackageData; } diff --git a/scripts/coreTypeScriptProjects.js b/scripts/coreTypeScriptProjects.js index 5b3d0cd3f3825e..a4aaf02329466f 100644 --- a/scripts/coreTypeScriptProjects.js +++ b/scripts/coreTypeScriptProjects.js @@ -19,6 +19,5 @@ export default { }, docs: { rootPath: path.join(process.cwd(), 'docs'), - tsConfigPath: 'tsconfig.json', }, }; diff --git a/scripts/exportsUtils.mjs b/scripts/exportsUtils.mjs new file mode 100644 index 00000000000000..69da915782c5e3 --- /dev/null +++ b/scripts/exportsUtils.mjs @@ -0,0 +1,326 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { resolveExports } from 'resolve-pkg-maps'; +import fg from 'fast-glob'; +import regexpEscape from 'regexp.escape'; + +const processedObjects = new WeakMap(); + +function ensurePrefix(str, prefix) { + return str.startsWith(prefix) ? str : `${prefix}${str}`; +} + +function ensureNoPrefix(str, prefix) { + return str.startsWith(prefix) ? str.slice(prefix.length) : str; +} + +/** + * Finds all exported paths from a package.json exports field and resolves them to actual file paths. + * + * @param {Object} options - Configuration options + * @param {string} [options.cwd=process.cwd()] - Working directory containing package.json + * @param {Object} [options.exports] - Exports object to analyze (if not provided, reads from package.json) + * @returns {Promise>>} Map of export paths to resolved files + */ + +export async function findAllExportedPaths({ cwd = process.cwd(), exports } = {}) { + let exportsObj = exports; + + // Read package.json if exports not provided + if (!exportsObj) { + try { + const packageJsonPath = path.resolve(cwd, 'package.json'); + const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8'); + const packageJson = JSON.parse(packageJsonContent); + exportsObj = packageJson.exports; + } catch (error) { + throw new Error(`Failed to read package.json from ${cwd}: ${error.message}`); + } + } + + if (!exportsObj) { + return new Map(); + } + + // Phase 1: Collect all patterns + const patterns = []; + collectPatterns(exportsObj, [], patterns); + + // Phase 2: Resolve patterns sequentially + const results = new Map(); + + for (const pattern of patterns) { + if (pattern.filePattern === null) { + // This is a blocking pattern - remove matching entries + const blockingRegex = createBlockingRegex(pattern.exportPattern); + // Remove all matching entries from results + for (const exportPath of results.keys()) { + if (blockingRegex.test(exportPath)) { + results.delete(exportPath); + } + } + } else { + // Normal pattern - resolve and add to results + // eslint-disable-next-line no-await-in-loop + const resolvedResults = await resolvePattern(pattern, cwd); + + for (const result of resolvedResults) { + if (!results.has(result.exportPath)) { + results.set(result.exportPath, []); + } + + results.get(result.exportPath).push({ + conditions: result.conditions, + path: result.filePath, + }); + } + } + } + + return results; +} + +/** + * Phase 1: Recursively collect all export patterns + */ +function collectPatterns(exportsObj, conditions, patterns, exportPath = '') { + if (exportsObj === null) { + // Handle null exports (blocking patterns) + patterns.push({ + exportPattern: exportPath, + filePattern: null, // null indicates this blocks the path + conditions: [...conditions], + }); + return; + } + + if (typeof exportsObj === 'string') { + if (exportsObj.trim() === '') { + throw new Error(`Empty export path found for ${exportPath}`); + } + + // Validate path doesn't go outside package + if (exportsObj.includes('..')) { + throw new Error( + `Export path ${exportsObj} attempts to access files outside package directory`, + ); + } + + patterns.push({ + exportPattern: exportPath, + filePattern: exportsObj, + conditions: [...conditions], + }); + return; + } + + if (typeof exportsObj === 'object' && !Array.isArray(exportsObj)) { + // Prevent circular references using WeakMap + if (processedObjects.has(exportsObj)) { + throw new Error(`Circular reference detected in exports at ${exportPath}`); + } + + processedObjects.set(exportsObj, true); + + try { + for (const [key, value] of Object.entries(exportsObj)) { + if (key.startsWith('.')) { + // This is an export path + collectPatterns(value, conditions, patterns, key); + } else { + // This is a condition + const newConditions = [...conditions, key]; + collectPatterns(value, newConditions, patterns, exportPath); + } + } + } finally { + processedObjects.delete(exportsObj); + } + } else if (Array.isArray(exportsObj)) { + throw new Error(`Arrays are not supported in exports field at ${exportPath}`); + } else { + throw new Error(`Invalid export value type: ${typeof exportsObj} at ${exportPath}`); + } +} + +/** + * Create regex to match paths that should be blocked by null exports + */ +function createBlockingRegex(exportPattern) { + const escaped = regexpEscape(exportPattern); + const regexPattern = escaped.replace('\\*', '.*'); + return new RegExp(`^${regexPattern}$`); +} + +/** + * Phase 2: Resolve a single pattern to actual file paths + */ +async function resolvePattern(pattern, cwd) { + const { exportPattern, filePattern, conditions } = pattern; + + if (!filePattern.includes('*')) { + // Non-wildcard pattern - return immediately + const absolutePath = path.resolve(cwd, filePattern); + return [ + { + exportPath: exportPattern, + filePath: absolutePath, + conditions, + }, + ]; + } + + // Wildcard pattern - use glob and regex + const globPattern = convertToGlob(filePattern); + + const matchedFiles = await fg(globPattern, { + cwd, + absolute: false, + onlyFiles: true, + ignore: ['node_modules/**', '.git/**', '**/.DS_Store'], + }); + + const wildcardIndex = filePattern.indexOf('*'); + const leadingChars = wildcardIndex; + const trailingChars = filePattern.length - wildcardIndex - 1; + return matchedFiles.map((matchedFile) => { + matchedFile = ensurePrefix(matchedFile, './'); + const expandedWildcard = matchedFile.slice(leadingChars, matchedFile.length - trailingChars); + const exportPath = exportPattern.replace('*', expandedWildcard); + const absolutePath = path.resolve(cwd, matchedFile); + + return { + exportPath, + filePath: absolutePath, + conditions, + }; + }); +} + +/** + * Convert file pattern with * to glob pattern + */ +function convertToGlob(pattern) { + if (!pattern.includes('*')) { + return pattern; // No wildcard + } + + // Check for exactly one wildcard + if (pattern.indexOf('*') !== pattern.lastIndexOf('*')) { + throw new Error(`Export pattern can only contain one wildcard: ${pattern}`); + } + + // Rule 1: * between two / slashes → convert to ** + if (pattern.includes('/*/')) { + return pattern.replace('/*/', '/**/*/'); + } + + // Rule 2: * between / and file extension → convert to **/*. + if (pattern.includes('/*.')) { + return pattern.replace('/*.', '/**/*.'); + } + + // Rule 3: * as final segment → convert to **/* + if (pattern.endsWith('/*')) { + return pattern.replace(/\/\*$/, '/**/*'); + } + + // If we reach here, it's an invalid wildcard usage + throw new Error(`Invalid wildcard pattern: ${pattern}. Wildcard must be entire path segment.`); +} + +/** + * Resolves and converts export path to be relative to shim location + * @param {Object} exports - Exports object from package.json + * @param {string} exportPath - Export path to resolve (without leading "./") + * @param {string[]} conditions - Conditions to resolve with + * @param {string} packageRoot - Absolute path to package root + * @param {string} shimLocation - Absolute path to shim directory + * @returns {string|null} Path relative to shim location with "./" prefix, or null if resolution fails + */ +function resolveForShim(exports, exportPath, conditions, packageRoot, shimLocation) { + try { + const results = resolveExports(exports, exportPath, conditions); + if (results && results.length > 0) { + const resolvedPath = results[0]; + const absoluteResolvedPath = path.resolve(packageRoot, resolvedPath); + const relativePath = path.relative(shimLocation, absoluteResolvedPath); + return relativePath.startsWith('.') ? relativePath : `./${relativePath}`; + } + } catch (error) { + // Ignore resolution errors + } + return null; +} + +/** + * Creates package.json shim files for all exported paths + * + * @param {string} cwd - Working directory to create shims in + * @param {Object} exports - Exports object from package.json + * @returns {Promise} + */ +export async function shimPackageExports(cwd, exports, pkgJson = {}) { + const exportedPaths = await findAllExportedPaths({ cwd, exports }); + + const iterator = exportedPaths.keys(); + const concurrency = 100; // Limit concurrent file operations + + // Worker function that processes items from shared iterator + // Avoid `Error: EMFILE: too many open files` on large packages + const worker = async () => { + for (const exportPath of iterator) { + if (exportPath === '.') { + continue; // Skip root export + } + + // Skip package.json + if (exportPath === './package.json') { + continue; + } + + // Skip non-JavaScript files + if (/\.[a-zA-Z0-9]+$/.test(exportPath) && !/\.(js|jsx|mjs|cjs|ts|tsx)$/.test(exportPath)) { + continue; + } + + // Create the shim directory + const shimDir = path.resolve(cwd, exportPath); + const absoluteCwd = path.resolve(cwd); + const pathToResolve = ensureNoPrefix(exportPath, './'); + + // Resolve and convert paths to be relative to shim location + const typesPath = resolveForShim(exports, pathToResolve, ['types'], absoluteCwd, shimDir); + const cjsPath = resolveForShim(exports, pathToResolve, ['require'], absoluteCwd, shimDir); + const esmPath = resolveForShim(exports, pathToResolve, ['import'], absoluteCwd, shimDir); + + // Skip if neither ESM nor CJS resolved + if (!cjsPath && !esmPath) { + continue; + } + + // Create package.json content + const packageJsonContent = { ...pkgJson }; + if (cjsPath) { + packageJsonContent.main = cjsPath; + } + if (esmPath) { + packageJsonContent.module = esmPath; + } + if (typesPath) { + packageJsonContent.types = typesPath; + } + + // Write the shim package.json + const shimPackageJsonPath = path.join(shimDir, 'package.json'); + // eslint-disable-next-line no-await-in-loop + await fs.mkdir(shimDir, { recursive: true }); + // eslint-disable-next-line no-await-in-loop + await fs.writeFile(shimPackageJsonPath, JSON.stringify(packageJsonContent, null, 2)); + } + }; + + // Start multiple workers concurrently + const workers = Array.from({ length: concurrency }, worker); + await Promise.all(workers); +}