@savvy-web/tsdown-plugins 0.1.0
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/README.md +61 -0
- package/build/build-target-groups.js +133 -0
- package/build/cjs-default-interop.js +98 -0
- package/build/node-builtin-default-interop.js +74 -0
- package/build/strip-maps.js +40 -0
- package/build/sync-public.js +67 -0
- package/build/target-groups.js +52 -0
- package/catalog/resolve-catalogs.js +24 -0
- package/config-validation/ConfigValidator.js +8 -0
- package/config-validation/ConfigValidatorLive.js +61 -0
- package/dts/resolved-tsconfig.js +44 -0
- package/entry/extract.js +68 -0
- package/entry/package-json-entries.js +12 -0
- package/errors.js +36 -0
- package/exe/build.js +23 -0
- package/exe/config.js +50 -0
- package/index.d.ts +1457 -0
- package/index.js +44 -0
- package/jsx/config.js +40 -0
- package/manifest/emit-manifest.js +56 -0
- package/manifest/transform.js +123 -0
- package/meta/api-extractor.js +59 -0
- package/meta/config.js +14 -0
- package/meta/generate.js +76 -0
- package/meta/merge-models.js +44 -0
- package/meta/message-suppressor.js +37 -0
- package/meta/tsconfig-resolver.js +260 -0
- package/meta/tsdoc-config.js +47 -0
- package/package.json +45 -0
- package/report/formatters/ci-annotations.js +20 -0
- package/report/formatters/json.js +12 -0
- package/report/formatters/markdown.js +25 -0
- package/report/formatters/silent.js +8 -0
- package/report/formatters/terminal.js +29 -0
- package/report/layers/EnvironmentDetectorLive.js +15 -0
- package/report/layers/ExecutorResolverLive.js +8 -0
- package/report/layers/FormatSelectorLive.js +8 -0
- package/report/layers/OutputRendererLive.js +23 -0
- package/report/pipeline.js +25 -0
- package/report/schema-export.js +18 -0
- package/report/schema.js +19 -0
- package/report/services/EnvironmentDetector.js +7 -0
- package/report/services/ExecutorResolver.js +7 -0
- package/report/services/FormatSelector.js +7 -0
- package/report/services/OutputRenderer.js +7 -0
- package/report/timer.js +15 -0
- package/targets/binding.js +15 -0
- package/targets/config.js +8 -0
- package/targets/resolve-targets.js +126 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { JsxEmit, ModuleDetectionKind, ModuleKind, ModuleResolutionKind, NewLineKind, ScriptTarget, flattenDiagnosticMessageText, getParsedCommandLineOfConfigFile, sys } from "typescript";
|
|
4
|
+
|
|
5
|
+
//#region src/meta/tsconfig-resolver.ts
|
|
6
|
+
/**
|
|
7
|
+
* JSON schema URL for tsconfig.json files.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
const TSCONFIG_SCHEMA_URL = "https://json.schemastore.org/tsconfig";
|
|
11
|
+
/**
|
|
12
|
+
* Boolean compiler options preserved in the portable config.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* These options affect type checking and module semantics without producing
|
|
16
|
+
* build artifacts. Emit-related options are excluded.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
const PRESERVED_BOOLEAN_OPTIONS = [
|
|
21
|
+
"strict",
|
|
22
|
+
"strictNullChecks",
|
|
23
|
+
"strictFunctionTypes",
|
|
24
|
+
"strictBindCallApply",
|
|
25
|
+
"strictPropertyInitialization",
|
|
26
|
+
"noImplicitAny",
|
|
27
|
+
"noImplicitThis",
|
|
28
|
+
"alwaysStrict",
|
|
29
|
+
"noUnusedLocals",
|
|
30
|
+
"noUnusedParameters",
|
|
31
|
+
"exactOptionalPropertyTypes",
|
|
32
|
+
"noImplicitReturns",
|
|
33
|
+
"noFallthroughCasesInSwitch",
|
|
34
|
+
"noUncheckedIndexedAccess",
|
|
35
|
+
"noImplicitOverride",
|
|
36
|
+
"noPropertyAccessFromIndexSignature",
|
|
37
|
+
"allowUnusedLabels",
|
|
38
|
+
"allowUnreachableCode",
|
|
39
|
+
"esModuleInterop",
|
|
40
|
+
"allowSyntheticDefaultImports",
|
|
41
|
+
"forceConsistentCasingInFileNames",
|
|
42
|
+
"resolveJsonModule",
|
|
43
|
+
"isolatedModules",
|
|
44
|
+
"verbatimModuleSyntax",
|
|
45
|
+
"skipLibCheck",
|
|
46
|
+
"skipDefaultLibCheck",
|
|
47
|
+
"downlevelIteration",
|
|
48
|
+
"importHelpers",
|
|
49
|
+
"preserveConstEnums",
|
|
50
|
+
"isolatedDeclarations",
|
|
51
|
+
"allowImportingTsExtensions",
|
|
52
|
+
"rewriteRelativeImportExtensions",
|
|
53
|
+
"allowArbitraryExtensions",
|
|
54
|
+
"useDefineForClassFields",
|
|
55
|
+
"noLib",
|
|
56
|
+
"preserveSymlinks"
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* String compiler options preserved in the portable config.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
const PRESERVED_STRING_OPTIONS = [
|
|
63
|
+
"jsxFactory",
|
|
64
|
+
"jsxFragmentFactory",
|
|
65
|
+
"jsxImportSource",
|
|
66
|
+
"reactNamespace"
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* Resolves a TypeScript `ParsedCommandLine` to a portable, JSON-serializable
|
|
70
|
+
* tsconfig (compilerOptions-only) for virtual TypeScript environments.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* Converts TypeScript's internal enum representation back to portable JSON
|
|
74
|
+
* suitable for tooling that needs type information without emitting files:
|
|
75
|
+
*
|
|
76
|
+
* - Converts enum values (target, module, moduleResolution, jsx, etc.) to strings.
|
|
77
|
+
* - Converts lib references from full paths (`lib.esnext.d.ts`) to short names (`esnext`).
|
|
78
|
+
* - Forces `composite: false` and `noEmit: true`.
|
|
79
|
+
* - Excludes path-dependent options (rootDir, outDir, baseUrl, paths, typeRoots, types).
|
|
80
|
+
* - Excludes emit-related options (declaration, sourceMap, etc.).
|
|
81
|
+
* - Excludes file selection (include, exclude, files, references).
|
|
82
|
+
* - Adds `$schema` for IDE support.
|
|
83
|
+
*
|
|
84
|
+
* @public
|
|
85
|
+
*/
|
|
86
|
+
var TsconfigResolver = class TsconfigResolver {
|
|
87
|
+
/** @internal */
|
|
88
|
+
static SCRIPT_TARGET_MAP = new Map([
|
|
89
|
+
[ScriptTarget.ES5, "es5"],
|
|
90
|
+
[ScriptTarget.ES2015, "es2015"],
|
|
91
|
+
[ScriptTarget.ES2016, "es2016"],
|
|
92
|
+
[ScriptTarget.ES2017, "es2017"],
|
|
93
|
+
[ScriptTarget.ES2018, "es2018"],
|
|
94
|
+
[ScriptTarget.ES2019, "es2019"],
|
|
95
|
+
[ScriptTarget.ES2020, "es2020"],
|
|
96
|
+
[ScriptTarget.ES2021, "es2021"],
|
|
97
|
+
[ScriptTarget.ES2022, "es2022"],
|
|
98
|
+
[ScriptTarget.ES2023, "es2023"],
|
|
99
|
+
[ScriptTarget.ES2024, "es2024"],
|
|
100
|
+
[ScriptTarget.ESNext, "esnext"],
|
|
101
|
+
[ScriptTarget.JSON, "json"]
|
|
102
|
+
]);
|
|
103
|
+
/** @internal */
|
|
104
|
+
static MODULE_KIND_MAP = new Map([
|
|
105
|
+
[ModuleKind.CommonJS, "commonjs"],
|
|
106
|
+
[ModuleKind.ES2015, "es2015"],
|
|
107
|
+
[ModuleKind.ES2020, "es2020"],
|
|
108
|
+
[ModuleKind.ES2022, "es2022"],
|
|
109
|
+
[ModuleKind.ESNext, "esnext"],
|
|
110
|
+
[ModuleKind.Node16, "node16"],
|
|
111
|
+
[101, "node18"],
|
|
112
|
+
[102, "node20"],
|
|
113
|
+
[ModuleKind.NodeNext, "nodenext"],
|
|
114
|
+
[ModuleKind.Preserve, "preserve"]
|
|
115
|
+
]);
|
|
116
|
+
/** @internal */
|
|
117
|
+
static MODULE_RESOLUTION_MAP = new Map([
|
|
118
|
+
[ModuleResolutionKind.Node10, "node10"],
|
|
119
|
+
[ModuleResolutionKind.Node16, "node16"],
|
|
120
|
+
[ModuleResolutionKind.NodeNext, "nodenext"],
|
|
121
|
+
[ModuleResolutionKind.Bundler, "bundler"]
|
|
122
|
+
]);
|
|
123
|
+
/** @internal */
|
|
124
|
+
static JSX_EMIT_MAP = new Map([
|
|
125
|
+
[JsxEmit.None, "none"],
|
|
126
|
+
[JsxEmit.Preserve, "preserve"],
|
|
127
|
+
[JsxEmit.React, "react"],
|
|
128
|
+
[JsxEmit.ReactNative, "react-native"],
|
|
129
|
+
[JsxEmit.ReactJSX, "react-jsx"],
|
|
130
|
+
[JsxEmit.ReactJSXDev, "react-jsxdev"]
|
|
131
|
+
]);
|
|
132
|
+
/** @internal */
|
|
133
|
+
static MODULE_DETECTION_MAP = new Map([
|
|
134
|
+
[ModuleDetectionKind.Legacy, "legacy"],
|
|
135
|
+
[ModuleDetectionKind.Auto, "auto"],
|
|
136
|
+
[ModuleDetectionKind.Force, "force"]
|
|
137
|
+
]);
|
|
138
|
+
/** @internal */
|
|
139
|
+
static NEW_LINE_MAP = new Map([[NewLineKind.CarriageReturnLineFeed, "crlf"], [NewLineKind.LineFeed, "lf"]]);
|
|
140
|
+
/** Converts a {@link ScriptTarget} enum value to its string form (e.g. `es2023`). */
|
|
141
|
+
static convertScriptTarget(target) {
|
|
142
|
+
if (target === void 0) return void 0;
|
|
143
|
+
const mapped = TsconfigResolver.SCRIPT_TARGET_MAP.get(target);
|
|
144
|
+
if (mapped !== void 0) return mapped;
|
|
145
|
+
return `es${target}`;
|
|
146
|
+
}
|
|
147
|
+
/** Converts a {@link ModuleKind} enum value to its string form (e.g. `nodenext`). */
|
|
148
|
+
static convertModuleKind(module) {
|
|
149
|
+
if (module === void 0) return void 0;
|
|
150
|
+
const mapped = TsconfigResolver.MODULE_KIND_MAP.get(module);
|
|
151
|
+
if (mapped !== void 0) return mapped;
|
|
152
|
+
return String(module);
|
|
153
|
+
}
|
|
154
|
+
/** Converts a {@link ModuleResolutionKind} enum value to its string form (e.g. `nodenext`). */
|
|
155
|
+
static convertModuleResolution(resolution) {
|
|
156
|
+
if (resolution === void 0) return void 0;
|
|
157
|
+
const mapped = TsconfigResolver.MODULE_RESOLUTION_MAP.get(resolution);
|
|
158
|
+
if (mapped !== void 0) return mapped;
|
|
159
|
+
return String(resolution);
|
|
160
|
+
}
|
|
161
|
+
/** Converts a {@link JsxEmit} enum value to its string form (e.g. `preserve`, `react-jsx`). */
|
|
162
|
+
static convertJsxEmit(jsx) {
|
|
163
|
+
if (jsx === void 0) return void 0;
|
|
164
|
+
const mapped = TsconfigResolver.JSX_EMIT_MAP.get(jsx);
|
|
165
|
+
if (mapped !== void 0) return mapped;
|
|
166
|
+
return String(jsx);
|
|
167
|
+
}
|
|
168
|
+
/** Converts a {@link ModuleDetectionKind} enum value to its string form (e.g. `force`). */
|
|
169
|
+
static convertModuleDetection(detection) {
|
|
170
|
+
if (detection === void 0) return void 0;
|
|
171
|
+
const mapped = TsconfigResolver.MODULE_DETECTION_MAP.get(detection);
|
|
172
|
+
if (mapped !== void 0) return mapped;
|
|
173
|
+
return String(detection);
|
|
174
|
+
}
|
|
175
|
+
/** Converts a {@link NewLineKind} enum value to its string form (`lf` or `crlf`). */
|
|
176
|
+
static convertNewLine(newLine) {
|
|
177
|
+
if (newLine === void 0) return void 0;
|
|
178
|
+
const mapped = TsconfigResolver.NEW_LINE_MAP.get(newLine);
|
|
179
|
+
if (mapped !== void 0) return mapped;
|
|
180
|
+
return String(newLine);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Converts a lib reference to its canonical short name.
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* `ParsedCommandLine` stores lib references as full paths like `lib.esnext.d.ts`
|
|
187
|
+
* or `/path/to/typescript/lib/lib.dom.d.ts`. This returns the short tsconfig form
|
|
188
|
+
* (`esnext`, `dom`).
|
|
189
|
+
*/
|
|
190
|
+
static convertLibReference(lib) {
|
|
191
|
+
return (lib.includes("/") || lib.includes("\\") ? lib.split(/[\\/]/).pop() ?? lib : lib).replace(/^lib\./, "").replace(/\.d\.ts$/, "");
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Resolves a parsed TypeScript config to a portable, compilerOptions-only tsconfig.
|
|
195
|
+
*/
|
|
196
|
+
resolve(parsed) {
|
|
197
|
+
const opts = parsed.options;
|
|
198
|
+
const compilerOptions = {};
|
|
199
|
+
if (opts.target !== void 0) compilerOptions.target = TsconfigResolver.convertScriptTarget(opts.target);
|
|
200
|
+
if (opts.module !== void 0) compilerOptions.module = TsconfigResolver.convertModuleKind(opts.module);
|
|
201
|
+
if (opts.moduleResolution !== void 0) compilerOptions.moduleResolution = TsconfigResolver.convertModuleResolution(opts.moduleResolution);
|
|
202
|
+
if (opts.moduleDetection !== void 0) compilerOptions.moduleDetection = TsconfigResolver.convertModuleDetection(opts.moduleDetection);
|
|
203
|
+
if (opts.jsx !== void 0) compilerOptions.jsx = TsconfigResolver.convertJsxEmit(opts.jsx);
|
|
204
|
+
if (opts.newLine !== void 0) compilerOptions.newLine = TsconfigResolver.convertNewLine(opts.newLine);
|
|
205
|
+
if (opts.lib && opts.lib.length > 0) compilerOptions.lib = opts.lib.map(TsconfigResolver.convertLibReference);
|
|
206
|
+
compilerOptions.composite = false;
|
|
207
|
+
compilerOptions.noEmit = true;
|
|
208
|
+
for (const opt of PRESERVED_BOOLEAN_OPTIONS) if (opts[opt] !== void 0) compilerOptions[opt] = opts[opt];
|
|
209
|
+
for (const opt of PRESERVED_STRING_OPTIONS) if (opts[opt] !== void 0) compilerOptions[opt] = opts[opt];
|
|
210
|
+
return {
|
|
211
|
+
$schema: TSCONFIG_SCHEMA_URL,
|
|
212
|
+
compilerOptions
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Resolves the package's effective compiler options (following `extends`) into a
|
|
218
|
+
* portable, JSON-serializable tsconfig for the meta release bundle.
|
|
219
|
+
*
|
|
220
|
+
* @remarks
|
|
221
|
+
* Resolves the package's own `<cwd>/tsconfig.json` (which extends the shared
|
|
222
|
+
* `@savvy-web/bundler/ecma.json` base) via the TypeScript API so the result
|
|
223
|
+
* carries the full effective options (target/module/strict/jsx/lib), then
|
|
224
|
+
* converts them to a portable, compilerOptions-only shape with no absolute
|
|
225
|
+
* paths or emit/file-selection options.
|
|
226
|
+
*
|
|
227
|
+
* When the package has no own `tsconfig.json` (e.g. a minimal test fixture),
|
|
228
|
+
* falls back to `fallbackConfigPath` — the build's already-resolved dts tsconfig,
|
|
229
|
+
* which always exists during a build. If neither is present, returns a minimal
|
|
230
|
+
* portable config carrying only the virtual-environment flags.
|
|
231
|
+
*
|
|
232
|
+
* @param cwd - Absolute package root.
|
|
233
|
+
* @param fallbackConfigPath - Optional resolved tsconfig to use when the package has no own one.
|
|
234
|
+
* @returns The portable tsconfig object.
|
|
235
|
+
*
|
|
236
|
+
* @public
|
|
237
|
+
*/
|
|
238
|
+
function resolvePortableTsconfig(cwd, fallbackConfigPath) {
|
|
239
|
+
const ownConfig = join(cwd, "tsconfig.json");
|
|
240
|
+
const configPath = existsSync(ownConfig) ? ownConfig : fallbackConfigPath !== void 0 && existsSync(fallbackConfigPath) ? fallbackConfigPath : void 0;
|
|
241
|
+
if (configPath === void 0) return {
|
|
242
|
+
$schema: TSCONFIG_SCHEMA_URL,
|
|
243
|
+
compilerOptions: {
|
|
244
|
+
composite: false,
|
|
245
|
+
noEmit: true
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const parsed = getParsedCommandLineOfConfigFile(configPath, {}, {
|
|
249
|
+
...sys,
|
|
250
|
+
onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
|
|
251
|
+
const message = flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
252
|
+
throw new Error(`Cannot resolve portable tsconfig at ${configPath}: ${message}`);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
if (parsed === void 0) throw new Error(`Failed to parse tsconfig at ${configPath}`);
|
|
256
|
+
return new TsconfigResolver().resolve(parsed);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
//#endregion
|
|
260
|
+
export { TsconfigResolver, resolvePortableTsconfig };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { StandardTags } from "@microsoft/tsdoc";
|
|
4
|
+
import deepEqual from "deep-equal";
|
|
5
|
+
|
|
6
|
+
//#region src/meta/tsdoc-config.ts
|
|
7
|
+
const TSDOC_SCHEMA = "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json";
|
|
8
|
+
const SYNTAX_KIND_MAP = {
|
|
9
|
+
block: "block",
|
|
10
|
+
inline: "inline",
|
|
11
|
+
modifier: "modifier"
|
|
12
|
+
};
|
|
13
|
+
/** Build the tsdoc.json object: standard tags enabled, every standard tag marked supported, plus any custom tags. */
|
|
14
|
+
function buildTsdocConfig(tsdoc) {
|
|
15
|
+
const supportForTags = {};
|
|
16
|
+
for (const def of StandardTags.allDefinitions) supportForTags[def.tagName] = true;
|
|
17
|
+
const tagDefinitions = tsdoc.tagDefinitions.map((t) => ({
|
|
18
|
+
tagName: t.tagName,
|
|
19
|
+
syntaxKind: SYNTAX_KIND_MAP[t.syntaxKind],
|
|
20
|
+
...t.allowMultiple !== void 0 ? { allowMultiple: t.allowMultiple } : {}
|
|
21
|
+
}));
|
|
22
|
+
for (const t of tagDefinitions) supportForTags[t.tagName] = true;
|
|
23
|
+
const config = {
|
|
24
|
+
$schema: TSDOC_SCHEMA,
|
|
25
|
+
noStandardTags: false,
|
|
26
|
+
reportUnsupportedHtmlElements: true,
|
|
27
|
+
supportForTags
|
|
28
|
+
};
|
|
29
|
+
if (tagDefinitions.length > 0) config.tagDefinitions = tagDefinitions;
|
|
30
|
+
return config;
|
|
31
|
+
}
|
|
32
|
+
/** Write tsdoc.json to `cwd`. Deterministic and idempotent: skips the write when the existing file is byte-equal to the computed config. Returns the path. */
|
|
33
|
+
function writeTsdocConfig(cwd, tsdoc) {
|
|
34
|
+
const path = join(cwd, "tsdoc.json");
|
|
35
|
+
const config = buildTsdocConfig({
|
|
36
|
+
suppressWarnings: tsdoc.suppressWarnings ?? [],
|
|
37
|
+
tagDefinitions: tsdoc.tagDefinitions ?? []
|
|
38
|
+
});
|
|
39
|
+
if (existsSync(path)) try {
|
|
40
|
+
if (deepEqual(JSON.parse(readFileSync(path, "utf-8")), config, { strict: true })) return path;
|
|
41
|
+
} catch {}
|
|
42
|
+
writeFileSync(path, `${JSON.stringify(config, null, " ")}\n`, "utf-8");
|
|
43
|
+
return path;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { writeTsdocConfig };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@savvy-web/tsdown-plugins",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Interface-only tsdown/rolldown plugin pack powering @savvy-web/bundler",
|
|
6
|
+
"homepage": "https://github.com/savvy-web/systems/tree/main/packages/tsdown-plugins",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/savvy-web/systems/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/savvy-web/systems.git",
|
|
13
|
+
"directory": "packages/tsdown-plugins"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "C. Spencer Beggs",
|
|
18
|
+
"email": "spencer@savvyweb.systems",
|
|
19
|
+
"url": "https://savvyweb.systems"
|
|
20
|
+
},
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./index.d.ts",
|
|
26
|
+
"import": "./index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@effect/platform-node": "^0.106.0",
|
|
31
|
+
"@microsoft/api-extractor": "^7.58.7",
|
|
32
|
+
"@microsoft/tsdoc": "^0.16.0",
|
|
33
|
+
"@microsoft/tsdoc-config": "^0.18.1",
|
|
34
|
+
"deep-equal": "^2.2.3",
|
|
35
|
+
"json-schema-effect": "^0.2.1",
|
|
36
|
+
"picocolors": "^1.1.1",
|
|
37
|
+
"sort-package-json": "^3.6.1",
|
|
38
|
+
"std-env": "^4.1.0",
|
|
39
|
+
"typescript": "^6.0.3",
|
|
40
|
+
"workspaces-effect": "^1.2.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"effect": ">=3.21.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/report/formatters/ci-annotations.ts
|
|
2
|
+
const esc = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
3
|
+
const CiAnnotationsFormatter = {
|
|
4
|
+
format: "ci-annotations",
|
|
5
|
+
render: (reports) => {
|
|
6
|
+
const lines = [];
|
|
7
|
+
for (const r of reports) for (const g of r.targetGroups) {
|
|
8
|
+
for (const e of g.errors) lines.push(`::error title=${esc(r.package)} (${esc(g.id)})::${esc(e)}`);
|
|
9
|
+
for (const w of g.warnings) lines.push(`::warning title=${esc(r.package)} (${esc(g.id)})::${esc(w)}`);
|
|
10
|
+
}
|
|
11
|
+
return lines.length === 0 ? [] : [{
|
|
12
|
+
target: "stdout",
|
|
13
|
+
content: lines.join("\n"),
|
|
14
|
+
contentType: "text/plain"
|
|
15
|
+
}];
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { CiAnnotationsFormatter };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/report/formatters/json.ts
|
|
2
|
+
const JsonFormatter = {
|
|
3
|
+
format: "json",
|
|
4
|
+
render: (reports) => [{
|
|
5
|
+
target: "stdout",
|
|
6
|
+
content: JSON.stringify(reports, null, 2),
|
|
7
|
+
contentType: "application/json"
|
|
8
|
+
}]
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { JsonFormatter };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/report/formatters/markdown.ts
|
|
2
|
+
const MarkdownFormatter = {
|
|
3
|
+
format: "markdown",
|
|
4
|
+
render: (reports) => {
|
|
5
|
+
const lines = [];
|
|
6
|
+
for (const r of reports) {
|
|
7
|
+
const failing = r.targetGroups.filter((g) => g.errors.length > 0);
|
|
8
|
+
if (failing.length > 0) {
|
|
9
|
+
lines.push(`## ❌ ${r.package}`);
|
|
10
|
+
for (const g of failing) {
|
|
11
|
+
lines.push(`- **${g.id}**`);
|
|
12
|
+
for (const e of g.errors) lines.push(` - ${e}`);
|
|
13
|
+
}
|
|
14
|
+
} else lines.push(`## ✅ ${r.package}`);
|
|
15
|
+
}
|
|
16
|
+
return [{
|
|
17
|
+
target: "stdout",
|
|
18
|
+
content: lines.join("\n"),
|
|
19
|
+
contentType: "text/markdown"
|
|
20
|
+
}];
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { MarkdownFormatter };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { formatTime } from "../timer.js";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
|
|
4
|
+
//#region src/report/formatters/terminal.ts
|
|
5
|
+
const TerminalFormatter = {
|
|
6
|
+
format: "terminal",
|
|
7
|
+
render: (reports, ctx) => {
|
|
8
|
+
const color = (fn, s) => ctx.noColor ? s : fn(s);
|
|
9
|
+
const lines = [];
|
|
10
|
+
for (const r of reports) {
|
|
11
|
+
lines.push(color(pc.bold, r.package));
|
|
12
|
+
for (const g of r.targetGroups) {
|
|
13
|
+
const status = g.errors.length ? color(pc.red, "✗") : color(pc.green, "✓");
|
|
14
|
+
lines.push(` ${status} ${g.id} — ${g.emittedFiles.length} files (${formatTime(g.timings.totalMs)})`);
|
|
15
|
+
for (const e of g.errors) lines.push(` ${color(pc.red, "error")}: ${e}`);
|
|
16
|
+
for (const w of g.warnings) lines.push(` ${color(pc.yellow, "warn")}: ${w}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const content = lines.join("\n");
|
|
20
|
+
return content === "" ? [] : [{
|
|
21
|
+
target: "stdout",
|
|
22
|
+
content,
|
|
23
|
+
contentType: "text/plain"
|
|
24
|
+
}];
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { TerminalFormatter };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EnvironmentDetector } from "../services/EnvironmentDetector.js";
|
|
2
|
+
import { Effect, Layer } from "effect";
|
|
3
|
+
import { isAgent, isCI } from "std-env";
|
|
4
|
+
|
|
5
|
+
//#region src/report/layers/EnvironmentDetectorLive.ts
|
|
6
|
+
const isGitHub = () => process.env.GITHUB_ACTIONS === "true" || process.env.GITHUB_ACTIONS === "1";
|
|
7
|
+
const EnvironmentDetectorLive = Layer.succeed(EnvironmentDetector, { detect: () => Effect.sync(() => {
|
|
8
|
+
if (isAgent) return "agent-shell";
|
|
9
|
+
if (isGitHub()) return "ci-github";
|
|
10
|
+
if (isCI) return "ci-generic";
|
|
11
|
+
return "terminal";
|
|
12
|
+
}) });
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { EnvironmentDetectorLive };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ExecutorResolver } from "../services/ExecutorResolver.js";
|
|
2
|
+
import { Effect, Layer } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/report/layers/ExecutorResolverLive.ts
|
|
5
|
+
const ExecutorResolverLive = Layer.succeed(ExecutorResolver, { resolve: (env) => Effect.succeed(env === "agent-shell" ? "agent" : env === "terminal" ? "human" : "ci") });
|
|
6
|
+
|
|
7
|
+
//#endregion
|
|
8
|
+
export { ExecutorResolverLive };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FormatSelector } from "../services/FormatSelector.js";
|
|
2
|
+
import { Effect, Layer } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/report/layers/FormatSelectorLive.ts
|
|
5
|
+
const FormatSelectorLive = Layer.succeed(FormatSelector, { select: (executor, explicit, env) => Effect.succeed(explicit ?? (env === "ci-github" && executor === "ci" ? "ci-annotations" : executor === "agent" ? "markdown" : executor === "ci" ? "json" : "terminal")) });
|
|
6
|
+
|
|
7
|
+
//#endregion
|
|
8
|
+
export { FormatSelectorLive };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CiAnnotationsFormatter } from "../formatters/ci-annotations.js";
|
|
2
|
+
import { JsonFormatter } from "../formatters/json.js";
|
|
3
|
+
import { MarkdownFormatter } from "../formatters/markdown.js";
|
|
4
|
+
import { SilentFormatter } from "../formatters/silent.js";
|
|
5
|
+
import { TerminalFormatter } from "../formatters/terminal.js";
|
|
6
|
+
import { OutputRenderer } from "../services/OutputRenderer.js";
|
|
7
|
+
import { Effect, Layer } from "effect";
|
|
8
|
+
|
|
9
|
+
//#region src/report/layers/OutputRendererLive.ts
|
|
10
|
+
const formatters = new Map([
|
|
11
|
+
["terminal", TerminalFormatter],
|
|
12
|
+
["json", JsonFormatter],
|
|
13
|
+
["markdown", MarkdownFormatter],
|
|
14
|
+
["ci-annotations", CiAnnotationsFormatter],
|
|
15
|
+
["silent", SilentFormatter]
|
|
16
|
+
]);
|
|
17
|
+
const OutputRendererLive = Layer.succeed(OutputRenderer, { render: (reports, format, ctx) => Effect.sync(() => {
|
|
18
|
+
const f = formatters.get(format);
|
|
19
|
+
return f ? f.render(reports, ctx) : [];
|
|
20
|
+
}) });
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { OutputRendererLive };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { EnvironmentDetector } from "./services/EnvironmentDetector.js";
|
|
2
|
+
import { EnvironmentDetectorLive } from "./layers/EnvironmentDetectorLive.js";
|
|
3
|
+
import { ExecutorResolver } from "./services/ExecutorResolver.js";
|
|
4
|
+
import { ExecutorResolverLive } from "./layers/ExecutorResolverLive.js";
|
|
5
|
+
import { FormatSelector } from "./services/FormatSelector.js";
|
|
6
|
+
import { FormatSelectorLive } from "./layers/FormatSelectorLive.js";
|
|
7
|
+
import { OutputRenderer } from "./services/OutputRenderer.js";
|
|
8
|
+
import { OutputRendererLive } from "./layers/OutputRendererLive.js";
|
|
9
|
+
import { Effect, Layer } from "effect";
|
|
10
|
+
|
|
11
|
+
//#region src/report/pipeline.ts
|
|
12
|
+
const ReportPipelineLive = Layer.mergeAll(EnvironmentDetectorLive, ExecutorResolverLive, FormatSelectorLive, OutputRendererLive);
|
|
13
|
+
const renderReport = (reports, options) => Effect.gen(function* () {
|
|
14
|
+
const detector = yield* EnvironmentDetector;
|
|
15
|
+
const executorResolver = yield* ExecutorResolver;
|
|
16
|
+
const formatSelector = yield* FormatSelector;
|
|
17
|
+
const renderer = yield* OutputRenderer;
|
|
18
|
+
const env = options.env ?? (yield* detector.detect());
|
|
19
|
+
const executor = yield* executorResolver.resolve(env);
|
|
20
|
+
const format = yield* formatSelector.select(executor, options.explicitFormat, env);
|
|
21
|
+
return yield* renderer.render(reports, format, { noColor: options.noColor });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { ReportPipelineLive, renderReport };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BuildReport } from "./schema.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { JsonSchemaExporter } from "json-schema-effect";
|
|
4
|
+
|
|
5
|
+
//#region src/report/schema-export.ts
|
|
6
|
+
const SCHEMA_ID = "https://savvyweb.systems/schemas/build-report.schema.json";
|
|
7
|
+
/** Generate the SchemaStore-compatible JSON Schema document for BuildReport. */
|
|
8
|
+
const generateBuildReportSchema = () => Effect.gen(function* () {
|
|
9
|
+
return yield* (yield* JsonSchemaExporter).generate({
|
|
10
|
+
name: "build-report",
|
|
11
|
+
schema: BuildReport,
|
|
12
|
+
rootDefName: "BuildReport",
|
|
13
|
+
$id: SCHEMA_ID
|
|
14
|
+
});
|
|
15
|
+
}).pipe(Effect.provide(JsonSchemaExporter.Live));
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { generateBuildReportSchema };
|
package/report/schema.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/report/schema.ts
|
|
4
|
+
const ReportTimings = Schema.Struct({ totalMs: Schema.Number }).annotations({ identifier: "ReportTimings" });
|
|
5
|
+
const TargetGroupReport = Schema.Struct({
|
|
6
|
+
id: Schema.String,
|
|
7
|
+
entries: Schema.Array(Schema.String),
|
|
8
|
+
emittedFiles: Schema.Array(Schema.String),
|
|
9
|
+
timings: ReportTimings,
|
|
10
|
+
warnings: Schema.Array(Schema.String),
|
|
11
|
+
errors: Schema.Array(Schema.String)
|
|
12
|
+
}).annotations({ identifier: "TargetGroupReport" });
|
|
13
|
+
const BuildReport = Schema.Struct({
|
|
14
|
+
package: Schema.String,
|
|
15
|
+
targetGroups: Schema.Array(TargetGroupReport)
|
|
16
|
+
}).annotations({ identifier: "BuildReport" });
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { BuildReport, ReportTimings, TargetGroupReport };
|
package/report/timer.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/report/timer.ts
|
|
2
|
+
function formatTime(ms) {
|
|
3
|
+
return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(2)}s`;
|
|
4
|
+
}
|
|
5
|
+
/** Create a wall-clock timer. (Date.now is fine in runtime build code.) */
|
|
6
|
+
function createTimer(now = Date.now) {
|
|
7
|
+
const start = now();
|
|
8
|
+
return {
|
|
9
|
+
elapsed: () => now() - start,
|
|
10
|
+
format: () => formatTime(now() - start)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { createTimer, formatTime };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
//#region src/targets/binding.ts
|
|
5
|
+
/** Write the target-to-group binding to dist/prod/targets.json for the release action to consume. Returns the path. */
|
|
6
|
+
function writeTargetsBinding(cwd, resolution) {
|
|
7
|
+
const dir = join(cwd, "dist", "prod");
|
|
8
|
+
mkdirSync(dir, { recursive: true });
|
|
9
|
+
const path = join(dir, "targets.json");
|
|
10
|
+
writeFileSync(path, `${JSON.stringify(resolution, null, " ")}\n`, "utf-8");
|
|
11
|
+
return path;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { writeTargetsBinding };
|