diff --git a/package.json b/package.json index a62da7b..261aa63 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,54 @@ "version": "6.3.1", "description": "Meta Names SDK", "main": "dist/index.js", + "module": "dist/esm/index.js", "types": "dist/index.d.ts", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/esm/index.js", + "require": "./dist/index.js", + "default": "./dist/index.js" + }, + "./models/domain": { + "types": "./dist/models/domain.d.ts", + "import": "./dist/esm/models/domain.js", + "require": "./dist/models/domain.js", + "default": "./dist/models/domain.js" + }, + "./providers/config": { + "types": "./dist/providers/config.d.ts", + "import": "./dist/esm/providers/config.js", + "require": "./dist/providers/config.js", + "default": "./dist/providers/config.js" + }, + "./interface": { + "types": "./dist/interface.d.ts", + "import": "./dist/esm/interface.js", + "require": "./dist/interface.js", + "default": "./dist/interface.js" + }, + "./transactions/ledger": { + "types": "./dist/transactions/ledger.d.ts", + "import": "./dist/esm/transactions/ledger.js", + "require": "./dist/transactions/ledger.js", + "default": "./dist/transactions/ledger.js" + }, + "./dist/*.js": { + "types": "./dist/*.d.ts", + "import": "./dist/esm/*.js", + "require": "./dist/*.js", + "default": "./dist/*.js" + }, + "./dist/*": { + "types": "./dist/*.d.ts", + "import": "./dist/esm/*.js", + "require": "./dist/*.js", + "default": "./dist/*.js" + }, + "./package.json": "./package.json" + }, "repository": "https://github.com/MetaNames/sdk.git", "author": "Yeboster ", "license": "MIT", @@ -14,12 +61,14 @@ "dist" ], "scripts": { - "build": "yarn tsc", + "build": "yarn build:cjs && yarn build:esm", "docs": "typedoc --out docs src", "format": "eslint --fix src", "test": "jest -i", "prepublishOnly": "yarn build", - "audit-ci": "bash scripts/audit-ci.sh" + "audit-ci": "bash scripts/audit-ci.sh", + "build:cjs": "tsc", + "build:esm": "tsc -p tsconfig.esm.json && node scripts/finalize-esm.js" }, "dependencies": { "@ledgerhq/hw-transport": "^6.34.0", diff --git a/scripts/finalize-esm.js b/scripts/finalize-esm.js new file mode 100644 index 0000000..8a864fb --- /dev/null +++ b/scripts/finalize-esm.js @@ -0,0 +1,83 @@ +#!/usr/bin/env node +/** + * Makes dist/esm loadable as real ESM. + * + * TypeScript emits relative import specifiers exactly as written in the source, + * and this codebase writes them without a file extension. That is fine for + * CommonJS and for bundlers, but Node's ESM resolver does not guess extensions + * or directory indexes, so `import './models'` would fail at runtime. This + * rewrites every relative specifier to the file it actually resolves to and + * marks the directory as ESM. + */ +const fs = require('fs') +const path = require('path') + +const root = path.join(__dirname, '..', 'dist', 'esm') + +function walk (dir) { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) return walk(full) + return entry.name.endsWith('.js') ? [full] : [] + }) +} + +const nodeModules = path.join(__dirname, '..', 'node_modules') + +/** './models' -> './models/index.js', './interface' -> './interface.js' */ +function addExtension (target, specifier) { + if (fs.existsSync(`${target}.js`)) return `${specifier}.js` + if (fs.existsSync(path.join(target, 'index.js'))) return `${specifier}/index.js` + return null +} + +function resolveSpecifier (fromFile, specifier) { + if (path.extname(specifier)) return specifier + + if (specifier.startsWith('.')) { + const resolved = addExtension(path.resolve(path.dirname(fromFile), specifier), specifier) + if (resolved) return resolved + throw new Error(`cannot resolve ${specifier} from ${fromFile}`) + } + + // Deep imports into a dependency ("pkg/lib/main/thing") need the same + // treatment: Node's ESM resolver will not add the extension for them either, + // and these dependencies publish CommonJS without an "exports" map. + const segments = specifier.split('/') + const depth = specifier.startsWith('@') ? 2 : 1 + if (segments.length <= depth) return specifier + + return addExtension(path.join(nodeModules, specifier), specifier) ?? specifier +} + +if (!fs.existsSync(root)) { + console.error('dist/esm does not exist -- run the esm build first') + process.exit(1) +} + +const files = walk(root) +let rewritten = 0 + +for (const file of files) { + const source = fs.readFileSync(file, 'utf8') + const updated = source.replace( + /(\bfrom\s*|\bimport\s*\(\s*)(['"])([^'"]+)\2/g, + (match, prefix, quote, specifier) => { + const resolved = resolveSpecifier(file, specifier) + if (resolved !== specifier) rewritten++ + return `${prefix}${quote}${resolved}${quote}` + } + ) + if (updated !== source) fs.writeFileSync(file, updated) +} + +// Node decides module format from the nearest package.json; the root one has no +// "type", so without this every file here would be parsed as CommonJS. That +// also makes this the nearest manifest for bundlers, so "sideEffects" has to be +// repeated here or the root declaration stops applying to these files. +fs.writeFileSync( + path.join(root, 'package.json'), + JSON.stringify({ type: 'module', sideEffects: false }, null, 2) + '\n' +) + +console.log(`finalized ${files.length} esm files, rewrote ${rewritten} specifiers`) diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 0000000..e1fa4dd --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + /* ES2022 modules so bundlers can tree-shake the barrel exports. */ + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "./dist/esm", + /* The CJS build is the one that emits .d.ts; duplicating them here would + only make the two copies drift. */ + "declaration": false, + "declarationMap": false + } +}