Skip to content

Commit

Permalink
victory-imports
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfauquette committed Jun 24, 2024
1 parent c46b401 commit 70262c6
Show file tree
Hide file tree
Showing 9 changed files with 442 additions and 12 deletions.
63 changes: 63 additions & 0 deletions packages/x-charts-vendor/.babelrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Transform d3 ESM libraries to vendored CommonJS libraries
*
* This produces `lib-vendor/d3-<package name>/src` files that have
* internally consistent references to other d3 packages. It is only meant
* to be used for the CommonJS import path.
*/
const path = require("path");

module.exports = {
only: ["node_modules/*/src/**/*.js"],
plugins: [
[
"@babel/transform-modules-commonjs",
{
strict: false,
allowTopLevelThis: true,
},
],
[
"module-resolver",
{
// Convert all imports for _other_ d3 dependencies to the relative
// path in our vendor package.
resolvePath(sourcePath, currentFile) {
const d3pattern = /^(?<pkg>(d3-[^\/]+|internmap))(?<path>.*)/;
const match = d3pattern.exec(sourcePath);
if (match) {
// We're assuming a common shape of d3 packages:
// - Only top level imports "d3-<whatever>"
// - With no path components (like "d3-<whatever>/path/to.js")
if (match.groups.path) {
throw new Error(
`Unable to process ${sourcePath} import in ${currentFile}`,
);
}

// Get Vendor package path.
const vendorPkg = `lib-vendor/${match.groups.pkg}/src/index.js`;

// Derive relative path to vendor lib to have a file like move from:
// - 'node_modules/d3-interpolate/src/rgb.js'
// - 'lib-vendor/d3-interpolate/src/rgb.js'
// and have an import transform like:
// - `d3-color`
// - `../../d3-color`
const currentFileVendor = currentFile.replace(
/^node_modules/,
"lib-vendor",
);
const relPathToPkg = path
.relative(path.dirname(currentFileVendor), vendorPkg)
.replace(/\\/g, "/");

return relPathToPkg;
}

return sourcePath;
},
},
],
],
};
3 changes: 3 additions & 0 deletions packages/x-charts-vendor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/lib-vendor
/d3-*
/internmap.js
70 changes: 70 additions & 0 deletions packages/x-charts-vendor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# VictoryVendor

Vendored dependencies for Victory.

## Background

D3 has released most of its libraries as ESM-only. This means that consumers in Node.js applications can no longer just `require()` anything with a d3 transitive dependency, including much of Victory.

To help provide an easy path to folks still using CommonJS in their Node.js applications that consume Victory, we now provide this package to vendor in various d3-related packages.

## Packages

We presently provide the following top-level libraries:
<!-- cat packages/victory-vendor/package.json | egrep '"d3-' | egrep -o 'd3-[^"]*'| sor t-->

- d3-ease
- d3-interpolate
- d3-scale
- d3-shape
- d3-timer

This is the total list of top and transitive libraries we vendor:
<!-- ls packages/victory-vendor/lib-vendor | sort -->

- d3-array
- d3-color
- d3-ease
- d3-format
- d3-interpolate
- d3-path
- d3-scale
- d3-shape
- d3-time
- d3-time-format
- d3-timer
- internmap

Note that this does _not_ include the following D3 libraries that still support CommonJS:

- d3-voronoi

## How it works

We provide two alternate paths and behaviors -- for ESM and CommonJS

### ESM

If you do a Node.js import like:

```js
import { interpolate } from "victory-vendor/d3-interpolate";
```

under the hood it's going to just re-export and pass you through to `node_modules/d3-interpolate`, the **real** ESM library from D3.

### CommonJS

If you do a Node.js import like:

```js
const { interpolate } = require("victory-vendor/d3-interpolate");
```

under the hood it's going to will go to an alternate path that contains the transpiled version of the underlying d3 library to be found at `victory-vendor/lib-vendor/d3-interpolate/**/*.js`. This futher has internally consistent import references to other `victory-vendor/lib-vendor/<pkg-name>` paths.

