├── .deepsource.toml ├── .gitattributes ├── .github └── workflows │ └── pipeline.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .mergify.yml ├── .prettierrc ├── LICENSE ├── README.md ├── commitlint.config.ts ├── eslint.config.mjs ├── jest.config.ts ├── logo.svg ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── config.ts ├── files.ts ├── import.ts ├── index.ts └── options.ts ├── tests ├── basic │ ├── basic.mjs │ └── basic.test.ts ├── edge │ ├── broken.js │ ├── edge.test.ts │ ├── empty.js │ ├── extension.cjs │ ├── extension.js │ ├── extension.mjs │ └── extension.ts ├── esmlibs │ └── esmlibs.test.ts ├── path │ └── path.test.ts └── resolver │ ├── nodeFetchImport.js │ └── resolver.test.ts └── tsconfig.json /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "javascript" 5 | 6 | [analyzers.meta] 7 | environment = [ 8 | "nodejs", 9 | "jest" 10 | ] -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/**/* linguist-vendored -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**/README.md' 9 | pull_request: 10 | branches: 11 | - "*" 12 | paths-ignore: 13 | - '**/README.md' 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | node-version: [22.x, 20.x, 18.x] 21 | fail-fast: true 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v2 26 | 27 | - name: Set up Node.js 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | 32 | - name: Install pnpm 33 | run: npm install -g pnpm 34 | 35 | - name: Install dependencies 36 | run: pnpm install 37 | 38 | - name: Run ESLint 39 | run: pnpm lint 40 | 41 | - name: Run tests with coverage 42 | run: pnpm test --coverage 43 | 44 | - name: Upload coverage reports to Codecov 45 | uses: codecov/codecov-action@v3 46 | env: 47 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/**/* 3 | coverage 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit ${1} 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Automatic merge for depfu pull requests 3 | conditions: 4 | - author=depfu[bot] 5 | - base=main 6 | actions: 7 | merge: 8 | method: merge 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "bracketSameLine": false 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Khiet Tam Nguyen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the “Software”), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # [![Import Sync](logo.svg)](https://github.com/nktnet1/import-sync) 4 | 5 | [![pipeline](https://github.com/nktnet1/import-sync/actions/workflows/pipeline.yml/badge.svg)](https://github.com/nktnet1/import-sync/actions/workflows/pipeline.yml) 6 |   7 | [![codecov](https://codecov.io/gh/nktnet1/import-sync/branch/main/graph/badge.svg?token=RAC7SKJTGU)](https://codecov.io/gh/nktnet1/import-sync) 8 |   9 | [![Maintainability](https://api.codeclimate.com/v1/badges/aaae5cf33d58299ed722/maintainability)](https://codeclimate.com/github/nktnet1/import-sync/maintainability) 10 |   11 | [![Snyk Security](https://snyk.io/test/github/nktnet1/import-sync/badge.svg)](https://snyk.io/test/github/nktnet1/import-sync) 12 |   13 | [![GitHub top language](https://img.shields.io/github/languages/top/nktnet1/import-sync)](https://github.com/search?q=repo%3Anktnet1%2Fimport-sync++language%3ATypeScript&type=code) 14 | 15 | [![NPM Version](https://img.shields.io/npm/v/import-sync?logo=npm)](https://www.npmjs.com/package/import-sync?activeTab=versions) 16 |   17 | [![install size](https://packagephobia.com/badge?p=import-sync)](https://packagephobia.com/result?p=import-sync) 18 |   19 | [![Depfu Dependencies](https://badges.depfu.com/badges/6c4074c4d23ad57ee2bfd9ff90456090/overview.svg)](https://depfu.com/github/nktnet1/import-sync?project_id=39032) 20 |   21 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fnktnet1%2Fimport-sync.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fnktnet1%2Fimport-sync?ref=badge_shield) 22 |   23 | [![NPM License](https://img.shields.io/npm/l/import-sync)](https://opensource.org/license/mit/) 24 |   25 | [![GitHub issues](https://img.shields.io/github/issues/nktnet1/import-sync.svg?style=social)](https://github.com/nktnet1/import-sync/issues) 26 | 27 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=nktnet1_import-sync&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=nktnet1_import-sync) 28 |   29 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/65161ae4d1c646ed83c9ef47b0a11473)](https://app.codacy.com/gh/nktnet1/import-sync/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 30 |   31 | [![DeepSource](https://app.deepsource.com/gh/nktnet1/import-sync.svg/?label=active+issues&show_trend=true&token=r1frerF1-N2Mhrc7ZXIC1uNa)](https://app.deepsource.com/gh/nktnet1/import-sync/) 32 |   33 | [![codebeat badge](https://codebeat.co/badges/acc44573-9938-4a14-bc41-7eb6a58dffbb)](https://codebeat.co/projects/github-com-nktnet1-import-sync-main) 34 |   35 | [![GitHub stars](https://img.shields.io/github/stars/nktnet1/import-sync.svg?style=social)](https://github.com/nktnet1/import-sync/stargazers) 36 | 37 | [![Downloads Total](https://badgen.net/npm/dt/import-sync)](https://moiva.io/?npm=import-sync) 38 |   39 | [![Downloads Yearly](https://badgen.net/npm/dy/import-sync)](https://moiva.io/?npm=import-sync) 40 |   41 | [![Downloads Monthly](https://badgen.net/npm/dm/import-sync)](https://moiva.io/?npm=import-sync) 42 |   43 | [![Downloads Weekly](https://badgen.net/npm/dw/import-sync)](https://moiva.io/?npm=import-sync) 44 |   45 | [![Downloads Daily](https://badgen.net/npm/dd/import-sync)](https://moiva.io/?npm=import-sync) 46 | 47 | --- 48 | 49 | Synchronously import dynamic ECMAScript Modules similar to CommonJS [require](https://nodejs.org/api/modules.html#requireid) 50 | 51 | Basic wrapper around [esm](https://github.com/standard-things/esm) for compatibility with both ESM and CJS projects in NodeJS 52 | 53 | Capable of importing ESM-only libraries such as [node-fetch@3](https://github.com/node-fetch/node-fetch#commonjs) in CJS projects 54 | 55 | [![Try with Replit](https://replit.com/badge?caption=Try%20with%20Replit)](https://replit.com/@nktnet1/import-sync-example#index.js) 56 | 57 |
58 | 59 | --- 60 | 61 | - [1. Installation](#1-installation) 62 | - [2. Usage](#2-usage) 63 | - [2.1. id](#21-id) 64 | - [2.2. options](#22-options) 65 | - [2.3. return](#23-return) 66 | - [3. License](#3-license) 67 | - [4. Limitations](#4-limitations) 68 | - [5. Caveats](#5-caveats) 69 | - [5.1. Idea](#51-idea) 70 | - [5.2. Discovery](#52-approach) 71 | - [5.3. Result](#53-result) 72 | - [6. Major Changes](#6-major-changes) 73 | - [6.1. 2023-12-16](#61-2023-12-16) 74 | 75 | ## 1. Installation 76 | 77 | ``` 78 | npm install import-sync 79 | ``` 80 | 81 | ## 2. Usage 82 | 83 | Try with [Replit](https://replit.com/@nktnet1/import-sync-example#index.js). 84 | 85 | ``` 86 | importSync(id, options); 87 | ``` 88 | 89 |
90 | Examples (click to view) 91 | 92 |
93 | 94 | Importing from the same directory 95 | 96 | ```javascript 97 | const { someVariable, someFunction } = importSync('./some-module'); 98 | ``` 99 | 100 | Importing `.mjs` file from a different directory 101 | 102 | ```javascript 103 | const { someFunction } = importSync('../src/someModule.mjs'); 104 | ``` 105 | 106 | Using a different basePath 107 | 108 | ```javascript 109 | const { someFunction } = importSync( 110 | './someModule', 111 | { basePath: process.cwd() } 112 | ); 113 | ``` 114 | 115 | Using additional esm options as described in esm's [documentation](https://github.com/standard-things/esm#options) 116 | 117 | ```javascript 118 | const { someFunction } = importSync( 119 | './someModule', 120 | { 121 | esmOptions: { 122 | cjs: { 123 | cache: true 124 | }, 125 | mode: 'all', 126 | force: true, 127 | } 128 | } 129 | ); 130 | ``` 131 | 132 | Importing an ESM-only module 133 | 134 | ```javascript 135 | const fetch = importSync('node-fetch'), 136 | ``` 137 | 138 |
139 | 140 |
141 | 142 | ### 2.1. id 143 | 144 | Module name or relative path similar to CommonJS [require](https://nodejs.org/api/modules.html#requireid). For example, 145 | - `'../animals/cats.js'` 146 | - `'./dogs.mjs'` 147 | - `'./minimal'` 148 | - `importSync` will look for matching extensions in the order `[.js, .mjs, .cjs, .ts]` 149 | - `'node-fetch'` 150 | - `importSync` can import pure-esm [node-fetch](https://github.com/node-fetch/node-fetch) (v3) into your cjs project 151 | 152 | ### 2.2. options 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 180 | 185 | 186 | 187 | 188 | 189 | 190 | 198 | 199 | 200 | 201 |
OptionDescriptionExampleDefault
basePath 165 | This will only take effect if the given id starts with ./ or ../. 166 |
167 | For example, 168 |
    169 |
  • ./localLib
  • 170 |
  • ../src/localLib
  • 171 |
172 | and not 173 |
    174 |
  • /home/user/localLib
  • 175 |
  • localLib.mjs
  • 176 |
  • node-fetch
  • 177 |
178 | The ❌ examples above will be interpreted as either absolute paths or library imports. 179 |
181 |
182 | ./myModule
183 | 
184 |
__dirname
esmOptionsOptions for the esm module as described in esm's documentation. 191 |
192 | {
193 |   cjs: true,
194 |   mode: 'auto'
195 | }
196 | 
197 |
undefined
202 | 203 | ### 2.3. return 204 | 205 | The `importSync` function returns the exported module content similar to NodeJS 206 | [require](https://nodejs.org/api/modules.html#requireid). 207 | 208 | If an unknown file path is provided a default 209 | [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error) 210 | object is thrown. 211 | 212 | ## 3. License 213 | 214 |
215 | 216 | Massachusetts Institute of Technology 217 | (MIT) 218 | 219 | 220 |
221 | 222 | ``` 223 | Copyright (c) 2023 Khiet Tam Nguyen 224 | 225 | Permission is hereby granted, free of charge, to any person obtaining a 226 | copy of this software and associated documentation files (the “Software”), 227 | to deal in the Software without restriction, including without limitation 228 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 229 | and/or sell copies of the Software, and to permit persons to whom the 230 | Software is furnished to do so, subject to the following conditions: 231 | 232 | The above copyright notice and this permission notice shall be included in 233 | all copies or substantial portions of the Software. 234 | 235 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 236 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 237 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 238 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 239 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 240 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 241 | DEALINGS IN THE SOFTWARE. 242 | ``` 243 | 244 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fnktnet1%2Fimport-sync.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fnktnet1%2Fimport-sync?ref=badge_large) 245 | 246 |
247 | 248 | ## 4. Limitations 249 | 250 | There are currently no known limitations. 251 | 252 | ## 5. Caveats 253 | 254 | > Please note that loading [ECMAScript Modules using require()](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) are soon to be supported in NodeJS natively. 255 | 256 | ### 5.1. Idea 257 | 258 | **import-sync** was created to enable the implementation of a global dryrun script that can be run by students undertaking 259 | [COMP1531 Software Engineering Fundamentals](https://webcms3.cse.unsw.edu.au/COMP1531/23T2/outline) in their major group project. This requires the ability to import external ES Modules from any directory or path for use in both CommonJS and ESM-based projects. 260 | 261 | The dryrun serves as a sanity check before the 262 | final submission is made, and is located in the centralised [COMP1531 course account](https://taggi.cse.unsw.edu.au/FAQ/Uploading_to_course_accounts/) at the path `~cs1531/bin`. Students who are connected to the CSE lab environment (e.g. via [VLAB](https://taggi.cse.unsw.edu.au/FAQ/VLAB_-_The_technical_details/)) can run the dryrun script from their major project repository, e.g. at the path `~z5313514/comp1531/project-backend`. 263 | 264 | ### 5.2. Discovery 265 | 266 | Initially, the [esm](https://github.com/standard-things/esm) library looked promising. However, when the global dryrun script was executed in a mock student's project directory, the following error occurred: 267 | 268 | > Error [ERR_REQUIRE_ESM]: require() of ES Module /import/ravel/5/z5313515/project-backend/src/auth.js not supported.
269 | Instead change the require of auth.js in null to a dynamic import() which is available in all CommonJS modules 270 | 271 | This is due to the `package.json` containing `"type": "module"`, as iteration 1 of the student major project uses ESM for the seamless transition to future iterations. 272 | 273 | The following approaches were thus attempted, but were unsatisfactory for our purpose: 274 | 275 | 1. [jewire](https://github.com/nktnet1/jewire)/[rewire](https://github.com/jhnns/rewire)/[require](https://nodejs.org/api/modules.html#requireid) 276 | - in iteration 1, the dryrun requires the import of ES6 modules, so [jewire](https://github.com/nktnet1/jewire) (which was used for the dryrun of iteration 0) was no longer satisfying our requirements 277 | - the same limitations of being CommonJS exclusive applies to [rewire](https://github.com/jhnns/rewire) and [require](https://nodejs.org/api/modules.html#requireid) 278 | 2. [import()](https://nodejs.org/api/esm.html#import-expressions) - ECMAScript dynamic import 279 | - this was the previous attempt at writing the dryrun 280 | - However, it relied on asynchronous code. Since COMP1531 is **fully synchronous** (including the use of [sync-request-curl](https://github.com/nktnet1/sync-request-curl) for sending HTTP requests), this became a source of mystery and confusion for students 281 | - additionally, students had to append the suffix `.js` to all of their file imports in the project solely to use the dryrun. This resulted in ambiguous error messages and obscure dryrun requirements unrelated to the project 282 | 3. [require-esm-in-cjs](https://github.com/SamGoody/require-esm-in-cjs) 283 | - this library utilises [deasync](https://github.com/abbr/deasync), which when used in NodeJS for Jest tests, could hang indefinitely as seen in Jest's issue [#9729](https://github.com/jestjs/jest/issues/9729) 284 | - since COMP1531 uses Jest as the sole testing framework, [deasync](https://github.com/abbr/deasync) was ruled out 285 | 4. Other async-to-sync conversions for dynamic [import()](https://nodejs.org/api/esm.html#import-expressions) 286 | - [synckit](https://github.com/un-ts/synckit): worker_threads, Jest and external imports did not work (unclear reason) 287 | - [sync-rpc](https://github.com/ForbesLindesay/sync-rpc): leaves orphan processes when used in Jest as explained in issue [#10](https://github.com/ForbesLindesay/sync-rpc/issues/10) 288 | - [fibers](https://github.com/laverdet/node-fibers): obsolete and does not work for node versions later than 16 289 | - [synchronize](https://github.com/al6x/synchronize): documentation link gives 404 and has fiber as a dependency 290 | - [sync/node-sync](https://github.com/ybogdanov/node-sync): uses fiber (note: "redblaze/node-sync" on github, "sync" on npm) 291 | 292 | ### 5.3. Result 293 | 294 | Upon a more thorough investigation into the initial issue with the 295 | [esm](https://github.com/standard-things/esm) module, the cause was the 296 | introduction of the exception starting from NodeJS version 13, as noted in 297 | [@fregante](https://github.com/fregante)'s comment: 298 | - https://github.com/standard-things/esm/issues/855#issuecomment-558319872 299 | 300 | Further down the thread was a link to the solution by [@guybedford](https://github.com/guybedford) 301 | - https://github.com/standard-things/esm/issues/868#issuecomment-594480715 302 | 303 | which removes the exception through module extension and serves as a satisfactory workaround. This reduced the codebase of **import-sync** to simply a wrapper around [esm](https://github.com/standard-things/esm). 304 | 305 | Another issue that **import-sync** (v2) addresses is [esm's open issue #904](https://github.com/standard-things/esm/issues/904), which yields the error message: 306 | > Error [ERR_INVALID_PROTOCOL]: Protocol 'node:' not supported. Expected 'file:' 307 | 308 | when importing ESM-only libraries such as [node-fetch@3](https://github.com/node-fetch/node-fetch/blob/8b3320d2a7c07bce4afc6b2bf6c3bbddda85b01f/README.md#commonjs) in a CommonJS module. This is done by overriding the default `Module._resolveFilename` function to remove the `node:` prefix, effectively changing any imports of the form (for example): 309 | ```javascript 310 | import http from 'node:http'; 311 | ``` 312 | to 313 | ```javascript 314 | import http from 'http'; 315 | ``` 316 | for all imported modules. 317 | 318 | For further discussions about this issue, visit: 319 | - https://stackoverflow.com/a/77329422/22324694 320 | 321 | --- 322 | 323 | ## 6. Major Changes 324 | 325 | ### 6.1. 2023-12-16 326 | 327 | As of version 2.2.0, **import-sync** has switched from using the archived [esm](https://github.com/standard-things/esm) package to the fork [@httptoolkit/esm](https://github.com/httptoolkit/esm). For further details, please see 328 | - issue [#37](https://github.com/nktnet1/import-sync/issues/37) 329 | - merge request [#38](https://github.com/nktnet1/import-sync/pull/38) 330 | 331 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | rules: { 3 | 'body-leading-blank': [1, 'always'], 4 | 'footer-leading-blank': [1, 'always'], 5 | 'header-max-length': [2, 'always', 90], 6 | 'scope-case': [2, 'always', 'lower-case'], 7 | 'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']], 8 | 'subject-empty': [2, 'never'], 9 | 'subject-full-stop': [2, 'never', '.'], 10 | 'type-case': [2, 'always', 'lower-case'], 11 | 'type-empty': [2, 'never'], 12 | 'type-enum': [ 13 | 2, 14 | 'always', 15 | [ 16 | 'build', 17 | 'chore', 18 | 'ci', 19 | 'deps', 20 | 'docs', 21 | 'feat', 22 | 'fix', 23 | 'refactor', 24 | 'revert', 25 | 'style', 26 | 'test', 27 | ], 28 | ], 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { FlatCompat } from '@eslint/eslintrc'; 4 | import js from '@eslint/js'; 5 | import globals from 'globals'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | recommendedConfig: js.configs.recommended, 12 | allConfig: js.configs.all, 13 | }); 14 | 15 | const eslintFlatConfig = [ 16 | ...compat.extends( 17 | 'plugin:@typescript-eslint/recommended', 18 | 'eslint:recommended', 19 | 'plugin:prettier/recommended', 20 | 'plugin:import/recommended', 21 | 'prettier', 22 | ), 23 | { 24 | languageOptions: { 25 | globals: { 26 | ...globals.node, 27 | ...globals.jest, 28 | }, 29 | }, 30 | }, 31 | { 32 | files: ['**/*.ts', '**/*.tsx'], 33 | languageOptions: { 34 | ecmaVersion: 2021, 35 | sourceType: 'module', 36 | }, 37 | settings: { 38 | 'import/resolver': { 39 | typescript: { 40 | project: './tsconfig.json', 41 | }, 42 | }, 43 | }, 44 | }, 45 | { 46 | rules: { 47 | '@next/next/no-img-element': 'off', 48 | 'no-unused-vars': 'off', 49 | '@typescript-eslint/no-explicit-any': 'off', 50 | 51 | '@typescript-eslint/no-unused-vars': [ 52 | 'error', 53 | { 54 | argsIgnorePattern: '^_', 55 | varsIgnorePattern: '^_', 56 | caughtErrorsIgnorePattern: '^_', 57 | }, 58 | ], 59 | 60 | indent: 'off', 61 | 'import/no-unresolved': 'error', 62 | 'import/named': 'off', 63 | semi: ['error', 'always'], 64 | 65 | quotes: [ 66 | 'error', 67 | 'single', 68 | { 69 | avoidEscape: true, 70 | allowTemplateLiterals: true, 71 | }, 72 | ], 73 | 'import/order': [ 74 | 'error', 75 | { 76 | groups: [ 77 | 'builtin', 78 | 'external', 79 | 'type', 80 | 'internal', 81 | 'parent', 82 | 'sibling', 83 | 'index', 84 | 'object', 85 | ], 86 | alphabetize: { 87 | order: 'asc', 88 | }, 89 | }, 90 | ], 91 | }, 92 | }, 93 | ]; 94 | 95 | export default eslintFlatConfig; 96 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | maxWorkers: 1, 7 | modulePathIgnorePatterns: ['/dist/'], 8 | forceExit: false, 9 | detectOpenHandles: true, 10 | testMatch: ['**/*.test.js', '**/*.test.cjs', '**/*.test.mjs', '**/*.test.ts'], 11 | transform: { 12 | '^.+\\.(ts)$': 'ts-jest', 13 | }, 14 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node', 'cjs', 'mjs'], 15 | collectCoverageFrom: ['src/**/*.ts'], 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "import-sync", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/nktnet1/import-sync" 6 | }, 7 | "version": "2.2.3", 8 | "files": [ 9 | "dist" 10 | ], 11 | "main": "dist/cjs/index.js", 12 | "module": "dist/esm/index.js", 13 | "scripts": { 14 | "test": "jest", 15 | "tc": "jest --coverage", 16 | "lint": "eslint --fix './**/*.ts'", 17 | "tsc": "tsc --noEmit", 18 | "build": "rm -rf dist && npm run build:esm && npm run build:cjs", 19 | "build:esm": "tsc", 20 | "build:cjs": "tsc --module CommonJS --outDir dist/cjs", 21 | "prepublishOnly": "npm run build", 22 | "prepare": "husky" 23 | }, 24 | "keywords": [ 25 | "ecmascript", 26 | "module", 27 | "import", 28 | "export", 29 | "es6", 30 | "esm", 31 | "nodejs", 32 | "commonjs", 33 | "require", 34 | "cjs", 35 | "dyammic", 36 | "runtime", 37 | "sync", 38 | "synchronous", 39 | "comp1531" 40 | ], 41 | "author": "Khiet Tam Nguyen", 42 | "license": "MIT", 43 | "description": "Synchronously import dynamic ECMAScript Modules similar to CommonJS require. Basic wrapper around esm for compatibility with both ESM and CJS projects in NodeJS.", 44 | "devDependencies": { 45 | "@commitlint/cli": "^19.8.1", 46 | "@commitlint/config-conventional": "^19.8.1", 47 | "@eslint/eslintrc": "^3.3.1", 48 | "@eslint/js": "^9.28.0", 49 | "@types/httptoolkit__esm": "^3.3.0", 50 | "@types/jest": "^29.5.14", 51 | "@types/node": "^22.15.29", 52 | "@typescript-eslint/eslint-plugin": "^8.33.1", 53 | "@typescript-eslint/parser": "^8.33.1", 54 | "eslint": "^9.28.0", 55 | "eslint-config-prettier": "^10.1.5", 56 | "eslint-import-resolver-typescript": "^4.4.2", 57 | "eslint-plugin-import": "^2.31.0", 58 | "eslint-plugin-jest": "^28.12.0", 59 | "eslint-plugin-prettier": "^5.4.1", 60 | "eslint-plugin-react-refresh": "^0.4.20", 61 | "globals": "^16.2.0", 62 | "husky": "^9.1.7", 63 | "jest": "^29.7.0", 64 | "node-datachannel": "^0.27.0", 65 | "node-fetch": "^3.3.2", 66 | "ts-jest": "^29.3.4", 67 | "ts-node": "^10.9.2", 68 | "typescript": "^5.8.3" 69 | }, 70 | "dependencies": { 71 | "@httptoolkit/esm": "^3.3.2" 72 | }, 73 | "packageManager": "pnpm@10.11.1" 74 | } 75 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | ignoredBuiltDependencies: 2 | - unrs-resolver 3 | onlyBuiltDependencies: 4 | - node-datachannel 5 | overrides: 6 | '@babel/helpers@<7.26.10': ^7.26.10 7 | tar-fs@<2.1.3: ^2.1.3 8 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const VALID_FILE_EXTENSIONS = Object.freeze(['.js', '.mjs', '.cjs', '.ts']); 2 | -------------------------------------------------------------------------------- /src/files.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { VALID_FILE_EXTENSIONS } from './config'; 4 | 5 | /** 6 | * Get the file path of the caller function. 7 | * 8 | * Implementation inspired by: 9 | * - https://www.npmjs.com/package/callsite?activeTab=code 10 | * 11 | * @returns {string} absolute path or an empty string if no caller 12 | */ 13 | export const getCallerDirname = (): string => { 14 | const orig = Error.prepareStackTrace; 15 | Error.prepareStackTrace = (_, stack) => stack; 16 | const err = new Error(); 17 | Error.captureStackTrace(err, getCallerDirname); 18 | const stack = err.stack as any; 19 | Error.prepareStackTrace = orig; 20 | const callerFilePath = stack[1].getFileName(); 21 | /* istanbul ignore next */ 22 | return path.dirname( 23 | callerFilePath.startsWith('file://') ? callerFilePath.substring(7) : callerFilePath, 24 | ); 25 | }; 26 | 27 | /** 28 | * Find the module file path by checking for available extensions 29 | * as defined by VALID_FILE_EXTENSIONS 30 | * 31 | * @param {string} filePath The absolute path to the file 32 | * @returns {string} The resolved file path with appended extension 33 | * @throws {Error} If the file path does not match any valid extensions 34 | */ 35 | const findFileWithExtensions = (filePath: string): string => { 36 | for (const ext of VALID_FILE_EXTENSIONS) { 37 | const extFilePath = `${filePath}${ext}`; 38 | if (fs.existsSync(extFilePath)) { 39 | return extFilePath; 40 | } 41 | } 42 | throw new Error(`No such file '${filePath}' with matching extensions [${VALID_FILE_EXTENSIONS}]`); 43 | }; 44 | 45 | /** 46 | * Find the module file path 47 | * 48 | * @param {string} modulePath - The path to the module 49 | * @param {string} basePath - The base path for the module 50 | * @returns {string} The resolved file path 51 | * @throws {Error} If the file is not found 52 | */ 53 | export const findModuleFile = (basePath: string, modulePath: string): string => { 54 | const filePath = path.join(basePath, modulePath); 55 | return fs.existsSync(filePath) ? filePath : findFileWithExtensions(filePath); 56 | }; 57 | -------------------------------------------------------------------------------- /src/import.ts: -------------------------------------------------------------------------------- 1 | import esm from '@httptoolkit/esm'; 2 | 3 | import { findModuleFile, getCallerDirname } from './files'; 4 | import { Options } from './options'; 5 | 6 | /** 7 | * Returns an ESM-imported module 8 | * 9 | * @param modulePath absolute path or id of the module to import 10 | * @param options same options as importSync 11 | * @returns the esm imported module 12 | */ 13 | const esmImport = (modulePath: string, options: Options) => { 14 | const esmRequire = esm(module, options.esmOptions); 15 | try { 16 | return esmRequire(modulePath); 17 | } catch (error: any) { 18 | throw new Error(` 19 | Failed to import from: 20 | ${modulePath} 21 | Options: 22 | ${JSON.stringify(options)} 23 | Require error message: 24 | ${error.stack} 25 | `); 26 | } 27 | }; 28 | 29 | /** 30 | * Imports ES6 modules synchronously similar to require in CommonJS 31 | * Can be used in both ES6 and CommonJS projects 32 | * 33 | * @param relativePath - the name or relative path of the module, e.g. ./arrays 34 | * @param {Options} [options] - options as defined in types.ts 35 | * @returns the imported module 36 | */ 37 | const importSync = (id: string, options: Options = {}) => { 38 | const basePath = options.basePath ?? getCallerDirname(); 39 | const modulePath = /^\.\.?\//.test(id) ? findModuleFile(basePath, id) : id; 40 | const importedModule = esmImport(modulePath, options); 41 | 42 | if (Object.keys(importedModule).length > 0) { 43 | return importedModule; 44 | } 45 | // In case CJS shows up as empty, e.g. when importing CommonJS/CommonTS into Jest 46 | try { 47 | // eslint-disable-next-line @typescript-eslint/no-require-imports 48 | const basicModule = require(modulePath); 49 | /* istanbul ignore next */ 50 | if (Object.keys(basicModule).length > 0) { 51 | return basicModule; 52 | } 53 | } catch (_error) { 54 | /* nothing to do */ 55 | } 56 | 57 | return importedModule; 58 | }; 59 | 60 | export default importSync; 61 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import importSync from './import'; 2 | 3 | export default importSync; 4 | export type { Options, ESMOptions } from './options'; 5 | 6 | module.exports = importSync; 7 | module.exports.default = importSync; 8 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import esm from '@httptoolkit/esm'; 2 | 3 | export type ESMOptions = Parameters[1]; 4 | 5 | export interface Options { 6 | basePath?: string; 7 | esmOptions?: ESMOptions; 8 | } 9 | -------------------------------------------------------------------------------- /tests/basic/basic.mjs: -------------------------------------------------------------------------------- 1 | export const helloString = 'helloworld'; 2 | 3 | export const array = [1, 2, 3, 4, 5]; 4 | 5 | export const object = { 6 | key1: 'value1', 7 | key2: 2, 8 | }; 9 | 10 | /** 11 | * Computes the sum of two numbers 12 | * 13 | * @param {number} a first number to add 14 | * @param {number} b second number to add 15 | * @returns the sum of 'a' and 'b' 16 | */ 17 | export const sum = (a, b) => a + b; 18 | -------------------------------------------------------------------------------- /tests/basic/basic.test.ts: -------------------------------------------------------------------------------- 1 | import importSync from '../../src'; 2 | const basic = importSync('./basic.mjs'); 3 | 4 | test('ESM import string variable', () => { 5 | expect(basic.helloString).toStrictEqual('helloworld'); 6 | }); 7 | 8 | test('ESM import primitive number array', () => { 9 | expect(basic.array).toStrictEqual([1, 2, 3, 4, 5]); 10 | }); 11 | 12 | test('ESM import shallow object', () => { 13 | // esm "serialises to the same string" starting from node v22 with .toStrictEqual 14 | expect(basic.object).toEqual({ key1: 'value1', key2: 2 }); 15 | }); 16 | 17 | test('ESM import function', () => { 18 | expect(basic.sum(1, 2)).toStrictEqual(3); 19 | }); 20 | 21 | test('ESM no extension relativePath', () => { 22 | expect(importSync('./basic').helloString).toStrictEqual('helloworld'); 23 | }); 24 | 25 | test('No such file', () => { 26 | expect(() => importSync('./unknown/helloworld')).toThrow(Error); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/edge/broken.js: -------------------------------------------------------------------------------- 1 | export const apple = 2 | -------------------------------------------------------------------------------- /tests/edge/edge.test.ts: -------------------------------------------------------------------------------- 1 | import importSync from '../../src'; 2 | 3 | test('Empty file', () => { 4 | expect(importSync('./empty')).toMatchObject({}); 5 | }); 6 | 7 | test('Broken file', () => { 8 | expect(() => importSync('./broken')).toThrow(Error); 9 | }); 10 | 11 | test.each([ 12 | { extension: '', message: 'JavaScript' }, 13 | { extension: '.js', message: 'JavaScript' }, 14 | { extension: '.mjs', message: 'ECMAScript' }, 15 | { extension: '.cjs', message: 'CommonJS' }, 16 | { extension: '.ts', message: 'TypeScript' }, 17 | ])("Testing extension '$extension' contains message '$message'", ({ extension, message }) => { 18 | expect(importSync(`./extension${extension}`)).toMatchObject({ message }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/edge/empty.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nktnet1/import-sync/bacbb16b936418b77183fc06422ecae964b44ea5/tests/edge/empty.js -------------------------------------------------------------------------------- /tests/edge/extension.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | message: 'CommonJS' 3 | }; 4 | -------------------------------------------------------------------------------- /tests/edge/extension.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | message: 'JavaScript', 3 | }; 4 | -------------------------------------------------------------------------------- /tests/edge/extension.mjs: -------------------------------------------------------------------------------- 1 | export const message = 'ECMAScript'; 2 | -------------------------------------------------------------------------------- /tests/edge/extension.ts: -------------------------------------------------------------------------------- 1 | export const message = 'TypeScript'; 2 | -------------------------------------------------------------------------------- /tests/esmlibs/esmlibs.test.ts: -------------------------------------------------------------------------------- 1 | import importSync from '../../src'; 2 | 3 | test('ESM no extension relativePath', () => { 4 | expect(typeof importSync('node-fetch').default).toStrictEqual('function'); 5 | }); 6 | 7 | test('ESM no extension relativePath', () => { 8 | expect(typeof importSync('node-datachannel').initLogger).toStrictEqual('function'); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/path/path.test.ts: -------------------------------------------------------------------------------- 1 | import importSync from '../../src'; 2 | 3 | test('ESM import with default relative path', () => { 4 | const exampleMjs = importSync('../basic/basic'); 5 | expect(exampleMjs.helloString).toStrictEqual('helloworld'); 6 | }); 7 | 8 | test('ESM import with absolute path', () => { 9 | const exampleMjs = importSync(`${process.cwd()}/tests/basic/basic`); 10 | expect(exampleMjs.helloString).toStrictEqual('helloworld'); 11 | }); 12 | 13 | test('ESM import with different base path', () => { 14 | const exampleMjs = importSync('./tests/basic/basic', { basePath: process.cwd() }); 15 | expect(exampleMjs.helloString).toStrictEqual('helloworld'); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/resolver/nodeFetchImport.js: -------------------------------------------------------------------------------- 1 | import nodeFetch from 'node-fetch'; 2 | 3 | export { nodeFetch }; 4 | -------------------------------------------------------------------------------- /tests/resolver/resolver.test.ts: -------------------------------------------------------------------------------- 1 | import importSync from '../../src'; 2 | 3 | test('Can resolve module imports that are pure-esm, e.g. node-fetch', () => { 4 | expect(importSync('node-fetch').default).toStrictEqual(expect.any(Function)); 5 | }); 6 | 7 | test('Can resolve imports of imports that are pure-esm, e.g. node-fetch', () => { 8 | expect(importSync('./nodeFetchImport.js')).toStrictEqual({ nodeFetch: expect.any(Function) }); 9 | }); 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "lib": ["es5", "es2015", "esnext", "dom"], 8 | "types": ["node", "jest"], 9 | "module": "ESNext", 10 | "moduleResolution": "node", 11 | "noImplicitAny": true, 12 | "noUnusedLocals": true, 13 | "sourceMap": true, 14 | "strict": true, 15 | "outDir": "dist/esm", 16 | "skipLibCheck": true, 17 | "target": "ES6" 18 | }, 19 | "include": [ "src/**/*.ts" , "types/**/*.d.ts" ] 20 | } 21 | --------------------------------------------------------------------------------