I've got a Typescript 5.2.x / Node.js 18.12.x / ESM-based application that is both a website and a CLI with shared business logic. I'm having issues with declaring global types in a way where they work in Node.js without esbuilding (they do work for both the browser and Node.js after esbuild-ing and - for Node.js - pkg-ing). Here's what the application source tree looks like:
user@id app % tree -I 'node_modules|dist*'
.
├── @types
│ └── index.d.ts
├── index.html
├── package-lock.json
├── package.json
├── readme.md
├── src
│ ├── business-logic.ts
│ ├── cli.ts
│ └── web.ts
├── test
│ └── test.business-logic.ts
├── tsconfig.cli.json
├── tsconfig.json
└── tsconfig.web.json
I have one global variable - hello
- defined in @types/index.d.ts
:
declare global {
var hello: string | undefined;
}
export {};
From package.json:
"scripts": {
"start:web": "npm run package:web && python3 -m http.server",
"package:web": "rm -rf ./dist.web && npx tsc -p tsconfig.web.json && npx esbuild ./dist.web/src/web.js --bundle --outfile=./dist.web/bundle.web.js",
"start:cli": "node --experimental-specifier-resolution=node --experimental-modules --no-warnings --loader ts-node/esm ./src/cli.ts",
"package:cli": "rm -rf ./dist.cli && npx tsc -p tsconfig.cli.json && npx esbuild ./dist.cli/src/cli.js --bundle --outfile=./dist.cli/bundle.cli.js && npx pkg ./dist.cli/bundle.cli.js --out-path ./dist.cli",
"start:cli:macos": "npm run package:cli && ./dist.cli/bundle.cli-macos",
"test": "node --experimental-specifier-resolution=node --experimental-modules --no-warnings --loader ts-node/esm ./test/test.business-logic.ts"
}
npm run start:web
and npm run start:cli:macos
work. npm run test
and npm run cli
still fail with a type error:
Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.ts
Does anyone know how to resolve this? I've include the rest of the relevant source files down below for reference:
package.json
:
{
"name": "app",
"version": "0.0.1",
"private": false,
"type": "module",
"dependencies": {},
"devDependencies": {
"typescript": "5.2.2",
"@types/node": "18.11.18",
"ts-node": "10.9.1",
"esbuild": "0.19.2",
"pkg": "5.8.1"
},
"scripts": {
"start:web": "npm run package:web && python3 -m http.server",
"package:web": "rm -rf ./dist.web && npx tsc -p tsconfig.web.json && npx esbuild ./dist.web/src/web.js --bundle --outfile=./dist.web/bundle.web.js",
"start:cli": "node --experimental-specifier-resolution=node --experimental-modules --no-warnings --loader ts-node/esm ./src/cli.ts",
"package:cli": "rm -rf ./dist.cli && npx tsc -p tsconfig.cli.json && npx esbuild ./dist.cli/src/cli.js --bundle --outfile=./dist.cli/bundle.cli.js && npx pkg ./dist.cli/bundle.cli.js --out-path ./dist.cli",
"start:cli:macos": "npm run package:cli && ./dist.cli/bundle.cli-macos",
"test": "node --experimental-specifier-resolution=node --experimental-modules --no-warnings --loader ts-node/esm ./test/test.business-logic.ts"
}
}
tsconfig.json
:
{
"compilerOptions": {
"target": "es2019",
"module": "es2022",
"moduleResolution": "node",
"lib": ["es2022", "dom"],
"types": ["node"],
"rootDir": ".",
"skipLibCheck": true, // Needed to skip node_modules imports from being included.
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"useDefineForClassFields": false,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
// TypeScript development rules:
"strict": true,
//"strictPropertyInitialization": false,
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noImplicitOverride": true,
"forceConsistentCasingInFileNames": true
},
"exclude": ["**/node_modules"],
"include": ["src/**/*.ts","test/**/*.ts", "@types/index.d.ts"]
}
tsconfig.web.json
:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist.web"
},
"exclude": ["**/node_modules", "src/cli.ts", "test"]
}
tsconfig.cli.json
:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist.cli"
},
"exclude": ["**/node_modules", "src/web.ts", "test"]
}
src/web.ts
:
console.log('load web');
globalThis.hello = 'world';
import { respond } from './business-logic';
console.log('hello', respond());
index.html
:
<html>
<body>Test</body>
<script defer src='dist.web/bundle.web.js'></script>
</html>
src/cli.ts
:
console.log('load cli');
globalThis.hello = 'world';
import { respond } from './business-logic';
console.log('hello', respond());
src/business-logic.ts
:
export const respond = (): string | undefined => {
return globalThis.hello;
}
test/test.business-logic.ts
:
import { test, before } from 'node:test';
import { strict as assert } from 'node:assert';
import { respond } from '../src/business-logic';
test('test.business-logic', async (t) => {
before(async () => {
globalThis.hello = 'test world';
});
await t.test('test respond', async (_t) => {
assert.equal('test world', respond());
});
});