@checkdigit/typescript-config 3.3.0 → 3.4.0-PR.36-4133
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +1 -1
- package/README.md +67 -10
- package/bin/builder.mjs +200 -0
- package/package.json +1 -41
- package/src/builder/builder.mts +236 -0
- package/src/builder/index.mts +44 -0
- package/src/test/typescript/module-directory/index.ts +3 -0
- package/tsconfig.json +3 -3
package/LICENSE.txt
CHANGED
package/README.md
CHANGED
@@ -6,22 +6,78 @@ Copyright (c) 2022-2023 [Check Digit, LLC](https://checkdigit.com)
|
|
6
6
|
|
7
7
|
### Introduction
|
8
8
|
|
9
|
-
This module contains the standard Check Digit Typescript configuration
|
9
|
+
This module contains the standard Check Digit Typescript configuration, along with our standard build tool `builder`.
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
### Typescript Configuration
|
12
|
+
|
13
|
+
- currently requires Node 16 or above.
|
14
|
+
- emits `esnext`, with the default libraries, to avoid down-leveling. It is intended that application spec tests pick
|
15
|
+
up any issues with using newer features unavailable in a particular environment. Browsers and NodeJS are fast moving
|
16
|
+
targets, and can add language features at any time.
|
13
17
|
- uses the `module` type of `commonjs`.
|
14
|
-
- all compiler options set for maximum strictness
|
18
|
+
- all compiler options set for maximum strictness.
|
19
|
+
|
20
|
+
### Builder
|
21
|
+
|
22
|
+
`builder` is a command line tool that generates either commonjs or esm modules, from Typescript source. It is intended
|
23
|
+
to be used when publishing a package to NPM, or to bundle a package for deployment. It uses `tsc` for generating
|
24
|
+
types, and `esbuild` for generating code.
|
25
|
+
|
26
|
+
#### Options
|
27
|
+
|
28
|
+
- `--type` the type of output to generate. Defaults to `module` (ESM). Valid values are `commonjs`, `module` or `types`.
|
29
|
+
- `--entryPoint` the entry point for the bundle, relative to the inDir. if not provided, the files in the inDir will
|
30
|
+
be processed as individual unbundled files.
|
31
|
+
- `--inDir` the input source code directory.
|
32
|
+
- `--outDir` the output directory.
|
33
|
+
- `--outFile` the output file, relative to `--outDir`. This is provided for single-file bundles, along with `--entryPoint`.
|
34
|
+
- `--external` external modules to exclude from the bundle. Built-in `node` modules are automatically excluded.
|
35
|
+
A wildcard `*` can be used to exclude multiple external modules.
|
36
|
+
- `--minify` whether to minify the output.
|
37
|
+
- `--sourceMap` whether to include inline sourcemap.
|
38
|
+
|
39
|
+
#### Examples
|
40
|
+
|
41
|
+
```
|
42
|
+
# build commonjs .cjs files from Typescript source
|
43
|
+
npx builder --type=commonjs --outDir=build-cjs
|
44
|
+
|
45
|
+
# build single-file commonjs .cjs bundle from Typescript source
|
46
|
+
npx builder --type=commonjs --entryPoint=index.ts --outDir=build-cjs-bundle --outFile=index.cjs
|
47
|
+
|
48
|
+
# build ESM .mjs files from Typescript source
|
49
|
+
npx builder --type=module --outDir=build-esm
|
50
|
+
|
51
|
+
# build single-file ESM .mjs bundle from Typescript source
|
52
|
+
npx builder --type=module --outDir=build-esm-bundle --entryPoint=index.ts --outFile=index.mjs
|
53
|
+
```
|
54
|
+
|
55
|
+
### Tests
|
56
|
+
|
57
|
+
This module includes a number of integration-style tests, to ensure that a specific version of Typescript will interoperate
|
58
|
+
with `builder`, in addition to libraries and frameworks used by Check Digit:
|
59
|
+
|
60
|
+
- Jest and `ts-jest`
|
61
|
+
- ESLint and `@typescript-eslint/eslint-plugin`
|
62
|
+
- Built-in `node:test` runner
|
63
|
+
- prettier
|
64
|
+
- tsc
|
65
|
+
- esbuild
|
66
|
+
|
67
|
+
We do this to ensure that Typescript upgrades do not break these dependencies. New major versions of Typescript are not immediately
|
68
|
+
supported by projects such as ts-jest, eslint, prettier, etc. Our policy is to wait until these projects fully support
|
69
|
+
the new version of Typescript, and/or without emitting warnings during these tests, before publishing.
|
15
70
|
|
16
|
-
|
71
|
+
### A note about versioning
|
17
72
|
|
18
73
|
Strict semver is a little complicated, as Typescript itself does not adhere to semver. So our "best effort" policy is:
|
19
74
|
|
20
|
-
- Each
|
21
|
-
with whatever the latest LTS version of Node is currently supported by Amazon Lambda, Google Cloud Functions
|
75
|
+
- Each update to the minimum Node target (e.g. Node 16 to Node 18) will result in a new major version of this module.
|
76
|
+
We coordinate this with whatever the latest LTS version of Node is currently supported by Amazon Lambda, Google Cloud Functions
|
22
77
|
and Azure Functions.
|
23
|
-
- Each new major version of Typescript (e.g. `4.2.x` to `4.3.x`) will result in a new minor version of this module.
|
24
|
-
-
|
78
|
+
- Each new "major" version of Typescript (e.g. `4.2.x` to `4.3.x`) will result in a new minor version of this module.
|
79
|
+
- A new minor update of Typescript (e.g. `4.3.1` to `4.3.2`) _may_ result in a patch, in
|
80
|
+
a situation where a specific need or issue requires setting a new minimum version of Typescript.
|
25
81
|
|
26
82
|
Bear in mind, any update of Typescript can potentially break your build. But hopefully in a way that's useful.
|
27
83
|
|
@@ -31,7 +87,8 @@ Bear in mind, any update of Typescript can potentially break your build. But hop
|
|
31
87
|
npm add -D @checkdigit/typescript-config
|
32
88
|
```
|
33
89
|
|
34
|
-
Note: you do not need to explicitly install Typescript itself, as
|
90
|
+
Note: you do not need to explicitly install Typescript itself, as the most recent supported version comes in as a
|
91
|
+
peer dependency of `@checkdigit/typescript-config`.
|
35
92
|
|
36
93
|
Make sure your project's `tsconfig.json` extends `@checkdigit/typescript-config`.
|
37
94
|
|
package/bin/builder.mjs
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
// src/builder/index.mts
|
3
|
+
import { strict as assert2 } from "node:assert";
|
4
|
+
import path2 from "node:path";
|
5
|
+
import { parseArgs } from "node:util";
|
6
|
+
|
7
|
+
// src/builder/builder.mts
|
8
|
+
import { strict as assert } from "node:assert";
|
9
|
+
import { promises as fs } from "node:fs";
|
10
|
+
import path from "node:path";
|
11
|
+
import typescript from "typescript";
|
12
|
+
import { build } from "esbuild";
|
13
|
+
async function getFiles(directory) {
|
14
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
15
|
+
const files = await Promise.all(
|
16
|
+
entries.map((entry) => {
|
17
|
+
const result = path.resolve(directory, entry.name);
|
18
|
+
return entry.isDirectory() ? getFiles(result) : result;
|
19
|
+
})
|
20
|
+
);
|
21
|
+
return files.flat();
|
22
|
+
}
|
23
|
+
function excludeSourceMaps(filter) {
|
24
|
+
return (pluginBuild) => {
|
25
|
+
pluginBuild.onLoad({ filter }, async (args) => {
|
26
|
+
if (args.path.endsWith(".js") || args.path.endsWith(".mjs") || args.path.endsWith(".cjs")) {
|
27
|
+
return {
|
28
|
+
contents: `${await fs.readFile(
|
29
|
+
args.path,
|
30
|
+
"utf8"
|
31
|
+
)}
|
32
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJtYXBwaW5ncyI6IkEifQ==`,
|
33
|
+
loader: "default"
|
34
|
+
};
|
35
|
+
}
|
36
|
+
return void 0;
|
37
|
+
});
|
38
|
+
};
|
39
|
+
}
|
40
|
+
function resolveTypescriptPaths(type2) {
|
41
|
+
const extension = type2 === "module" ? "mjs" : "cjs";
|
42
|
+
return (pluginBuild) => {
|
43
|
+
pluginBuild.onResolve({ filter: /.*/u }, async (resolved) => {
|
44
|
+
if (resolved.kind === "entry-point" || !resolved.path.startsWith(".") || resolved.path.endsWith(".js")) {
|
45
|
+
return { external: resolved.kind !== "entry-point" };
|
46
|
+
}
|
47
|
+
let isDirectory = false;
|
48
|
+
try {
|
49
|
+
const stats = await fs.lstat(path.join(resolved.resolveDir, resolved.path));
|
50
|
+
isDirectory = stats.isDirectory();
|
51
|
+
} catch {
|
52
|
+
}
|
53
|
+
let newPath = resolved.path;
|
54
|
+
newPath += isDirectory ? `/index.${extension}` : `.${extension}`;
|
55
|
+
return { path: newPath, external: true };
|
56
|
+
});
|
57
|
+
};
|
58
|
+
}
|
59
|
+
async function builder_default({
|
60
|
+
type: type2,
|
61
|
+
entryPoint: entryPoint2,
|
62
|
+
inDir: inDir2,
|
63
|
+
outDir: outDir2,
|
64
|
+
outFile: outFile2,
|
65
|
+
external: external2 = [],
|
66
|
+
minify: minify2 = false,
|
67
|
+
sourceMap: sourceMap2
|
68
|
+
}) {
|
69
|
+
const messages2 = [];
|
70
|
+
assert.ok(
|
71
|
+
entryPoint2 === void 0 && outFile2 === void 0 || entryPoint2 !== void 0 && outFile2 !== void 0,
|
72
|
+
"entryPoint and outFile must both be provided"
|
73
|
+
);
|
74
|
+
const allSourceFiles = await getFiles(inDir2);
|
75
|
+
const productionSourceFiles = entryPoint2 === void 0 ? allSourceFiles.filter((file) => file.endsWith(".ts")) : [path.join(inDir2, entryPoint2)];
|
76
|
+
const program = typescript.createProgram(productionSourceFiles, {
|
77
|
+
module: typescript.ModuleKind.ESNext,
|
78
|
+
moduleResolution: typescript.ModuleResolutionKind.Bundler,
|
79
|
+
target: typescript.ScriptTarget.ESNext,
|
80
|
+
declaration: true,
|
81
|
+
noEmit: type2 !== "types",
|
82
|
+
noEmitOnError: true,
|
83
|
+
emitDeclarationOnly: type2 === "types",
|
84
|
+
rootDir: inDir2,
|
85
|
+
outDir: outDir2,
|
86
|
+
noLib: false,
|
87
|
+
skipLibCheck: true,
|
88
|
+
strict: true,
|
89
|
+
preserveConstEnums: true,
|
90
|
+
noImplicitReturns: true,
|
91
|
+
noUnusedLocals: true,
|
92
|
+
noUnusedParameters: true,
|
93
|
+
alwaysStrict: true,
|
94
|
+
verbatimModuleSyntax: false,
|
95
|
+
noFallthroughCasesInSwitch: true,
|
96
|
+
forceConsistentCasingInFileNames: true,
|
97
|
+
emitDecoratorMetadata: true,
|
98
|
+
experimentalDecorators: true,
|
99
|
+
resolveJsonModule: true,
|
100
|
+
esModuleInterop: true,
|
101
|
+
noUncheckedIndexedAccess: true,
|
102
|
+
noPropertyAccessFromIndexSignature: true,
|
103
|
+
allowUnusedLabels: false,
|
104
|
+
allowUnreachableCode: false,
|
105
|
+
noImplicitOverride: true,
|
106
|
+
useUnknownInCatchVariables: true,
|
107
|
+
exactOptionalPropertyTypes: true
|
108
|
+
});
|
109
|
+
const emitResult = program.emit();
|
110
|
+
const allDiagnostics = typescript.sortAndDeduplicateDiagnostics([
|
111
|
+
...typescript.getPreEmitDiagnostics(program),
|
112
|
+
...emitResult.diagnostics
|
113
|
+
]);
|
114
|
+
for (const diagnostic of allDiagnostics) {
|
115
|
+
if (diagnostic.file) {
|
116
|
+
assert.ok(diagnostic.start !== void 0);
|
117
|
+
const { line, character } = typescript.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
|
118
|
+
const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
119
|
+
messages2.push(`tsc: ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
|
120
|
+
} else {
|
121
|
+
messages2.push(`tsc: ${typescript.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`);
|
122
|
+
}
|
123
|
+
}
|
124
|
+
if (messages2.length > 0) {
|
125
|
+
throw new Error(`tsc failed ${JSON.stringify(messages2)}`);
|
126
|
+
}
|
127
|
+
if (type2 === "types") {
|
128
|
+
return [];
|
129
|
+
}
|
130
|
+
const buildResult = await build({
|
131
|
+
entryPoints: productionSourceFiles,
|
132
|
+
bundle: true,
|
133
|
+
minify: minify2,
|
134
|
+
platform: "node",
|
135
|
+
format: type2 === "module" ? "esm" : "cjs",
|
136
|
+
sourcesContent: false,
|
137
|
+
sourcemap: sourceMap2 ? "inline" : false,
|
138
|
+
...outFile2 === void 0 ? {
|
139
|
+
// individual files
|
140
|
+
outdir: outDir2,
|
141
|
+
outExtension: { ".js": type2 === "module" ? ".mjs" : ".cjs" },
|
142
|
+
plugins: [
|
143
|
+
{
|
144
|
+
name: "resolve-typescript-paths",
|
145
|
+
setup: resolveTypescriptPaths(type2)
|
146
|
+
}
|
147
|
+
]
|
148
|
+
} : {
|
149
|
+
// bundling
|
150
|
+
outfile: path.join(outDir2, outFile2),
|
151
|
+
legalComments: "none",
|
152
|
+
external: external2,
|
153
|
+
plugins: [
|
154
|
+
{
|
155
|
+
name: "exclude-source-maps",
|
156
|
+
setup: excludeSourceMaps(/node_modules/u)
|
157
|
+
}
|
158
|
+
]
|
159
|
+
}
|
160
|
+
});
|
161
|
+
messages2.push(...buildResult.errors.map((error) => `esbuild error: ${error.text}`));
|
162
|
+
messages2.push(...buildResult.warnings.map((warning) => `esbuild warning: ${warning.text}`));
|
163
|
+
if (messages2.length > 0) {
|
164
|
+
throw new Error(`esbuild failed ${JSON.stringify(messages2)}`);
|
165
|
+
}
|
166
|
+
return messages2;
|
167
|
+
}
|
168
|
+
|
169
|
+
// src/builder/index.mts
|
170
|
+
var {
|
171
|
+
values: { type, inDir, outDir, entryPoint, outFile, external, minify, sourceMap }
|
172
|
+
} = parseArgs({
|
173
|
+
options: {
|
174
|
+
type: { type: "string", short: "t", default: "module" },
|
175
|
+
inDir: { type: "string", short: "i", default: "src" },
|
176
|
+
outDir: { type: "string", short: "o", default: "build" },
|
177
|
+
entryPoint: { type: "string", short: "e", default: void 0 },
|
178
|
+
outFile: { type: "string", short: "f", default: void 0 },
|
179
|
+
external: { type: "string", short: "x", multiple: true, default: [] },
|
180
|
+
minify: { type: "boolean", short: "m", default: false },
|
181
|
+
sourceMap: { type: "boolean", short: "s", default: false }
|
182
|
+
}
|
183
|
+
});
|
184
|
+
assert2.ok(type === "module" || type === "commonjs" || type === "types", "type must be types, module or commonjs");
|
185
|
+
assert2.ok(inDir !== void 0, "inDir is required");
|
186
|
+
assert2.ok(outDir !== void 0, "outDir is required");
|
187
|
+
var messages = await builder_default({
|
188
|
+
type,
|
189
|
+
inDir: path2.join(process.cwd(), inDir),
|
190
|
+
outDir: path2.join(process.cwd(), outDir),
|
191
|
+
entryPoint,
|
192
|
+
outFile,
|
193
|
+
external,
|
194
|
+
minify,
|
195
|
+
sourceMap
|
196
|
+
});
|
197
|
+
if (messages.length > 0) {
|
198
|
+
console.warn(JSON.stringify(messages, void 0, 2));
|
199
|
+
process.exit(1);
|
200
|
+
}
|
package/package.json
CHANGED
@@ -1,41 +1 @@
|
|
1
|
-
{
|
2
|
-
"name": "@checkdigit/typescript-config",
|
3
|
-
"version": "3.3.0",
|
4
|
-
"description": "Check Digit standard Typescript configuration",
|
5
|
-
"prettier": "@checkdigit/prettier-config",
|
6
|
-
"engines": {
|
7
|
-
"node": ">=16"
|
8
|
-
},
|
9
|
-
"peerDependencies": {
|
10
|
-
"@types/node": ">=16",
|
11
|
-
"typescript": ">=5.0.2 <5.1"
|
12
|
-
},
|
13
|
-
"repository": {
|
14
|
-
"type": "git",
|
15
|
-
"url": "git+https://github.com/checkdigit/typescript-config.git"
|
16
|
-
},
|
17
|
-
"author": "Check Digit, LLC",
|
18
|
-
"license": "MIT",
|
19
|
-
"bugs": {
|
20
|
-
"url": "https://github.com/checkdigit/typescript-config/issues"
|
21
|
-
},
|
22
|
-
"homepage": "https://github.com/checkdigit/typescript-config#readme",
|
23
|
-
"scripts": {
|
24
|
-
"preversion": "npm test",
|
25
|
-
"postversion": "git push && git push --tags",
|
26
|
-
"prettier": "prettier --list-different .",
|
27
|
-
"prettier:fix": "prettier --write .",
|
28
|
-
"test": "npm run ci:compile && npm run ci:test && npm run ci:style",
|
29
|
-
"ci:compile": "tsc --noEmit",
|
30
|
-
"ci:test": "tsc && node build/index.js | grep -q 'complete' && rimraf build",
|
31
|
-
"ci:style": "npm run prettier"
|
32
|
-
},
|
33
|
-
"devDependencies": {
|
34
|
-
"@checkdigit/prettier-config": "^3.3.0",
|
35
|
-
"rimraf": "^4.4.0"
|
36
|
-
},
|
37
|
-
"files": [
|
38
|
-
"tsconfig.json",
|
39
|
-
"SECURITY.md"
|
40
|
-
]
|
41
|
-
}
|
1
|
+
{"name":"@checkdigit/typescript-config","version":"3.4.0-PR.36-4133","description":"Check Digit standard Typescript configuration","prettier":"@checkdigit/prettier-config","engines":{"node":">=16"},"bin":{"builder":"./bin/builder.mjs"},"peerDependencies":{"@types/node":">=16","esbuild":"0.17.19","typescript":">=5.0.4 <5.1"},"repository":{"type":"git","url":"git+https://github.com/checkdigit/typescript-config.git"},"author":"Check Digit, LLC","license":"MIT","bugs":{"url":"https://github.com/checkdigit/typescript-config/issues"},"homepage":"https://github.com/checkdigit/typescript-config#readme","scripts":{"preversion":"npm test","postversion":"git push && git push --tags","prepare":"npm run build-builder","lint:fix":"eslint -f unix --ext .ts,.mts src --fix","lint":"eslint -f unix --ext .ts,.mts src","prettier":"prettier --ignore-path .gitignore --list-different .","prettier:fix":"prettier --ignore-path .gitignore --write .","test":"npm run ci:compile && npm run ci:test && npm run ci:lint && npm run ci:style","build-builder":"esbuild src/builder/index.mts --bundle --platform=node --format=esm --external:typescript --external:esbuild --outfile=build-builder/builder.mjs && mkdir -p bin && { echo '#!/usr/bin/env node'; cat build-builder/builder.mjs; } > bin/builder.mjs && chmod +x bin/builder.mjs","build-types":"rimraf build-types && npx builder --type=types --outDir=build-types","build-cjs":"rimraf build-cjs && npx builder --type=commonjs --outDir=build-cjs","build-cjs-bundle":"rimraf build-cjs-bundle && npx builder --type=commonjs --entryPoint=test/index.test.ts --outDir=build-cjs-bundle --outFile=test/index.test.cjs --minify --sourceMap","build-cjs-bundle-no-external":"rimraf build-cjs-bundle-no-external && npx builder --type=commonjs --external=./node_modules/* --entryPoint=test/index.test.ts --outDir=build-cjs-bundle-no-external --outFile=test/index.test.cjs","build-esm":"rimraf build-esm && npx builder --type=module --outDir=build-esm","build-esm-bundle":"rimraf build-esm-bundle && npx builder --type=module --outDir=build-esm-bundle --entryPoint=test/index.test.ts --outFile=test/index.test.mjs","build-esm-bundle-no-external":"rimraf build-esm-bundle-no-external && npx builder --type=module --external=./node_modules/* --outDir=build-esm-bundle-no-external --entryPoint=test/index.test.ts --outFile=test/index.test.mjs --minify","test-jest-esm":"NODE_OPTIONS=\"--experimental-vm-modules\" jest --coverage=false src/*.mts src/*/*.mts src/*/*/*.mts","test-jest-cjs":"jest --coverage=false src/*.ts src/*/*.ts src/*/*/*.ts","test-cjs":"node --test build-cjs/test/index.test.cjs","test-cjs-bundle":"node --test build-cjs-bundle/test/index.test.cjs","test-cjs-bundle-no-external":"node --test build-cjs-bundle-no-external/test/index.test.cjs","test-esm":"node --test build-esm/test/index.test.mjs","test-esm-bundle":"echo \"node --test build-esm-bundle/test/index.test.mjs\"","test-esm-bundle-no-external":"node --test build-esm-bundle-no-external/test/index.test.mjs","ci:test":"npm run test-jest-cjs && npm run test-jest-esm && npm run test-cjs && npm run test-cjs-bundle && npm run test-cjs-bundle-no-external && npm run test-esm && npm run test-esm-bundle && npm run test-esm-bundle-no-external","ci:compile":"npm run build-builder && npm run build-types && npm run build-cjs && npm run build-cjs-bundle && npm run build-cjs-bundle-no-external && npm run build-esm && npm run build-esm-bundle && npm run build-esm-bundle-no-external","ci:lint":"npm run lint","ci:style":"npm run prettier"},"devDependencies":{"@checkdigit/prettier-config":"^3.4.0","@types/debug":"^4.1.8","@types/jest":"^29.5.1","@types/uuid":"^9.0.1","@typescript-eslint/eslint-plugin":"^5.59.7","@typescript-eslint/parser":"^5.59.7","debug":"^4.3.4","eslint":"^8.41.0","eslint-config-prettier":"^8.8.0","get-port":"^6.1.2","got":"^11.8.6","jest":"^29.5.0","rimraf":"^5.0.1","ts-jest":"^29.1.0","uuid":"^9.0.0"},"eslintConfig":{"parser":"@typescript-eslint/parser","plugins":["@typescript-eslint"],"parserOptions":{"project":"./tsconfig.json"},"extends":["eslint:all","plugin:@typescript-eslint/recommended","plugin:@typescript-eslint/recommended-requiring-type-checking","plugin:@typescript-eslint/strict","prettier"],"rules":{"@typescript-eslint/non-nullable-type-assertion-style":"error","capitalized-comments":"off","one-var":"off","sort-keys":"off","sort-imports":"off","func-style":["error","declaration",{"allowArrowFunctions":true}],"no-magic-numbers":["error",{"ignore":[0,1,2]}],"no-undefined":"off","no-ternary":"off"},"overrides":[{"files":["*.spec.mts","*.test.mts","*.spec.ts","*.test.ts"],"rules":{"@typescript-eslint/non-nullable-type-assertion-style":"off","@typescript-eslint/ban-types":"off","@typescript-eslint/require-await":"off","@typescript-eslint/consistent-type-definitions":"off","@typescript-eslint/ban-ts-comment":"off","@typescript-eslint/no-unnecessary-condition":"off","@typescript-eslint/consistent-indexed-object-style":"off","@typescript-eslint/no-unused-vars":"off","@typescript-eslint/no-unsafe-member-access":"off","line-comment-position":"off","no-inline-comments":"off","no-param-reassign":"off","id-length":"off","no-magic-numbers":"off","func-names":"off","no-duplicate-imports":"off","symbol-description":"off","no-invalid-this":"off","max-lines-per-function":"off","max-lines":"off","max-statements":"off","no-await-in-loop":"off"}}]},"jest":{"moduleFileExtensions":["js","mjs","cjs","ts","mts","json","node"],"extensionsToTreatAsEsm":[".mts"],"transform":{"^.+\\.mts$":["ts-jest",{"isolatedModules":true,"diagnostics":false,"useESM":true}],"^.+\\.ts$":["ts-jest",{"isolatedModules":true,"diagnostics":false,"useESM":false}]},"collectCoverageFrom":["<rootDir>/src/**","!<rootDir>/src/**/*.spec.mts","!<rootDir>/src/**/*.test.mts","!<rootDir>/src/**/*.spec.ts","!<rootDir>/src/**/*.test.ts"],"testMatch":["<rootDir>/src/**/*.spec.ts","<rootDir>/src/**/*.spec.mts"]},"files":["tsconfig.json","SECURITY.md","/src/"]}
|
@@ -0,0 +1,236 @@
|
|
1
|
+
// builder/builder.mts
|
2
|
+
|
3
|
+
import { strict as assert } from 'node:assert';
|
4
|
+
import { promises as fs } from 'node:fs';
|
5
|
+
import path from 'node:path';
|
6
|
+
import typescript from 'typescript';
|
7
|
+
|
8
|
+
import { PluginBuild, build } from 'esbuild';
|
9
|
+
|
10
|
+
export interface BuilderOptions {
|
11
|
+
/**
|
12
|
+
* whether to produce ESM or CommonJS code
|
13
|
+
*/
|
14
|
+
type: 'module' | 'commonjs' | 'types';
|
15
|
+
|
16
|
+
/**
|
17
|
+
* the entry point for the bundle, relative to the inDir. if not provided, the files in the inDir will be processed
|
18
|
+
* as individual unbundled files
|
19
|
+
*/
|
20
|
+
entryPoint?: string | undefined;
|
21
|
+
|
22
|
+
/**
|
23
|
+
* source code
|
24
|
+
*/
|
25
|
+
inDir: string;
|
26
|
+
|
27
|
+
/**
|
28
|
+
* build directory
|
29
|
+
*/
|
30
|
+
outDir: string;
|
31
|
+
|
32
|
+
/**
|
33
|
+
* build file, relative to the outDir
|
34
|
+
*/
|
35
|
+
outFile?: string | undefined;
|
36
|
+
|
37
|
+
/**
|
38
|
+
* external modules to exclude from the bundle
|
39
|
+
*/
|
40
|
+
external?: string[] | undefined;
|
41
|
+
|
42
|
+
/**
|
43
|
+
* whether to minify output
|
44
|
+
*/
|
45
|
+
minify?: boolean | undefined;
|
46
|
+
|
47
|
+
/**
|
48
|
+
* whether to include sourcemap
|
49
|
+
*/
|
50
|
+
sourceMap?: boolean | undefined;
|
51
|
+
}
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Recursively obtains all files in a directory
|
55
|
+
* @param {string} directory
|
56
|
+
* @returns {Promise<string[]>}
|
57
|
+
*/
|
58
|
+
async function getFiles(directory: string): Promise<string[]> {
|
59
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
60
|
+
const files = await Promise.all(
|
61
|
+
entries.map((entry) => {
|
62
|
+
const result = path.resolve(directory, entry.name);
|
63
|
+
return entry.isDirectory() ? getFiles(result) : result;
|
64
|
+
})
|
65
|
+
);
|
66
|
+
return files.flat();
|
67
|
+
}
|
68
|
+
|
69
|
+
function excludeSourceMaps(filter: RegExp) {
|
70
|
+
return (pluginBuild: PluginBuild) => {
|
71
|
+
// ignore source maps for any Javascript file that matches filter
|
72
|
+
pluginBuild.onLoad({ filter }, async (args) => {
|
73
|
+
if (args.path.endsWith('.js') || args.path.endsWith('.mjs') || args.path.endsWith('.cjs')) {
|
74
|
+
return {
|
75
|
+
contents: `${await fs.readFile(
|
76
|
+
args.path,
|
77
|
+
'utf8'
|
78
|
+
)}\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJtYXBwaW5ncyI6IkEifQ==`,
|
79
|
+
loader: 'default',
|
80
|
+
};
|
81
|
+
}
|
82
|
+
return undefined;
|
83
|
+
});
|
84
|
+
};
|
85
|
+
}
|
86
|
+
|
87
|
+
function resolveTypescriptPaths(type: 'module' | 'commonjs') {
|
88
|
+
const extension = type === 'module' ? 'mjs' : 'cjs';
|
89
|
+
return (pluginBuild: PluginBuild) => {
|
90
|
+
// rewrite paths based on standard node resolution
|
91
|
+
pluginBuild.onResolve({ filter: /.*/u }, async (resolved) => {
|
92
|
+
if (resolved.kind === 'entry-point' || !resolved.path.startsWith('.') || resolved.path.endsWith('.js')) {
|
93
|
+
return { external: resolved.kind !== 'entry-point' };
|
94
|
+
}
|
95
|
+
let isDirectory = false;
|
96
|
+
try {
|
97
|
+
const stats = await fs.lstat(path.join(resolved.resolveDir, resolved.path));
|
98
|
+
isDirectory = stats.isDirectory();
|
99
|
+
} catch {
|
100
|
+
// do nothing
|
101
|
+
}
|
102
|
+
let newPath = resolved.path;
|
103
|
+
newPath += isDirectory ? `/index.${extension}` : `.${extension}`;
|
104
|
+
return { path: newPath, external: true };
|
105
|
+
});
|
106
|
+
};
|
107
|
+
}
|
108
|
+
|
109
|
+
// eslint-disable-next-line func-names,max-lines-per-function,max-statements
|
110
|
+
export default async function ({
|
111
|
+
type,
|
112
|
+
entryPoint,
|
113
|
+
inDir,
|
114
|
+
outDir,
|
115
|
+
outFile,
|
116
|
+
external = [],
|
117
|
+
minify = false,
|
118
|
+
sourceMap,
|
119
|
+
}: BuilderOptions): Promise<string[]> {
|
120
|
+
const messages: string[] = [];
|
121
|
+
|
122
|
+
assert.ok(
|
123
|
+
(entryPoint === undefined && outFile === undefined) || (entryPoint !== undefined && outFile !== undefined),
|
124
|
+
'entryPoint and outFile must both be provided'
|
125
|
+
);
|
126
|
+
|
127
|
+
const allSourceFiles = await getFiles(inDir);
|
128
|
+
const productionSourceFiles =
|
129
|
+
entryPoint === undefined ? allSourceFiles.filter((file) => file.endsWith('.ts')) : [path.join(inDir, entryPoint)];
|
130
|
+
|
131
|
+
/**
|
132
|
+
* Emit declarations using typescript compiler, if type is 'types'. Otherwise, just compile to ensure the build is good.
|
133
|
+
*/
|
134
|
+
const program = typescript.createProgram(productionSourceFiles, {
|
135
|
+
module: typescript.ModuleKind.ESNext,
|
136
|
+
moduleResolution: typescript.ModuleResolutionKind.Bundler,
|
137
|
+
target: typescript.ScriptTarget.ESNext,
|
138
|
+
declaration: true,
|
139
|
+
noEmit: type !== 'types',
|
140
|
+
noEmitOnError: true,
|
141
|
+
emitDeclarationOnly: type === 'types',
|
142
|
+
rootDir: inDir,
|
143
|
+
outDir,
|
144
|
+
noLib: false,
|
145
|
+
skipLibCheck: true,
|
146
|
+
strict: true,
|
147
|
+
preserveConstEnums: true,
|
148
|
+
noImplicitReturns: true,
|
149
|
+
noUnusedLocals: true,
|
150
|
+
noUnusedParameters: true,
|
151
|
+
alwaysStrict: true,
|
152
|
+
verbatimModuleSyntax: false,
|
153
|
+
noFallthroughCasesInSwitch: true,
|
154
|
+
forceConsistentCasingInFileNames: true,
|
155
|
+
emitDecoratorMetadata: true,
|
156
|
+
experimentalDecorators: true,
|
157
|
+
resolveJsonModule: true,
|
158
|
+
esModuleInterop: true,
|
159
|
+
noUncheckedIndexedAccess: true,
|
160
|
+
noPropertyAccessFromIndexSignature: true,
|
161
|
+
allowUnusedLabels: false,
|
162
|
+
allowUnreachableCode: false,
|
163
|
+
noImplicitOverride: true,
|
164
|
+
useUnknownInCatchVariables: true,
|
165
|
+
exactOptionalPropertyTypes: true,
|
166
|
+
});
|
167
|
+
const emitResult = program.emit();
|
168
|
+
const allDiagnostics = typescript.sortAndDeduplicateDiagnostics([
|
169
|
+
...typescript.getPreEmitDiagnostics(program),
|
170
|
+
...emitResult.diagnostics,
|
171
|
+
]);
|
172
|
+
for (const diagnostic of allDiagnostics) {
|
173
|
+
if (diagnostic.file) {
|
174
|
+
assert.ok(diagnostic.start !== undefined);
|
175
|
+
const { line, character } = typescript.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
|
176
|
+
const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
177
|
+
messages.push(`tsc: ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
|
178
|
+
} else {
|
179
|
+
// eslint-disable-next-line no-console
|
180
|
+
messages.push(`tsc: ${typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`);
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
if (messages.length > 0) {
|
185
|
+
throw new Error(`tsc failed ${JSON.stringify(messages)}`);
|
186
|
+
}
|
187
|
+
|
188
|
+
if (type === 'types') {
|
189
|
+
return [];
|
190
|
+
}
|
191
|
+
|
192
|
+
/**
|
193
|
+
* Emit ESM javascript using esbuild
|
194
|
+
*/
|
195
|
+
const buildResult = await build({
|
196
|
+
entryPoints: productionSourceFiles,
|
197
|
+
bundle: true,
|
198
|
+
minify,
|
199
|
+
platform: 'node',
|
200
|
+
format: type === 'module' ? 'esm' : 'cjs',
|
201
|
+
sourcesContent: false,
|
202
|
+
sourcemap: sourceMap ? 'inline' : false,
|
203
|
+
...(outFile === undefined
|
204
|
+
? {
|
205
|
+
// individual files
|
206
|
+
outdir: outDir,
|
207
|
+
outExtension: { '.js': type === 'module' ? '.mjs' : '.cjs' },
|
208
|
+
plugins: [
|
209
|
+
{
|
210
|
+
name: 'resolve-typescript-paths',
|
211
|
+
setup: resolveTypescriptPaths(type),
|
212
|
+
},
|
213
|
+
],
|
214
|
+
}
|
215
|
+
: {
|
216
|
+
// bundling
|
217
|
+
outfile: path.join(outDir, outFile),
|
218
|
+
legalComments: 'none',
|
219
|
+
external,
|
220
|
+
plugins: [
|
221
|
+
{
|
222
|
+
name: 'exclude-source-maps',
|
223
|
+
setup: excludeSourceMaps(/node_modules/u),
|
224
|
+
},
|
225
|
+
],
|
226
|
+
}),
|
227
|
+
});
|
228
|
+
|
229
|
+
messages.push(...buildResult.errors.map((error) => `esbuild error: ${error.text}`));
|
230
|
+
messages.push(...buildResult.warnings.map((warning) => `esbuild warning: ${warning.text}`));
|
231
|
+
if (messages.length > 0) {
|
232
|
+
throw new Error(`esbuild failed ${JSON.stringify(messages)}`);
|
233
|
+
}
|
234
|
+
|
235
|
+
return messages;
|
236
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
// builder/index.mts
|
2
|
+
|
3
|
+
import { strict as assert } from 'node:assert';
|
4
|
+
import path from 'node:path';
|
5
|
+
import { parseArgs } from 'node:util';
|
6
|
+
|
7
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
8
|
+
// @ts-expect-error
|
9
|
+
import builder from './builder.mts';
|
10
|
+
|
11
|
+
const {
|
12
|
+
values: { type, inDir, outDir, entryPoint, outFile, external, minify, sourceMap },
|
13
|
+
} = parseArgs({
|
14
|
+
options: {
|
15
|
+
type: { type: 'string', short: 't', default: 'module' },
|
16
|
+
inDir: { type: 'string', short: 'i', default: 'src' },
|
17
|
+
outDir: { type: 'string', short: 'o', default: 'build' },
|
18
|
+
entryPoint: { type: 'string', short: 'e', default: undefined },
|
19
|
+
outFile: { type: 'string', short: 'f', default: undefined },
|
20
|
+
external: { type: 'string', short: 'x', multiple: true, default: [] },
|
21
|
+
minify: { type: 'boolean', short: 'm', default: false },
|
22
|
+
sourceMap: { type: 'boolean', short: 's', default: false },
|
23
|
+
},
|
24
|
+
});
|
25
|
+
|
26
|
+
assert.ok(type === 'module' || type === 'commonjs' || type === 'types', 'type must be types, module or commonjs');
|
27
|
+
assert.ok(inDir !== undefined, 'inDir is required');
|
28
|
+
assert.ok(outDir !== undefined, 'outDir is required');
|
29
|
+
|
30
|
+
const messages = await builder({
|
31
|
+
type,
|
32
|
+
inDir: path.join(process.cwd(), inDir),
|
33
|
+
outDir: path.join(process.cwd(), outDir),
|
34
|
+
entryPoint,
|
35
|
+
outFile,
|
36
|
+
external,
|
37
|
+
minify,
|
38
|
+
sourceMap,
|
39
|
+
});
|
40
|
+
if (messages.length > 0) {
|
41
|
+
// eslint-disable-next-line no-console
|
42
|
+
console.warn(JSON.stringify(messages, undefined, 2));
|
43
|
+
process.exit(1);
|
44
|
+
}
|
package/tsconfig.json
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"compilerOptions": {
|
3
|
-
"module": "
|
4
|
-
"target": "
|
5
|
-
"lib": ["esnext", "dom", "webworker"],
|
3
|
+
"module": "NodeNext",
|
4
|
+
"target": "esnext",
|
6
5
|
"sourceMap": true,
|
7
6
|
"inlineSources": true,
|
8
7
|
"outDir": "build",
|
@@ -28,6 +27,7 @@
|
|
28
27
|
"noUncheckedIndexedAccess": true,
|
29
28
|
"noPropertyAccessFromIndexSignature": true,
|
30
29
|
"allowUnusedLabels": false,
|
30
|
+
"allowUnreachableCode": false,
|
31
31
|
"noImplicitOverride": true,
|
32
32
|
"useUnknownInCatchVariables": true,
|
33
33
|
"exactOptionalPropertyTypes": true
|