Skip to content

Commit

Permalink
Fix language files for Node+ESM (#1377)
Browse files Browse the repository at this point in the history
* Add babel script add-import-extension

* Adjust check-publish-package script to new way of building ES

* Make babel output ES build with mjs extensions

* Add exports property to package.json

* Fix the exports property

* Tweaking package.json exports for cjs modules

* Adjust package.json exports for individual language files in CJS

* Add info about the source of the add-import-extension babel script

* Add changelog entry

* Move types key to be the first key in JSON object (exports property)

* Fix publint suggestions

* Add paths ./i18n/languages/xxXX to exports property in package.json

* Use path ./i18n/languages/xxXX to import languages in the docs

* Add paths with .js extensions to exports in package.json

* Add migration guide for 3.0

* Working on the migration guide

* Unpolished vershion of the migration guide

* Upgrade migration instructions for Angular projects

* Add more explenations to the migration guide

* Add a fallback option for parcel projects to the migration guide

* Undo the version number change in package.json

* Update migration guide

* Remove unnsecessary step from the parcel section of the migration guide

* Rephrase CHANGELOG.md
  • Loading branch information
sequba authored Nov 12, 2024
1 parent 5def4ef commit c27a9d5
Show file tree
Hide file tree
Showing 9 changed files with 634 additions and 14 deletions.
90 changes: 90 additions & 0 deletions .config/babel/add-import-extension.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Based on https://github.com/handsontable/handsontable/blob/bd7628544ff83d6e74a9cc949e2c3c38fef74d76/handsontable/.config/plugin/babel/add-import-extension.js

const { existsSync, lstatSync } = require('fs');
const { dirname, resolve } = require('path');
const { types } = require('@babel/core');
const { declare } = require('@babel/helper-plugin-utils');

const VALID_EXTENSIONS = ['js', 'mjs'];

const hasExtension = moduleName => VALID_EXTENSIONS.some(ext => moduleName.endsWith(`.${ext}`));
const isCoreJSPolyfill = moduleName => moduleName.startsWith('core-js');
const isLocalModule = moduleName => moduleName.startsWith('.');
const isProcessableModule = (moduleName) => {
return !hasExtension(moduleName) && (isCoreJSPolyfill(moduleName) || isLocalModule(moduleName));
};

const createVisitor = ({ declaration, origArgs, extension = 'js' }) => {
return (path, { file }) => {
const { node: { source, exportKind, importKind } } = path;
const { opts: { filename } } = file;
const isTypeOnly = exportKind === 'type' || importKind === 'type';

if (!source || isTypeOnly || !isProcessableModule(source.value)) {
return;
}

const { value: moduleName } = source;
const absoluteFilePath = resolve(dirname(filename), moduleName);
const finalExtension = isCoreJSPolyfill(moduleName) ? 'js' : extension;

let newModulePath;

// Resolves a case where "import" points to a module name which exists as a file and
// as a directory. For example in this case:
// ```
// import { registerPlugin } from 'plugins';
// ```
// and with this directory structure:
// |- editors
// |- plugins
// |- filters/
// |- ...
// +- index.js
// |- plugins.js
// |- ...
// +- index.js
//
// the plugin will rename import declaration to point to the `plugins.js` file.
if (existsSync(`${absoluteFilePath}.js`)) {
newModulePath = `${moduleName}.${finalExtension}`;

// In a case when the file doesn't exist and the module is a directory it will
// rename to `plugins/index.js`.
} else if (existsSync(absoluteFilePath) && lstatSync(absoluteFilePath).isDirectory()) {
newModulePath = `${moduleName}/index.${finalExtension}`;

// And for other cases it simply put the extension on the end of the module path
} else {
newModulePath = `${moduleName}.${finalExtension}`;
}

path.replaceWith(declaration(...origArgs(path), types.stringLiteral(newModulePath)));
};
};

module.exports = declare((api, options) => {
api.assertVersion(7);

return {
name: 'add-import-extension',
visitor: {
// It covers default and named imports
ImportDeclaration: createVisitor({
extension: options.extension,
declaration: types.importDeclaration,
origArgs: ({ node: { specifiers } }) => [specifiers],
}),
ExportNamedDeclaration: createVisitor({
extension: options.extension,
declaration: types.exportNamedDeclaration,
origArgs: ({ node: { declaration, specifiers } }) => [declaration, specifiers],
}),
ExportAllDeclaration: createVisitor({
extension: options.extension,
declaration: types.exportAllDeclaration,
origArgs: () => [],
}),
}
};
});
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Changed

- **Breaking change**: Change ES module build to use `mjs` files and `exports` property in `package.json` to make importing language files possible in Node environment. [#1344](https://github.com/handsontable/hyperformula/issues/1344)
- **Breaking change**: Removed the `binarySearchThreshold` configuration option. [#1439](https://github.com/handsontable/hyperformula/issues/1439)

## [2.7.1] - 2024-07-18
Expand Down
4 changes: 3 additions & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ module.exports = {
},
// Environment for transpiling files to be compatible with ES Modules.
es: {
plugins: [],
plugins: [
['./.config/babel/add-import-extension.js', { extension: 'mjs' }],
],
},
},
};
3 changes: 2 additions & 1 deletion docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,8 @@ module.exports = {
children: [
['/guide/release-notes', 'Release notes'],
['/guide/migration-from-0.6-to-1.0', 'Migrating from 0.6 to 1.0'],
['/guide/migration-from-1.0-to-2.0', 'Migrating from 1.x to 2.0'],
['/guide/migration-from-1.x-to-2.0', 'Migrating from 1.x to 2.0'],
['/guide/migration-from-2.x-to-3.0', 'Migrating from 2.x to 3.0'],
]
},
{
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/localizing-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ register the language like so:

```javascript
// import the French language pack
import frFR from 'hyperformula/es/i18n/languages/frFR';
import frFR from 'hyperformula/i18n/languages/frFR';

// register the language
HyperFormula.registerLanguage('frFR', frFR);
```

::: tip
To import the language packs, use the module-system-specific dedicated bundles at:
* **ES**: `hyperformula/es/i18n/languages/`
* **CommonJS**: `hyperformula/commonjs/i18n/languages/`
* **ES**: `hyperformula/i18n/languages/`
* **CommonJS**: `hyperformula/i18n/languages/`
* **UMD**: `hyperformula/dist/languages/`

For the UMD build, the languages are accessible through `HyperFormula.languages`, e.g., `HyperFormula.languages.frFR`.
Expand Down
File renamed without changes.
78 changes: 78 additions & 0 deletions docs/guide/migration-from-2.x-to-3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,84 @@

To upgrade your HyperFormula version from 2.x.x to 3.0.0, follow this guide.

## Importing language files

We changed the way of importing language files in ES module system to a more modern way using `mjs` files and `exports` property. This change is required to make HyperFormula compatible with newer ESM configurations in Node and browser environments.

The previous import paths became deprecated. For most environments they still work in version 3.0.0, but it will be removed in the future. To avoid any issues, update your code to use the new paths.

### New import paths for ES and CommonJS module systems

For ES and CommonJS modules, use the path `hyperformula/i18n/languages`, to import the language files. E.g.:

```javascript
import { frFR } from "hyperformula/i18n/languages"; // ESM

const { frFR } = require('hyperformula/i18n/languages'); // CommonJS
```
If you use the UMD module system, you don't need to change anything.

### Additional steps for projects using Angular

1. Make sure you use Typescript 5 or newer
2. In your `tsconfig.json`, set:

```
"moduleResolution": "bundler",
```

### Additional steps for projects using Typescript

In your `tsconfig.json`, set:

```
"module": "node16",
"moduleResolution": "node16",
```

### Additional steps for projects using Webpack 4 or older

1. In your code, use the legacy paths for importing language files. Unfortunately, Webpack 4 does not support `exports` property. E.g.:

```javascript
import { frFR } from "hyperformula/es/i18n/languages";
```

2. In your `webpack.config.js`, add the following configuration to handle `.mjs` files properly:

```javascript
module: {
rules: [
{
test: /\.m?js$/,
include: /node_modules/,
type: "javascript/auto",
},
],
}
```

### Additional steps for projects using Parcel

1. Make sure you use Parcel 2.9 or newer. Older versions of Parcel do not support `exports` property.
2. In your `package.json`, add the [following configuration](https://parceljs.org/blog/v2-9-0/#new-resolver):

```
"@parcel/resolver-default": {
"packageExports": true
}
```

If you don't want to upgrade Parcel, you can keep using the legacy import paths for language files, but they will be removed in one of the upcoming releases. E.g.:

```javascript
import { frFR } from "hyperformula/es/i18n/languages";
```

### Other projects

We tested the changes with the most popular bundlers and frameworks. If you use a different configuration, and you encounter any issues, please contact us via GitHub. We will try to make it work for you, although for older versions of bundlers and frameworks, it might be impossible.

## Removal of the `binarySearchThreshold` configuration option (deprecated since version 1.1.0)

The `binarySearchThreshold` has no effect since version 1.1.0. If your codebase still uses this option, please remove it.
Loading

0 comments on commit c27a9d5

Please sign in to comment.