@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License
2
2
 
3
- Copyright (c) 2021-2022 Check Digit, LLC
3
+ Copyright (c) 2021-2023 Check Digit, LLC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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
- - requires Node 16 or above
12
- - emits ES2022
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
- #### A note about versioning
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 new target (e.g. `ES2019` to `ES2020`) will result in a new major version of this module. We coordinate this
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
- - Each new minor update of Typescript (e.g. `4.3.1` to `4.3.2`) will result in a new patch version of this module.
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 it comes in as a peer dependency of `@checkdigit/typescript-config`.
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
 
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ // module-directory/index.ts
2
+
3
+ export default () => 'module-directory-index';
package/tsconfig.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "module": "commonjs",
4
- "target": "es2022",
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