diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 00000000..b03f4086
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+lts/krypton
diff --git a/.storybook/main.js b/.storybook/main.js
index cbf56f51..649d43b5 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -1,5 +1,5 @@
-const sass = require("sass");
const path = require("path");
+const sass = require("sass");
const sassRenderSyncConfig = require("./../scripts/sassConfig");
const { silenceDeprecations } = require("../scripts/sassDeprecationConfig");
@@ -9,15 +9,6 @@ module.exports = {
"@storybook/addon-links",
"@storybook/addon-docs",
"@storybook/addon-a11y",
- {
- name: "@storybook/preset-scss",
- options: {
- sassLoaderOptions: {
- implementation: sass,
- sassOptions: {...sassRenderSyncConfig, silenceDeprecations},
- },
- },
- },
"@storybook/addon-webpack5-compiler-babel",
],
framework: {
@@ -64,6 +55,16 @@ module.exports = {
},
},
webpackFinal: async (config, { configType }) => {
+ // never let the compiled package output take part in the Storybook build,
+ // stories always have to use the sources
+ // a directory import like `"./../../../../"` is resolved by the `exports` field of our
+ // own package.json and would silently pull `dist/esm/` into the preview bundle,
+ // this restriction removes it from the candidates so that the root `index.ts` is used
+ const distPath = path.resolve(__dirname, "..", "dist");
+ config.resolve.restrictions = [
+ ...(config.resolve.restrictions ?? []),
+ new RegExp(`^(?!${distPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[/\\\\])`),
+ ];
// `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
if (configType === "PRODUCTION") {
// remove source maps from production storybook
@@ -72,13 +73,29 @@ module.exports = {
config.devtool = false;
}
config.module.rules = [
+ {
+ test: /\.s[ac]ss$/i,
+ use: [
+ require.resolve("style-loader"),
+ {
+ loader: require.resolve("css-loader"),
+ options: {
+ importLoaders: 1,
+ },
+ },
+ {
+ loader: require.resolve("sass-loader"),
+ options: {
+ implementation: sass,
+ sassOptions: { ...sassRenderSyncConfig, silenceDeprecations },
+ },
+ },
+ ],
+ },
{
test: /\.(png|jpg|gif|svg)(\\?.*)?$/,
include: /\.tobase64\./,
- loader: "url-loader",
- options: {
- limit: true,
- },
+ type: "asset/inline",
},
...config.module.rules.map((rule) => {
if (
@@ -92,11 +109,6 @@ module.exports = {
return rule;
}),
];
- config.resolve.alias = {
- ...config.resolve.alias,
- // Fix nasty bug with importing from this package, Storybook webpack cannot resolve it otherwise
- "@codemirror/legacy-modes": path.resolve(__dirname, "../node_modules/@codemirror/legacy-modes"),
- };
return config;
},
};
diff --git a/.storybook/preview.js b/.storybook/preview.js
index be7d5ee2..dfd916d4 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -17,7 +17,7 @@ export const parameters = {
const preview = {
// Enables auto-generated documentation for all stories
// @see https://storybook.js.org/docs/writing-docs/autodocs
- tags: ['autodocs'],
+ tags: ["autodocs"],
parameters,
};
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 301b6e92..715decb0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,10 +11,27 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- ``
- `noDrag` parameter: Add the `nodrag` class to the Switch element. Default: `true`
+### Changed
+
+- Upgrading base libraries
+ - Carbon, Codemirror, React-Flow
+- minimum node version (`engines.node`) is `18.19.0` now
+ - the build of the ESM distribution needs a synchronous `import.meta.resolve`, which is only available since this version
+
### Fixed
- ``
- fix description and story to point out that `PropertyValueList` need always to be used as wrapper
+- BOM issue on compressed stylesheet
+ - first rule `selector` becomes `BOM:selector` that is valid but will never apply
+ - we fixed this problem by adding a dummy rule as first rule
+- ESM distribution
+ - the imports of the `@codemirror/legacy-modes` modes were written with an additional `.js` suffix, but the `exports` map of this package only provides the extension-less sub paths, so they were expanded to unresolvable paths like `mode/jinja2.js.js`
+- Storybook
+ - the `` story imported the package root directory, this way the `exports` field of our own `package.json` pulled the built `dist/esm/` output into the preview bundle instead of the sources, and the Storybook build failed as soon as `dist/` existed
+ - the webpack configuration excludes `dist/` from module resolution now, so the sources are always used even if a story references the package root
+- Added explicitly `assert` dependency
+ - the linter of `` uses `jshint`, which imports `console-browserify`, and this package requires the node core modules `assert` and `util` without declaring them; bundlers based on webpack 5 do not provide shims for node core modules anymore, so the polyfill (and `util` together with it) is part of the delivery now
### Deprecated
diff --git a/babel.config.json b/babel.config.json
index e3abab77..2c3b19e1 100644
--- a/babel.config.json
+++ b/babel.config.json
@@ -1,8 +1,3 @@
{
- "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript", "@babel/preset-flow"],
- "plugins": [
- ["@babel/plugin-transform-private-methods", { "loose": true }],
- ["@babel/plugin-transform-private-property-in-object", { "loose": true }],
- ["@babel/plugin-transform-class-properties", { "loose": true }]
- ]
+ "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
}
diff --git a/package.json b/package.json
index 51c9caa6..8f3bf94c 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
"registry": "https://registry.npmjs.org"
},
"engines": {
- "node": ">=18.18.0"
+ "node": ">=18.19.0"
},
"style": "src/index.scss",
"main": "dist/cjs/index.js",
@@ -71,18 +71,19 @@
"@blueprintjs/colors": "^5.1.16",
"@blueprintjs/core": "6.8.1",
"@blueprintjs/select": "6.1.1",
- "@carbon/icons": "^11.80.0",
- "@carbon/react": "^1.107.1",
- "@codemirror/lang-html": "^6.4.11",
+ "@carbon/icons": "^11.87.0",
+ "@carbon/react": "^1.115.0",
+ "@codemirror/lang-html": "^6.4.12",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/lang-json": "^6.0.2",
- "@codemirror/lang-markdown": "^6.5.0",
+ "@codemirror/lang-markdown": "^6.5.2",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.3",
- "@codemirror/legacy-modes": "^6.5.2",
+ "@codemirror/legacy-modes": "^6.5.3",
"@mavrin/remark-typograf": "^2.2.0",
- "@xyflow/react": "^12.9.3",
+ "@xyflow/react": "^12.11.5",
+ "assert": "^2.1.0",
"classnames": "^2.5.1",
"codemirror": "^6.0.2",
"color": "^5.0.3",
@@ -95,7 +96,7 @@
"react": "^18.2.0",
"react-dom": "^18.3.1",
"react-flow-renderer": "9.7.4",
- "react-inlinesvg": "^4.4.1",
+ "react-inlinesvg": "^4.5.0",
"react-is": "^18.3.1",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.1",
@@ -106,73 +107,70 @@
"remark-parse": "^11.0.0",
"reset-css": "^5.0.2",
"unified": "^11.0.5",
+ "util": "^0.12.5",
"wicg-inert": "^3.1.3"
},
"devDependencies": {
- "@babel/core": "^7.29.0",
- "@babel/plugin-transform-class-properties": "^7.28.6",
- "@babel/plugin-transform-private-methods": "^7.28.6",
- "@babel/plugin-transform-private-property-in-object": "^7.28.6",
- "@babel/preset-env": "^7.29.5",
- "@babel/preset-flow": "^7.27.1",
- "@babel/preset-react": "^7.28.5",
- "@babel/preset-typescript": "^7.28.5",
+ "@babel/core": "^7.29.7",
+ "@babel/preset-env": "^7.29.7",
+ "@babel/preset-react": "^7.29.7",
+ "@babel/preset-typescript": "^7.29.7",
"@eslint/compat": "^2.1.0",
- "@eslint/eslintrc": "^3.3.5",
+ "@eslint/eslintrc": "^3.3.6",
"@eslint/js": "^10.0.1",
- "@storybook/addon-a11y": "^10.4.0",
- "@storybook/addon-docs": "^10.4.0",
- "@storybook/addon-links": "^10.4.0",
+ "@storybook/addon-a11y": "^10.5.10",
+ "@storybook/addon-docs": "^10.5.10",
+ "@storybook/addon-links": "^10.5.10",
"@storybook/addon-webpack5-compiler-babel": "^4.0.1",
- "@storybook/cli": "^10.4.0",
- "@storybook/preset-scss": "^1.0.3",
- "@storybook/react": "^10.4.0",
- "@storybook/react-webpack5": "^10.4.0",
+ "@storybook/cli": "^10.5.10",
+ "@storybook/react": "^10.5.10",
+ "@storybook/react-webpack5": "^10.5.10",
"@testing-library/dom": "^10.4.1",
- "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.2",
"@types/he": "^1.2.3",
"@types/jest": "^30.0.0",
"@types/jshint": "^2.12.4",
- "@types/lodash": "^4.17.24",
+ "@types/lodash": "^4.17.25",
"@types/n3": "^1.26.1",
"@types/react-dom": "^18.3.7",
"@types/react-is": "^19.2.0",
"@types/react-syntax-highlighter": "^15.5.13",
- "@typescript-eslint/eslint-plugin": "^8.59.3",
- "@typescript-eslint/parser": "^8.59.3",
+ "@typescript-eslint/eslint-plugin": "^8.68.0",
+ "@typescript-eslint/parser": "^8.68.0",
"babel-jest": "^30.4.1",
- "chromatic": "^16.10.1",
- "eslint": "^10.3.0",
+ "chromatic": "^18.6.1",
+ "css-loader": "^7.1.4",
+ "eslint": "^10.9.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
- "eslint-plugin-simple-import-sort": "^13.0.0",
+ "eslint-plugin-simple-import-sort": "^14.0.0",
"husky": "^4.3.8",
"identity-obj-proxy": "^3.0.0",
"jest": "^30.4.2",
"jest-environment-jsdom": "^30.4.1",
"jest-fixed-jsdom": "^0.0.11",
- "jest-pnp-resolver": "^1.2.3",
"lint-staged": "^15.5.2",
"node-sass-package-importer": "^5.3.3",
"path": "^0.12.7",
- "prettier": "^3.8.3",
+ "prettier": "^3.9.6",
"react-app-polyfill": "^3.0.0",
"react-lorem-ipsum": "^1.4.9",
"regenerator-runtime": "^0.14.1",
"rimraf": "^6.1.3",
- "sass": "^1.99.0",
- "sass-loader": "^16.0.8",
- "storybook": "^10.4.0",
- "stylelint": "^17.11.0",
- "stylelint-config-recess-order": "^7.7.0",
+ "sass": "^1.103.1",
+ "sass-loader": "^17.0.0",
+ "storybook": "^10.5.10",
+ "style-loader": "^4.0.0",
+ "stylelint": "^17.14.1",
+ "stylelint-config-recess-order": "^7.8.0",
"stylelint-config-standard-scss": "^17.0.0",
"stylelint-order": "^8.1.1",
"ts-node": "^10.9.2",
"tsc-esm-fix": "^3.1.2",
"typescript": "^5.9.3",
- "url-loader": "^4.1.1",
- "yargs": "^18.0.0"
+ "webpack": "^5.109.2",
+ "yargs": "^18.1.0"
},
"peerDependencies": {
"@blueprintjs/core": ">=6",
@@ -180,21 +178,11 @@
},
"resolutions": {
"**/@blueprintjs/core": "6.8.1",
- "node-sass-package-importer/**/postcss": "^8.5.18",
- "stylelint-order/**/postcss": "^8.5.12",
- "stylelint/**/postcss": "^8.5.18",
"hast-util-from-parse5": "8.0.0",
"**/lodash": "^4.18.1",
"**/minimatch": "^3.1.4",
- "**/serialize-javascript": "^7.0.5",
- "**/ws": "^8.21.0",
- "babel-jest/**/js-yaml": "^3.15.1",
- "stylelint/**/js-yaml": "^4.3.1",
- "@eslint/eslintrc/**/js-yaml": "^4.3.1",
- "**/nanoid": "^3.3.18",
- "**/fast-uri": "^3.1.3",
- "sass/**/immutable": "^5.1.8",
- "**/brace-expansion": "^1.1.18"
+ "**/postcss": "^8.5.18",
+ "**/serialize-javascript": "^7.0.5"
},
"husky": {
"hooks": {
@@ -225,7 +213,6 @@
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.d.ts"
],
- "resolver": "jest-pnp-resolver",
"setupFiles": [
"react-app-polyfill/jsdom"
],
diff --git a/scripts/compile-sass.ts b/scripts/compile-sass.ts
index f3288bf6..355fea1f 100644
--- a/scripts/compile-sass.ts
+++ b/scripts/compile-sass.ts
@@ -1,20 +1,63 @@
+import * as fs from "fs";
import tildeImporter from "node-sass-package-importer";
+import * as path from "path";
import * as sass from "sass";
import yargs from "yargs";
import sassRenderSyncConfig from "./sassConfig";
-import { silenceDeprecations } from "./sassDeprecationConfig"
+import { silenceDeprecations } from "./sassDeprecationConfig";
const args = yargs(process.argv.slice(2)).argv as any;
+/**
+ * Returns the directory a package was installed into, or `undefined` if it cannot be found.
+ * The `node_modules` chain of the resolver is walked directly instead of resolving the
+ * `package.json` of the package, because a package is free to hide that file behind its
+ * `exports` map, which would let `require.resolve()` fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`.
+ * Symbolic links are resolved, so that the real location is used, e.g. the store directory of pnpm.
+ */
+const packageDirectory = (packageName: string) => {
+ for (const searchPath of require.resolve.paths(packageName) ?? []) {
+ const candidate = path.join(searchPath, ...packageName.split("/"));
+ if (fs.existsSync(path.join(candidate, "package.json"))) return fs.realpathSync(candidate);
+ }
+ return undefined;
+};
+
+/**
+ * Returns the dependency root a package was installed into, i.e. the `node_modules` directory
+ * holding it. The number of levels to climb is taken from the package name, this way scoped
+ * (`@scope/name`) and plain (`name`) packages are both handled correctly.
+ */
+const dependencyRoot = (packageName: string) => {
+ const directory = packageDirectory(packageName);
+ return directory === undefined ? undefined : path.resolve(directory, ...packageName.split("/").map(() => ".."));
+};
+
+// Imports without tilde syntax are not handled by the importer below, sass resolves them relative
+// to the importing file and via these include paths. `@blueprintjs/core` and `@carbon/react` both
+// use them internally to reach their sibling packages, e.g. `@blueprintjs/colors` or
+// `@carbon/styles`. Their dependency roots are looked up separately because the packages do not
+// need to share one, e.g. if only one of them is hoisted into a parent workspace. The local
+// `node_modules` is kept as a fallback, it is also used alone if a dependency root cannot be
+// determined. All paths are absolute, so that duplicates of the same directory can be removed.
+const includePaths = [
+ ...new Set(
+ [dependencyRoot("@blueprintjs/core"), dependencyRoot("@carbon/react"), path.resolve("node_modules")].filter(
+ (includePath): includePath is string => includePath !== undefined,
+ ),
+ ),
+];
+
const styles = sass.renderSync({
importer: tildeImporter(),
...sassRenderSyncConfig,
- silenceDeprecations: silenceDeprecations as sass.DeprecationOrId[],
+ silenceDeprecations: [...silenceDeprecations, "legacy-js-api"] as sass.DeprecationOrId[],
file: "src/index.scss",
- includePaths: ["node_modules"], // Carbon does not use tilde import syntax
+ includePaths,
});
if (args.outputCss) {
+ // eslint-disable-next-line no-console
console.log(styles.css.toString());
}
diff --git a/scripts/fix-esm-dist b/scripts/fix-esm-dist
index 61006373..ef7c3043 100755
--- a/scripts/fix-esm-dist
+++ b/scripts/fix-esm-dist
@@ -1,4 +1,9 @@
+#!/bin/sh
+# abort as soon as one of the steps fails, otherwise a broken ESM output would be
+# reported as a successful build because the exit code of the last command wins
+set -e
yarn tsc-esm-fix --target='dist/esm'
+node ./scripts/fix-esm-imports.mjs dist/esm
cat >dist/esm/package.json <
+ !specifier.startsWith(".") && !specifier.startsWith("/") && !/^\w[\w+.-]*:/.test(specifier);
+
+const collectFiles = (directory) =>
+ fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
+ const entryPath = path.join(directory, entry.name);
+ if (entry.isDirectory()) return collectFiles(entryPath);
+ return entry.name.endsWith(".js") ? [entryPath] : [];
+ });
+
+const resolvesToFile = (specifier) => {
+ let resolved;
+ try {
+ resolved = import.meta.resolve(specifier);
+ } catch (error) {
+ // only a failed resolution is an expected answer, everything else (a broken resolver, a
+ // permission problem, …) has to abort the build instead of silently repairing nothing
+ if (unresolvableCodes.has(error?.code)) return false;
+ throw error;
+ }
+ return fs.existsSync(fileURLToPath(resolved));
+};
+
+const replacements = new Map();
+
+/** @returns the specifier that should be used instead, or `undefined` to keep it as it is */
+const repairedSpecifier = (specifier) => {
+ if (!replacements.has(specifier)) {
+ const withoutExtension = specifier.replace(/\.js$/, "");
+ const repairable =
+ isBareSpecifier(specifier) &&
+ specifier !== withoutExtension &&
+ !resolvesToFile(specifier) &&
+ resolvesToFile(withoutExtension);
+ replacements.set(specifier, repairable ? withoutExtension : undefined);
+ }
+ return replacements.get(specifier);
+};
+
+/** @returns the module references of the given source, the last one in the file first */
+const moduleReferences = (contents) =>
+ [...ts.preProcessFile(contents, true, true).importedFiles].sort((one, other) => other.pos - one.pos);
+
+let repaired = 0;
+
+for (const file of collectFiles(target)) {
+ const contents = fs.readFileSync(file, "utf8");
+ let fixedContents = contents;
+ // the references are replaced from the end of the file to its beginning, this way the
+ // positions of all not yet handled ones stay valid
+ for (const reference of moduleReferences(contents)) {
+ const specifier = reference.fileName;
+ const replacement = repairedSpecifier(specifier);
+ if (replacement === undefined) continue;
+ // `pos` addresses the quote opening the reference, its text starts behind it
+ const start = reference.pos + 1;
+ if (fixedContents.slice(start, start + specifier.length) !== specifier) {
+ throw new Error(
+ `fix-esm-imports: cannot repair "${specifier}" in ${file}, the source at position ${start} does not match the parsed reference`,
+ );
+ }
+ fixedContents = fixedContents.slice(0, start) + replacement + fixedContents.slice(start + specifier.length);
+ }
+ if (fixedContents !== contents) {
+ fs.writeFileSync(file, fixedContents);
+ repaired += 1;
+ }
+}
+
+for (const [specifier, replacement] of replacements) {
+ if (replacement !== undefined) {
+ console.log(`fix-esm-imports: "${specifier}" -> "${replacement}"`);
+ }
+}
+console.log(`fix-esm-imports: repaired imports in ${repaired} file(s) of ${target}`);
diff --git a/scripts/sassDeprecationConfig.js b/scripts/sassDeprecationConfig.js
index 888046ce..a286bfc3 100644
--- a/scripts/sassDeprecationConfig.js
+++ b/scripts/sassDeprecationConfig.js
@@ -1,3 +1,3 @@
module.exports = {
- silenceDeprecations: ["import", "legacy-js-api"],
-}
+ silenceDeprecations: ["import"],
+};
diff --git a/src/_bom-workaround.scss b/src/_bom-workaround.scss
new file mode 100644
index 00000000..bb47a3c1
--- /dev/null
+++ b/src/_bom-workaround.scss
@@ -0,0 +1,28 @@
+.bom-problem-workaround-dummy-rule {
+ /**
+ * This is a workaround to prevent the loss of the first rule of our compressed CSS.
+ *
+ * Sass emits a BOM instead of a `@charset` rule when it creates a compressed
+ * stylesheet that contains non-ASCII characters.
+ * Postcss preserves that BOM since v8.5.24, before that it silently dropped it.
+ * Css-loader does not remove it, although it inlines the CSS into a JS string that
+ * style-loader writes into a `