@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.
Files changed (49) hide show
  1. package/README.md +61 -0
  2. package/build/build-target-groups.js +133 -0
  3. package/build/cjs-default-interop.js +98 -0
  4. package/build/node-builtin-default-interop.js +74 -0
  5. package/build/strip-maps.js +40 -0
  6. package/build/sync-public.js +67 -0
  7. package/build/target-groups.js +52 -0
  8. package/catalog/resolve-catalogs.js +24 -0
  9. package/config-validation/ConfigValidator.js +8 -0
  10. package/config-validation/ConfigValidatorLive.js +61 -0
  11. package/dts/resolved-tsconfig.js +44 -0
  12. package/entry/extract.js +68 -0
  13. package/entry/package-json-entries.js +12 -0
  14. package/errors.js +36 -0
  15. package/exe/build.js +23 -0
  16. package/exe/config.js +50 -0
  17. package/index.d.ts +1457 -0
  18. package/index.js +44 -0
  19. package/jsx/config.js +40 -0
  20. package/manifest/emit-manifest.js +56 -0
  21. package/manifest/transform.js +123 -0
  22. package/meta/api-extractor.js +59 -0
  23. package/meta/config.js +14 -0
  24. package/meta/generate.js +76 -0
  25. package/meta/merge-models.js +44 -0
  26. package/meta/message-suppressor.js +37 -0
  27. package/meta/tsconfig-resolver.js +260 -0
  28. package/meta/tsdoc-config.js +47 -0
  29. package/package.json +45 -0
  30. package/report/formatters/ci-annotations.js +20 -0
  31. package/report/formatters/json.js +12 -0
  32. package/report/formatters/markdown.js +25 -0
  33. package/report/formatters/silent.js +8 -0
  34. package/report/formatters/terminal.js +29 -0
  35. package/report/layers/EnvironmentDetectorLive.js +15 -0
  36. package/report/layers/ExecutorResolverLive.js +8 -0
  37. package/report/layers/FormatSelectorLive.js +8 -0
  38. package/report/layers/OutputRendererLive.js +23 -0
  39. package/report/pipeline.js +25 -0
  40. package/report/schema-export.js +18 -0
  41. package/report/schema.js +19 -0
  42. package/report/services/EnvironmentDetector.js +7 -0
  43. package/report/services/ExecutorResolver.js +7 -0
  44. package/report/services/FormatSelector.js +7 -0
  45. package/report/services/OutputRenderer.js +7 -0
  46. package/report/timer.js +15 -0
  47. package/targets/binding.js +15 -0
  48. package/targets/config.js +8 -0
  49. 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,8 @@
1
+ //#region src/report/formatters/silent.ts
2
+ const SilentFormatter = {
3
+ format: "silent",
4
+ render: () => []
5
+ };
6
+
7
+ //#endregion
8
+ export { SilentFormatter };
@@ -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 };
@@ -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 };
@@ -0,0 +1,7 @@
1
+ import { Context } from "effect";
2
+
3
+ //#region src/report/services/EnvironmentDetector.ts
4
+ var EnvironmentDetector = class extends Context.Tag("@savvy-web/tsdown-plugins/EnvironmentDetector")() {};
5
+
6
+ //#endregion
7
+ export { EnvironmentDetector };
@@ -0,0 +1,7 @@
1
+ import { Context } from "effect";
2
+
3
+ //#region src/report/services/ExecutorResolver.ts
4
+ var ExecutorResolver = class extends Context.Tag("@savvy-web/tsdown-plugins/ExecutorResolver")() {};
5
+
6
+ //#endregion
7
+ export { ExecutorResolver };
@@ -0,0 +1,7 @@
1
+ import { Context } from "effect";
2
+
3
+ //#region src/report/services/FormatSelector.ts
4
+ var FormatSelector = class extends Context.Tag("@savvy-web/tsdown-plugins/FormatSelector")() {};
5
+
6
+ //#endregion
7
+ export { FormatSelector };
@@ -0,0 +1,7 @@
1
+ import { Context } from "effect";
2
+
3
+ //#region src/report/services/OutputRenderer.ts
4
+ var OutputRenderer = class extends Context.Tag("@savvy-web/tsdown-plugins/OutputRenderer")() {};
5
+
6
+ //#endregion
7
+ export { OutputRenderer };
@@ -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 };
@@ -0,0 +1,8 @@
1
+ //#region src/targets/config.ts
2
+ /** True when a target value is the object form (carries registry/name/from). */
3
+ function isTargetObject(value) {
4
+ return typeof value === "object" && value !== null;
5
+ }
6
+
7
+ //#endregion
8
+ export { isTargetObject };