├── .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 | # [](https://github.com/nktnet1/import-sync)
4 |
5 | [](https://github.com/nktnet1/import-sync/actions/workflows/pipeline.yml)
6 |
7 | [](https://codecov.io/gh/nktnet1/import-sync)
8 |
9 | [](https://codeclimate.com/github/nktnet1/import-sync/maintainability)
10 |
11 | [](https://snyk.io/test/github/nktnet1/import-sync)
12 |
13 | [](https://github.com/search?q=repo%3Anktnet1%2Fimport-sync++language%3ATypeScript&type=code)
14 |
15 | [](https://www.npmjs.com/package/import-sync?activeTab=versions)
16 |
17 | [](https://packagephobia.com/result?p=import-sync)
18 |
19 | [](https://depfu.com/github/nktnet1/import-sync?project_id=39032)
20 |
21 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fnktnet1%2Fimport-sync?ref=badge_shield)
22 |
23 | [](https://opensource.org/license/mit/)
24 |
25 | [](https://github.com/nktnet1/import-sync/issues)
26 |
27 | [](https://sonarcloud.io/summary/new_code?id=nktnet1_import-sync)
28 |
29 | [](https://app.codacy.com/gh/nktnet1/import-sync/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
30 |
31 | [](https://app.deepsource.com/gh/nktnet1/import-sync/)
32 |
33 | [](https://codebeat.co/projects/github-com-nktnet1-import-sync-main)
34 |
35 | [](https://github.com/nktnet1/import-sync/stargazers)
36 |
37 | [](https://moiva.io/?npm=import-sync)
38 |
39 | [](https://moiva.io/?npm=import-sync)
40 |
41 | [](https://moiva.io/?npm=import-sync)
42 |
43 | [](https://moiva.io/?npm=import-sync)
44 |
45 | [](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 | [](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 | Option |
157 | Description |
158 | Example |
159 | Default |
160 |
161 |
162 |
163 | basePath |
164 |
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 | |
180 |
181 |
182 | ./myModule
183 |
184 | |
185 | __dirname |
186 |
187 |
188 | esmOptions |
189 | Options for the esm module as described in esm's documentation. |
190 |
191 |
192 | {
193 | cjs: true,
194 | mode: 'auto'
195 | }
196 |
197 | |
198 | undefined |
199 |
200 |
201 |
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 | [](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 |
--------------------------------------------------------------------------------