Note that for some tooling (like Jest) that doesn't play well with `package.json:exports` routing to this CommonJS path, we **also** output a root file in the form of `victory-vendor/d3-interpolate.js`.

## Licenses

This project is released under the MIT license, but the vendor'ed in libraries include other licenses (e.g. ISC) that we enumerate in our `package.json:license` field.
53 changes: 53 additions & 0 deletions packages/x-charts-vendor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "mui/x-charts-vendor",
"version": "37.0.2",
"description": "Vendored dependencies for Victory",
"keywords": [
"data visualization",
"React",
"d3",
"charting"
],
"repository": {
"type": "git",
"url": "https://github.com/mui/mui-x"
},
"author": "Formidable",
"license": "MIT AND ISC",
"exports": {
"./package.json": "./package.json",
"./d3-*": {
"types": "./d3-*.d.ts",
"import": "./es/d3-*.js",
"default": "./lib/d3-*.js"
}
},
"dependencies": {
"d3-color": "^3.1.0",
"d3-delaunay": "^6.0.4",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.2.0",
"@types/d3-color": "^3.1.3",
"@types/d3-delaunay": "^6.0.4",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-scale": "^4.0.8",
"@types/d3-shape": "^3.1.6"
},
"devDependencies": {
"d3-color": "^3.1.0",
"d3-format": "^3.1.0",
"d3-path": "^3.0.1",
"d3-time-format": "^4.1.0",
"d3-voronoi": "^1.1.4",
"internmap": "^2.0.3",
"execa": "^6.1.0",
"rimraf": "^3.0.2"
},
"publishConfig": {
"provenance": true
},
"scripts": {
"build": "node ./scripts/build.js"
}
}
169 changes: 169 additions & 0 deletions packages/x-charts-vendor/scripts/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/* global __dirname:false */
/**
* Build d3 vendor libraries from `node_modules`.
*
* **Note - transitive dependencies**: Because pnpm lacks a `nohoist` option,
* if you have a `d3-*` dependency that has a transitive dependency on another
* module (e.g., `d3-interpolate` depends on `d3-color`) you need to add a
* compatible version to `package.json:devDependencies` here to make sure we
* get the library in our `node_modules` and appropriately build it.
*/
const fs = require("fs").promises;
const path = require("path");
const { promisify } = require("util");

const rimraf = require("rimraf");
const rimrafP = promisify(rimraf);

const vendorPkg = require("../package.json");
const VENDOR_PKGS = new Set(Object.keys(vendorPkg.dependencies));

const { log, error } = console; // eslint-disable-line no-undef

// Templates.
const getEsmIndex = (pkg) => `
// \`x-charts-vendor/${pkg.name}\` (ESM)
// See upstream license: ${pkg.repository.url.replace(
/\.git$/,
"",
)}/blob/main/LICENSE
//
// Our ESM package uses the underlying installed dependencies of \`node_modules/${
pkg.name
}\`
export * from "${pkg.name}";
`;

const getCjsIndex = (pkg) => `
// \`x-charts-vendor/${pkg.name}\` (CommonJS)
// See upstream license: ${pkg.repository.url.replace(
/\.git$/,
"",
)}/blob/main/LICENSE
//
// Our CommonJS package relies on transpiled vendor files in \`lib-vendor/${
pkg.name
}\`
module.exports = require("../lib-vendor/${pkg.name}/src/index.js");
`;

const getCjsRootIndex = (pkg) => `
// \`x-charts-vendor/${pkg.name}\` (CommonJS)
// See upstream license: ${pkg.repository.url.replace(
/\.git$/,
"",
)}/blob/main/LICENSE
//
// This file only exists for tooling that doesn't work yet with package.json:exports
// by proxying through the CommonJS version.
module.exports = require("./lib/${pkg.name}");
`;

const getTypeDefinitionFile = (pkg) => `
// \`x-charts-vendor/${pkg.name}\` (TypeScript)
//
// Export the type definitions for this package:
export * from "${pkg.name}";
`;

// Main.
const main = async () => {
// Lazy ESM imports.
const { execa } = await import("execa");

// Get d3-related packages we want to vendor.
const pkgs = (
await fs.readdir(path.resolve(__dirname, "../node_modules/"))
).filter((name) => /^(d3-|internmap)/.test(name));

// Safety check: we assume that **all** are flattened to root level of this
// package, and want to make sure there are no nested dependencies.
for (const pkgName of pkgs) {
const pkgModsPath = path.resolve(
__dirname,
`../node_modules/git${pkgName}/node_modules`,
);
const stat = await fs.lstat(pkgModsPath).catch(() => null);
if (stat) {
throw new Error(`Found nested modules: ${pkgModsPath}`);
}
}

// Clean out and ensure base library paths exist
const EsmBasePath = path.resolve(__dirname, `../es`);
const CjsBasePath = path.resolve(__dirname, `../lib`);
const VendorBasePath = path.resolve(__dirname, `../lib-vendor`);
const baseDirs = [EsmBasePath, CjsBasePath, VendorBasePath];
const cleanGlobs = [].concat(baseDirs, path.resolve(__dirname, "../d3-*"));

log("Cleaning old vendor directories.");
await Promise.all(cleanGlobs.map((glob) => rimrafP(glob)));
log("Creating empty vendor directories.");
await Promise.all(
baseDirs.map((libPath) => fs.mkdir(libPath, { recursive: true })),
);

// Transpile.
log("Transpiling vendor sources.");
await execa(
"pnpm",
[
"babel",
"--config-file",
path.resolve(__dirname, "../.babelrc.js"),
"-d",
path.resolve(__dirname, "../lib-vendor"),
path.resolve(__dirname, "../node_modules"),
],
{
stdio: "inherit",
},
);

// Iterate and generate index files.
log("Copying licenses and generating indexes.");
for (const pkgName of pkgs) {
log(`- ${pkgName}`);

const pkgBase = path.resolve(__dirname, `../node_modules/${pkgName}`);
const pkgPath = path.join(pkgBase, `package.json`);
const pkg = await fs
.readFile(pkgPath)
.then((buf) => JSON.parse(buf.toString()));
const libVendorPath = path.resolve(__dirname, `../lib-vendor/${pkgName}`);

// Create library indexes and copy licenses to `lib-vendor.
await Promise.all([
fs.writeFile(path.join(EsmBasePath, `${pkgName}.js`), getEsmIndex(pkg)),
fs.writeFile(path.join(CjsBasePath, `${pkgName}.js`), getCjsIndex(pkg)),
fs.copyFile(
path.join(pkgBase, "LICENSE"),
path.join(libVendorPath, "LICENSE"),
),
// Root hack file for non package.json:exports systems
VENDOR_PKGS.has(pkgName) &&
fs.writeFile(
path.resolve(__dirname, `../${pkgName}.js`),
getCjsRootIndex(pkg),
),
// Generate TypeScript definitions
VENDOR_PKGS.has(pkgName) &&
fs.writeFile(
path.resolve(__dirname, `../${pkgName}.d.ts`),
getTypeDefinitionFile(pkg),
),
]);
}
};

if (require.main === module) {
main()
// eslint-disable-next-line promise/always-return
.then(() => {
log("Build finished.");
})
.catch((err) => {
error(err);
process.exit(-1);
});
}
17 changes: 17 additions & 0 deletions packages/x-charts-vendor/tests/d3-interpolate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* This test verifies that these modules and types are exported correctly
*/

/* eslint-disable @typescript-eslint/no-unused-vars */
import {
// @ts-expect-error Make sure invalid imports fail:
INVALID_TYPE,
interpolate,
NumberArray,
} from "@mui/x-charts/d3-interpolate";

describe("d3-interpolate", () => {
it("exports valid functions", () => {
expect(interpolate).toBeInstanceOf(Function);
});
});
Loading

0 comments on commit 70262c6

Please sign in to comment.