@savvy-web/silk-effects 0.6.1 → 1.0.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 (155) hide show
  1. package/README.md +48 -17
  2. package/_virtual/_rolldown/runtime.js +18 -0
  3. package/changesets/api/categories.js +247 -0
  4. package/changesets/api/changelog.js +134 -0
  5. package/changesets/api/dependency-table.js +163 -0
  6. package/changesets/api/linter.js +168 -0
  7. package/changesets/api/transformer.js +140 -0
  8. package/changesets/categories/index.js +299 -0
  9. package/changesets/categories/types.js +66 -0
  10. package/changesets/changelog/formatting.js +119 -0
  11. package/changesets/changelog/getDependencyReleaseLine.js +114 -0
  12. package/changesets/changelog/getReleaseLine.js +122 -0
  13. package/changesets/changelog/index.js +99 -0
  14. package/changesets/constants.js +43 -0
  15. package/changesets/errors.js +305 -0
  16. package/changesets/index.js +146 -0
  17. package/changesets/markdownlint/index.js +29 -0
  18. package/changesets/markdownlint/rules/content-structure.js +98 -0
  19. package/changesets/markdownlint/rules/dependency-table-format.js +170 -0
  20. package/changesets/markdownlint/rules/heading-hierarchy.js +61 -0
  21. package/changesets/markdownlint/rules/required-sections.js +54 -0
  22. package/changesets/markdownlint/rules/uncategorized-content.js +54 -0
  23. package/changesets/markdownlint/rules/utils.js +30 -0
  24. package/changesets/remark/plugins/aggregate-dependency-tables.js +47 -0
  25. package/changesets/remark/plugins/contributor-footnotes.js +123 -0
  26. package/changesets/remark/plugins/deduplicate-items.js +30 -0
  27. package/changesets/remark/plugins/issue-link-refs.js +58 -0
  28. package/changesets/remark/plugins/merge-sections.js +43 -0
  29. package/changesets/remark/plugins/normalize-format.js +47 -0
  30. package/changesets/remark/plugins/reorder-sections.js +34 -0
  31. package/changesets/remark/presets.js +119 -0
  32. package/changesets/remark/rules/content-structure.js +22 -0
  33. package/changesets/remark/rules/dependency-table-format.js +40 -0
  34. package/changesets/remark/rules/heading-hierarchy.js +19 -0
  35. package/changesets/remark/rules/required-sections.js +17 -0
  36. package/changesets/remark/rules/uncategorized-content.js +31 -0
  37. package/changesets/schemas/changeset.js +146 -0
  38. package/changesets/schemas/dependency-table.js +189 -0
  39. package/changesets/schemas/git.js +69 -0
  40. package/changesets/schemas/github.js +175 -0
  41. package/changesets/schemas/options.js +182 -0
  42. package/changesets/schemas/package-scope.js +128 -0
  43. package/changesets/schemas/primitives.js +72 -0
  44. package/changesets/schemas/version-files.js +151 -0
  45. package/changesets/services/branch-analyzer.js +278 -0
  46. package/changesets/services/changelog.js +50 -0
  47. package/changesets/services/config-inspector.js +390 -0
  48. package/changesets/services/github.js +178 -0
  49. package/changesets/services/markdown.js +106 -0
  50. package/changesets/services/workspace-snapshot.js +182 -0
  51. package/changesets/utils/commit-parser.js +80 -0
  52. package/changesets/utils/dep-diff.js +77 -0
  53. package/changesets/utils/dependency-table.js +347 -0
  54. package/changesets/utils/issue-refs.js +101 -0
  55. package/changesets/utils/jsonpath.js +175 -0
  56. package/changesets/utils/logger.js +50 -0
  57. package/changesets/utils/markdown-link.js +57 -0
  58. package/changesets/utils/publishability.js +39 -0
  59. package/changesets/utils/remark-pipeline.js +79 -0
  60. package/changesets/utils/section-parser.js +94 -0
  61. package/changesets/utils/strip-frontmatter.js +46 -0
  62. package/changesets/utils/version-blocks.js +108 -0
  63. package/changesets/utils/version-files.js +336 -0
  64. package/changesets/utils/worktree-snapshot.js +142 -0
  65. package/changesets/vendor/github-info.js +55 -0
  66. package/commitlint/config/factory.js +69 -0
  67. package/commitlint/config/plugins.js +227 -0
  68. package/commitlint/config/rules.js +155 -0
  69. package/commitlint/config/schema.js +46 -0
  70. package/commitlint/detection/dco.js +53 -0
  71. package/commitlint/detection/scopes.js +45 -0
  72. package/commitlint/formatter/format.js +85 -0
  73. package/commitlint/formatter/messages.js +79 -0
  74. package/commitlint/hook/diagnostics/branch.js +36 -0
  75. package/commitlint/hook/diagnostics/cache.js +37 -0
  76. package/commitlint/hook/diagnostics/commitlint-config.js +36 -0
  77. package/commitlint/hook/diagnostics/open-issues.js +56 -0
  78. package/commitlint/hook/diagnostics/package-manager.js +51 -0
  79. package/commitlint/hook/diagnostics/signing.js +107 -0
  80. package/commitlint/hook/envelope.js +46 -0
  81. package/commitlint/hook/output.js +45 -0
  82. package/commitlint/hook/parse-bash-command.js +105 -0
  83. package/commitlint/hook/rules/closes-trailer.js +31 -0
  84. package/commitlint/hook/rules/forbidden-content.js +32 -0
  85. package/commitlint/hook/rules/plan-leakage.js +36 -0
  86. package/commitlint/hook/rules/signing-flag-conflict.js +25 -0
  87. package/commitlint/hook/rules/soft-wrap.js +37 -0
  88. package/commitlint/hook/rules/types.js +14 -0
  89. package/commitlint/hook/rules/verbosity.js +31 -0
  90. package/commitlint/hook/silence-logger.js +39 -0
  91. package/commitlint/index.js +146 -0
  92. package/commitlint/prompt/config.js +91 -0
  93. package/commitlint/prompt/emojis.js +74 -0
  94. package/commitlint/prompt/prompter.js +135 -0
  95. package/commitlint/static.js +73 -0
  96. package/errors/BiomeSyncError.js +21 -0
  97. package/errors/ChangesetConfigError.js +20 -0
  98. package/errors/ConfigNotFoundError.js +21 -0
  99. package/errors/SectionParseError.js +16 -0
  100. package/errors/SectionValidationError.js +16 -0
  101. package/errors/SectionWriteError.js +16 -0
  102. package/errors/TagFormatError.js +20 -0
  103. package/errors/ToolNotFoundError.js +11 -0
  104. package/errors/ToolResolutionError.js +11 -0
  105. package/errors/ToolVersionMismatchError.js +11 -0
  106. package/errors/VersioningDetectionError.js +20 -0
  107. package/errors/WorkspaceAnalysisError.js +21 -0
  108. package/index.d.ts +9743 -8380
  109. package/index.js +36 -6657
  110. package/lint/Handler.js +39 -0
  111. package/lint/cli/sections.js +65 -0
  112. package/lint/cli/templates/markdownlint.gen.js +183 -0
  113. package/lint/config/Preset.js +152 -0
  114. package/lint/config/createConfig.js +89 -0
  115. package/lint/handlers/Biome.js +179 -0
  116. package/lint/handlers/Markdown.js +139 -0
  117. package/lint/handlers/PackageJson.js +130 -0
  118. package/lint/handlers/PnpmWorkspace.js +141 -0
  119. package/lint/handlers/ShellScripts.js +58 -0
  120. package/lint/handlers/TypeScript.js +134 -0
  121. package/lint/handlers/Yaml.js +167 -0
  122. package/lint/index.js +52 -0
  123. package/lint/utils/Command.js +285 -0
  124. package/lint/utils/Filter.js +100 -0
  125. package/lint/utils/Workspace.js +86 -0
  126. package/package.json +52 -63
  127. package/schemas/CommentStyle.js +16 -0
  128. package/schemas/ResolvedTool.js +63 -0
  129. package/schemas/SavvySections.js +113 -0
  130. package/schemas/SectionBlock.js +70 -0
  131. package/schemas/SectionDefinition.js +121 -0
  132. package/schemas/SectionResults.js +12 -0
  133. package/schemas/TagStrategySchemas.js +18 -0
  134. package/schemas/ToolDefinition.js +39 -0
  135. package/schemas/ToolResults.js +14 -0
  136. package/schemas/VersioningSchemas.js +95 -0
  137. package/schemas/WorkspaceAnalysisSchemas.js +190 -0
  138. package/services/BiomeSchemaSync.js +133 -0
  139. package/services/ChangesetConfig.js +78 -0
  140. package/services/ChangesetConfigReader.js +106 -0
  141. package/services/ConfigDiscovery.js +71 -0
  142. package/services/ManagedSection.js +288 -0
  143. package/services/SilkPublishability.js +193 -0
  144. package/services/SilkWorkspaceAnalyzer.js +213 -0
  145. package/services/TagStrategy.js +54 -0
  146. package/services/ToolDiscovery.js +229 -0
  147. package/services/VersioningStrategy.js +67 -0
  148. package/tsdoc-metadata.json +11 -11
  149. package/turbo/digest.js +127 -0
  150. package/turbo/errors.js +48 -0
  151. package/turbo/index.js +32 -0
  152. package/turbo/schemas/DryRun.js +57 -0
  153. package/turbo/schemas/results.js +61 -0
  154. package/turbo/services/TurboInspector.js +100 -0
  155. package/utils/ToolCommand.js +40 -0
@@ -0,0 +1,288 @@
1
+ import { SectionParseError } from "../errors/SectionParseError.js";
2
+ import { SectionWriteError } from "../errors/SectionWriteError.js";
3
+ import { CheckResult, SyncResult } from "../schemas/SectionResults.js";
4
+ import { SectionBlock } from "../schemas/SectionBlock.js";
5
+ import { Context, Effect, Equal, Function, Layer } from "effect";
6
+ import { FileSystem } from "@effect/platform";
7
+
8
+ //#region src/services/ManagedSection.ts
9
+ function beginMarker(toolName, commentStyle) {
10
+ return `${commentStyle} --- BEGIN ${toolName.toUpperCase()} MANAGED SECTION ---`;
11
+ }
12
+ function endMarker(toolName, commentStyle) {
13
+ return `${commentStyle} --- END ${toolName.toUpperCase()} MANAGED SECTION ---`;
14
+ }
15
+ function parseContent(content, toolName, commentStyle) {
16
+ const begin = beginMarker(toolName, commentStyle);
17
+ const end = endMarker(toolName, commentStyle);
18
+ const beginIndex = content.indexOf(begin);
19
+ const endIndex = content.indexOf(end);
20
+ if (beginIndex === -1 || endIndex === -1 || endIndex <= beginIndex) return null;
21
+ let managed = content.slice(beginIndex + begin.length, endIndex);
22
+ if (managed.startsWith("\n")) managed = managed.slice(1);
23
+ if (managed.endsWith("\n")) managed = managed.slice(0, -1);
24
+ return {
25
+ before: content.slice(0, beginIndex),
26
+ managed,
27
+ after: content.slice(endIndex + end.length)
28
+ };
29
+ }
30
+ function assembleContent(before, managed, after, toolName, commentStyle) {
31
+ return `${before}${beginMarker(toolName, commentStyle)}\n${managed}\n${endMarker(toolName, commentStyle)}${after}`;
32
+ }
33
+ const BEGIN_MARKER_RE = /^(#|\/\/) --- BEGIN (.+?) MANAGED SECTION ---$/gm;
34
+ function sectionKey(toolName, commentStyle) {
35
+ return `${toolName.toUpperCase()}::${commentStyle}`;
36
+ }
37
+ /**
38
+ * Locate every managed section in `content`, in document order, across all tools and
39
+ * comment styles. Unterminated begin markers are skipped.
40
+ */
41
+ function findAllSections(content) {
42
+ const results = [];
43
+ for (const match of content.matchAll(BEGIN_MARKER_RE)) {
44
+ const style = match[1];
45
+ const name = match[2];
46
+ const beginStart = match.index;
47
+ const beginEnd = beginStart + match[0].length;
48
+ const end = `${style} --- END ${name} MANAGED SECTION ---`;
49
+ const endIdx = content.indexOf(end, beginEnd);
50
+ if (endIdx === -1) continue;
51
+ let inner = content.slice(beginEnd, endIdx);
52
+ if (inner.startsWith("\n")) inner = inner.slice(1);
53
+ if (inner.endsWith("\n")) inner = inner.slice(0, -1);
54
+ results.push({
55
+ key: sectionKey(name, style),
56
+ commentStyle: style,
57
+ content: inner,
58
+ raw: content.slice(beginStart, endIdx + end.length),
59
+ start: beginStart,
60
+ end: endIdx + end.length
61
+ });
62
+ }
63
+ return results;
64
+ }
65
+ /**
66
+ * Service for managing delimited sections in user-editable files.
67
+ *
68
+ * All methods use dual API (data-first and data-last).
69
+ * Identity-only operations (`read`, `isManaged`) take a {@link SectionDefinition}.
70
+ * Content operations (`write`, `sync`, `check`) take a {@link SectionBlock}.
71
+ *
72
+ * @since 0.2.0
73
+ */
74
+ var ManagedSection = class extends Context.Tag("@savvy-web/silk-effects/ManagedSection")() {};
75
+ /**
76
+ * Live implementation of {@link ManagedSection} backed by `@effect/platform` FileSystem.
77
+ *
78
+ * @since 0.2.0
79
+ */
80
+ const ManagedSectionLive = Layer.effect(ManagedSection, Effect.gen(function* () {
81
+ const fs = yield* FileSystem.FileSystem;
82
+ const read = Function.dual(2, (path, definition) => Effect.gen(function* () {
83
+ if (!(yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)))) return null;
84
+ const parsed = parseContent(yield* fs.readFileString(path).pipe(Effect.mapError((cause) => new SectionParseError({
85
+ path,
86
+ reason: String(cause)
87
+ }))), definition.toolName, definition.commentStyle);
88
+ if (parsed === null) return null;
89
+ return SectionBlock.make({
90
+ toolName: definition.toolName,
91
+ commentStyle: definition.commentStyle,
92
+ content: parsed.managed
93
+ });
94
+ }));
95
+ const isManaged = Function.dual(2, (path, definition) => Effect.gen(function* () {
96
+ if (!(yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)))) return false;
97
+ const raw = yield* fs.readFileString(path).pipe(Effect.orElseSucceed(() => ""));
98
+ const begin = beginMarker(definition.toolName, definition.commentStyle);
99
+ const end = endMarker(definition.toolName, definition.commentStyle);
100
+ const beginIdx = raw.indexOf(begin);
101
+ const endIdx = raw.indexOf(end);
102
+ return beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx;
103
+ }));
104
+ const write = Function.dual(2, (path, block) => Effect.gen(function* () {
105
+ const exists = yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false));
106
+ let fileContent;
107
+ if (exists) {
108
+ const raw = yield* fs.readFileString(path).pipe(Effect.mapError((cause) => new SectionWriteError({
109
+ path,
110
+ reason: String(cause)
111
+ })));
112
+ const parsed = parseContent(raw, block.toolName, block.commentStyle);
113
+ if (parsed !== null) fileContent = assembleContent(parsed.before, block.content, parsed.after, block.toolName, block.commentStyle);
114
+ else {
115
+ const trimmed = raw.trimEnd();
116
+ const begin = beginMarker(block.toolName, block.commentStyle);
117
+ const end = endMarker(block.toolName, block.commentStyle);
118
+ fileContent = `${trimmed}\n\n${begin}\n${block.content}\n${end}\n`;
119
+ }
120
+ } else {
121
+ const begin = beginMarker(block.toolName, block.commentStyle);
122
+ const end = endMarker(block.toolName, block.commentStyle);
123
+ fileContent = `${begin}\n${block.content}\n${end}\n`;
124
+ }
125
+ yield* fs.writeFileString(path, fileContent).pipe(Effect.mapError((cause) => new SectionWriteError({
126
+ path,
127
+ reason: String(cause)
128
+ })));
129
+ }));
130
+ return {
131
+ read,
132
+ write,
133
+ isManaged,
134
+ sync: Function.dual(2, (path, block) => Effect.gen(function* () {
135
+ const onDisk = yield* read(path, {
136
+ toolName: block.toolName,
137
+ commentStyle: block.commentStyle
138
+ }).pipe(Effect.mapError((cause) => new SectionWriteError({
139
+ path,
140
+ reason: String(cause)
141
+ })));
142
+ if (onDisk === null) {
143
+ yield* write(path, block);
144
+ return SyncResult.Created();
145
+ }
146
+ if (Equal.equals(onDisk, block)) return SyncResult.Unchanged();
147
+ const d = SectionBlock.diff(onDisk, block);
148
+ yield* write(path, block);
149
+ return SyncResult.Updated({ diff: d });
150
+ })),
151
+ syncMany: Function.dual(2, (path, blocks) => Effect.gen(function* () {
152
+ const original = (yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false))) ? yield* fs.readFileString(path).pipe(Effect.mapError((cause) => new SectionWriteError({
153
+ path,
154
+ reason: String(cause)
155
+ }))) : "";
156
+ const keyOf = (b) => sectionKey(b.toolName, b.commentStyle);
157
+ const found = findAllSections(original);
158
+ const onDiskByKey = /* @__PURE__ */ new Map();
159
+ for (const f of found) if (!onDiskByKey.has(f.key)) onDiskByKey.set(f.key, f);
160
+ const results = blocks.map((block) => {
161
+ const onDisk = onDiskByKey.get(keyOf(block));
162
+ if (onDisk === void 0) return SyncResult.Created();
163
+ const current = SectionBlock.make({
164
+ toolName: block.toolName,
165
+ commentStyle: block.commentStyle,
166
+ content: onDisk.content
167
+ });
168
+ if (Equal.equals(current, block)) return SyncResult.Unchanged();
169
+ return SyncResult.Updated({ diff: SectionBlock.diff(current, block) });
170
+ });
171
+ const items = [];
172
+ let cursor = 0;
173
+ for (const f of found) {
174
+ items.push({
175
+ kind: "text",
176
+ value: original.slice(cursor, f.start)
177
+ });
178
+ items.push({
179
+ kind: "section",
180
+ key: f.key,
181
+ raw: f.raw,
182
+ render: null
183
+ });
184
+ cursor = f.end;
185
+ }
186
+ items.push({
187
+ kind: "text",
188
+ value: original.slice(cursor)
189
+ });
190
+ const targetKeys = new Set(blocks.map(keyOf));
191
+ const slotIndices = [];
192
+ items.forEach((item, idx) => {
193
+ if (item.kind === "section" && targetKeys.has(item.key)) slotIndices.push(idx);
194
+ });
195
+ const itemIndexByDeclared = /* @__PURE__ */ new Map();
196
+ let slotCursor = 0;
197
+ blocks.forEach((block, declaredIdx) => {
198
+ if (!onDiskByKey.has(keyOf(block))) return;
199
+ if (slotCursor >= slotIndices.length) return;
200
+ const itemIdx = slotIndices[slotCursor];
201
+ const item = items[itemIdx];
202
+ if (item.kind === "section") item.render = block;
203
+ itemIndexByDeclared.set(declaredIdx, itemIdx);
204
+ slotCursor += 1;
205
+ });
206
+ const beforeAnchor = /* @__PURE__ */ new Map();
207
+ const afterAnchor = /* @__PURE__ */ new Map();
208
+ const appendList = [];
209
+ const pushInto = (map, anchor, block) => {
210
+ const arr = map.get(anchor) ?? [];
211
+ arr.push(block);
212
+ map.set(anchor, arr);
213
+ };
214
+ blocks.forEach((block, i) => {
215
+ if (onDiskByKey.has(keyOf(block))) return;
216
+ let placed = false;
217
+ for (let j = i + 1; j < blocks.length && !placed; j += 1) {
218
+ const itemIdx = itemIndexByDeclared.get(j);
219
+ if (itemIdx !== void 0) {
220
+ pushInto(beforeAnchor, itemIdx, block);
221
+ placed = true;
222
+ }
223
+ }
224
+ for (let j = i - 1; j >= 0 && !placed; j -= 1) {
225
+ const itemIdx = itemIndexByDeclared.get(j);
226
+ if (itemIdx !== void 0) {
227
+ pushInto(afterAnchor, itemIdx, block);
228
+ placed = true;
229
+ }
230
+ }
231
+ if (!placed) appendList.push(block);
232
+ });
233
+ const out = [];
234
+ items.forEach((item, idx) => {
235
+ if (item.kind === "text") {
236
+ out.push(item.value);
237
+ return;
238
+ }
239
+ for (const block of beforeAnchor.get(idx) ?? []) out.push(block.rendered, "\n\n");
240
+ out.push(item.render === null ? item.raw : item.render.rendered);
241
+ for (const block of afterAnchor.get(idx) ?? []) out.push("\n\n", block.rendered);
242
+ });
243
+ let output = out.join("");
244
+ for (const block of appendList) output = output.trim() === "" ? `${block.rendered}\n` : `${output.replace(/\n+$/, "")}\n\n${block.rendered}\n`;
245
+ if (output !== "" && !output.endsWith("\n")) output += "\n";
246
+ if (output !== original) yield* fs.writeFileString(path, output).pipe(Effect.mapError((cause) => new SectionWriteError({
247
+ path,
248
+ reason: String(cause)
249
+ })));
250
+ return results;
251
+ })),
252
+ check: Function.dual(2, (path, block) => Effect.gen(function* () {
253
+ const onDisk = yield* read(path, {
254
+ toolName: block.toolName,
255
+ commentStyle: block.commentStyle
256
+ });
257
+ if (onDisk === null) return CheckResult.NotFound();
258
+ const isUpToDate = Equal.equals(onDisk, block);
259
+ const d = SectionBlock.diff(onDisk, block);
260
+ return CheckResult.Found({
261
+ isUpToDate,
262
+ diff: d
263
+ });
264
+ })),
265
+ remove: Function.dual(2, (path, definition) => Effect.gen(function* () {
266
+ if (!(yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)))) return false;
267
+ const parsed = parseContent(yield* fs.readFileString(path).pipe(Effect.mapError((cause) => new SectionWriteError({
268
+ path,
269
+ reason: String(cause)
270
+ }))), definition.toolName, definition.commentStyle);
271
+ if (parsed === null) return false;
272
+ const before = parsed.before.replace(/\n+$/, "");
273
+ const after = parsed.after.replace(/^\n+/, "");
274
+ let next;
275
+ if (before !== "" && after !== "") next = `${before}\n\n${after}`;
276
+ else if (before !== "") next = `${before}\n`;
277
+ else next = after;
278
+ yield* fs.writeFileString(path, next).pipe(Effect.mapError((cause) => new SectionWriteError({
279
+ path,
280
+ reason: String(cause)
281
+ })));
282
+ return true;
283
+ }))
284
+ };
285
+ }));
286
+
287
+ //#endregion
288
+ export { ManagedSection, ManagedSectionLive };
@@ -0,0 +1,193 @@
1
+ import { ChangesetConfig } from "./ChangesetConfig.js";
2
+ import { Effect, Layer } from "effect";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { PublishTarget, PublishabilityDetector, PublishabilityDetectorLive, WorkspaceDiscovery } from "workspaces-effect";
5
+ import { FileSystem } from "@effect/platform";
6
+
7
+ //#region src/services/SilkPublishability.ts
8
+ const NPM_DEFAULT = "https://registry.npmjs.org/";
9
+ /**
10
+ * Default registry endpoints for the well-known target keys. Kept in sync with the
11
+ * bundler's `resolveTargets` so the pre-build fallback matches what the binding will carry
12
+ * (no trailing slash).
13
+ */
14
+ const DEFAULT_REGISTRIES = {
15
+ npm: "https://registry.npmjs.org",
16
+ github: "https://npm.pkg.github.com"
17
+ };
18
+ /**
19
+ * Silk publishability rules over `workspaces-effect`'s {@link PublishTarget}.
20
+ *
21
+ * @remarks
22
+ * In silk mode `private: true` is the norm on workspace `package.json`; publishability is
23
+ * derived from `publishConfig`, with the `private` flag consulted only as a last-resort
24
+ * default. All helpers are static so a consumer sees the full rule surface in one place.
25
+ *
26
+ * @since 0.4.0
27
+ */
28
+ var SilkPublishability = class {
29
+ /**
30
+ * Apply silk publishability rules to a raw `package.json` and the bundler's resolved
31
+ * target binding. Targets-first precedence:
32
+ *
33
+ * - A non-empty `publishConfig.targets` map (the bundler's Record-map form) makes the
34
+ * package publishable regardless of `private`. With a `binding` (post-prod-build), one
35
+ * {@link PublishTarget} is emitted per resolved registry target, its `directory` set to
36
+ * the bound group's `dist/prod/<group>/pkg` dir. Without a binding (pre-build), one
37
+ * placeholder target is emitted per declared key so publishability and target counts
38
+ * are correct; the directory is best-effort and unused until the build writes the
39
+ * binding.
40
+ * - Else `publishConfig.access` → one target at `publishConfig.directory`.
41
+ * - Else `private !== true` → one default public target.
42
+ * - Else `[]`.
43
+ *
44
+ * @param pkgName - The package's name (the base name for `true`/empty-object targets).
45
+ * @param raw - The raw `package.json` (silk reads the unschematized `publishConfig`).
46
+ * @param binding - The parsed `dist/prod/targets.json` binding, or `null` when the prod
47
+ * build has not run yet. See {@link readTargetsBinding}.
48
+ */
49
+ static detect(pkgName, raw, binding) {
50
+ const pc = raw.publishConfig;
51
+ const targets = pc?.targets;
52
+ if (targets && typeof targets === "object" && !Array.isArray(targets) && Object.keys(targets).length > 0) {
53
+ const access = pc?.access ?? "public";
54
+ if (binding) {
55
+ const dirByGroup = new Map(binding.groups.map((g) => [g.id, g.dir]));
56
+ return binding.targets.map((t) => new PublishTarget({
57
+ name: t.name,
58
+ registry: t.registry,
59
+ directory: dirByGroup.get(t.group) ?? `dist/prod/${t.group}/pkg`,
60
+ access
61
+ }));
62
+ }
63
+ return Object.keys(targets).map((id) => new PublishTarget({
64
+ name: pkgName,
65
+ registry: DEFAULT_REGISTRIES[id] ?? pc?.registry ?? NPM_DEFAULT,
66
+ directory: `dist/prod/${id}/pkg`,
67
+ access
68
+ }));
69
+ }
70
+ if (pc && (pc.access === "public" || pc.access === "restricted")) return [new PublishTarget({
71
+ name: pkgName,
72
+ registry: pc.registry ?? NPM_DEFAULT,
73
+ directory: pc.directory ?? ".",
74
+ access: pc.access
75
+ })];
76
+ if (raw.private !== true) return [new PublishTarget({
77
+ name: pkgName,
78
+ registry: pc?.registry ?? NPM_DEFAULT,
79
+ directory: pc?.directory ?? ".",
80
+ access: pc?.access ?? "public"
81
+ })];
82
+ return [];
83
+ }
84
+ /**
85
+ * Resolve a package's publish targets via {@link PublishabilityDetector}, then drop any
86
+ * whose built `directory` package.json is `private: true`. Returned targets keep the
87
+ * detector's original (possibly package-relative) `directory`.
88
+ */
89
+ static resolveTargets(pkg, root) {
90
+ return Effect.gen(function* () {
91
+ const detector = yield* PublishabilityDetector;
92
+ const fs = yield* FileSystem.FileSystem;
93
+ const targets = yield* detector.detect(pkg, root);
94
+ const kept = [];
95
+ for (const t of targets) if (!(yield* isTargetPrivate(fs, isAbsolute(t.directory) ? t.directory : join(pkg.path, t.directory)))) kept.push(t);
96
+ return kept;
97
+ });
98
+ }
99
+ /**
100
+ * The publishable, non-ignored packages, resolved through the single
101
+ * {@link PublishabilityDetector} (which already honors changeset ignore in adaptive mode).
102
+ */
103
+ static listPublishable(root) {
104
+ return Effect.gen(function* () {
105
+ const discovery = yield* WorkspaceDiscovery;
106
+ const detector = yield* PublishabilityDetector;
107
+ const packages = yield* discovery.listPackages().pipe(Effect.orDie);
108
+ const out = [];
109
+ for (const pkg of packages) {
110
+ const targets = yield* detector.detect(pkg, root);
111
+ if (targets.length > 0) out.push({
112
+ name: pkg.name,
113
+ version: pkg.version,
114
+ path: pkg.path,
115
+ targetCount: targets.length
116
+ });
117
+ }
118
+ return out;
119
+ });
120
+ }
121
+ };
122
+ /** True when a built target directory's package.json is `private: true`. Missing/unreadable/malformed → false. */
123
+ const isTargetPrivate = (fs, targetDir) => fs.readFileString(join(targetDir, "package.json")).pipe(Effect.flatMap((content) => Effect.try({
124
+ try: () => JSON.parse(content).private === true,
125
+ catch: () => /* @__PURE__ */ new Error("invalid package.json")
126
+ })), Effect.orElseSucceed(() => false));
127
+ /** Read a raw `package.json` from disk via FileSystem; missing/unreadable/malformed → null. */
128
+ const readRaw = (fs, packageJsonPath) => fs.readFileString(packageJsonPath).pipe(Effect.flatMap((content) => Effect.try({
129
+ try: () => JSON.parse(content),
130
+ catch: () => /* @__PURE__ */ new Error("invalid package.json")
131
+ })), Effect.orElseSucceed(() => null));
132
+ /**
133
+ * Read the bundler's `<pkgPath>/dist/prod/targets.json` binding via FileSystem.
134
+ *
135
+ * @remarks
136
+ * Returns `null` when the file is missing/unreadable/malformed — i.e. before the prod build
137
+ * has run. {@link SilkPublishability.detect} falls back to declared-key placeholders in that
138
+ * case. Used by the silk {@link PublishabilityDetector} layers and the workspace analyzer.
139
+ *
140
+ * @param fs - The FileSystem service.
141
+ * @param pkgPath - Absolute path to the package directory.
142
+ * @since 1.0.0
143
+ */
144
+ const readTargetsBinding = (fs, pkgPath) => fs.readFileString(join(pkgPath, "dist", "prod", "targets.json")).pipe(Effect.flatMap((content) => Effect.try({
145
+ try: () => JSON.parse(content),
146
+ catch: () => /* @__PURE__ */ new Error("invalid targets.json")
147
+ })), Effect.orElseSucceed(() => null));
148
+ /**
149
+ * Override of `workspaces-effect`'s {@link PublishabilityDetector} Tag with pure silk rules.
150
+ *
151
+ * @remarks Requires `FileSystem` (captured at layer build); `detect` reads the raw
152
+ * `package.json` from `pkg.packageJsonPath` and applies {@link SilkPublishability.detect}.
153
+ *
154
+ * @since 0.4.0
155
+ */
156
+ const SilkPublishabilityDetectorLive = Layer.effect(PublishabilityDetector, Effect.gen(function* () {
157
+ const fs = yield* FileSystem.FileSystem;
158
+ return { detect: (pkg, _root) => Effect.gen(function* () {
159
+ const raw = yield* readRaw(fs, pkg.packageJsonPath);
160
+ if (!raw) return [];
161
+ const binding = yield* readTargetsBinding(fs, pkg.path);
162
+ return SilkPublishability.detect(pkg.name, raw, binding);
163
+ }) };
164
+ }));
165
+ /**
166
+ * Ignore-aware override of {@link PublishabilityDetector}. `detect` short-circuits to `[]`
167
+ * for changeset-ignored packages, then dispatches on {@link ChangesetConfig.mode}:
168
+ * `none` → `[]`; `silk` → {@link SilkPublishability.detect}; `vanilla` → the library default.
169
+ *
170
+ * @remarks Requires `FileSystem` + {@link ChangesetConfig} at build.
171
+ *
172
+ * @since 0.4.0
173
+ */
174
+ const PublishabilityDetectorAdaptiveLive = Layer.effect(PublishabilityDetector, Effect.gen(function* () {
175
+ const fs = yield* FileSystem.FileSystem;
176
+ const config = yield* ChangesetConfig;
177
+ const vanilla = yield* Effect.provide(PublishabilityDetector, PublishabilityDetectorLive);
178
+ return { detect: (pkg, root) => Effect.gen(function* () {
179
+ if (yield* config.isIgnored(pkg.name, root)) return [];
180
+ const mode = yield* config.mode(root);
181
+ if (mode === "none") return [];
182
+ if (mode === "silk") {
183
+ const raw = yield* readRaw(fs, pkg.packageJsonPath);
184
+ if (!raw) return [];
185
+ const binding = yield* readTargetsBinding(fs, pkg.path);
186
+ return SilkPublishability.detect(pkg.name, raw, binding);
187
+ }
188
+ return yield* vanilla.detect(pkg, root);
189
+ }) };
190
+ }));
191
+
192
+ //#endregion
193
+ export { PublishabilityDetectorAdaptiveLive, SilkPublishability, SilkPublishabilityDetectorLive, readTargetsBinding };
@@ -0,0 +1,213 @@
1
+ import { ChangesetConfigReader } from "./ChangesetConfigReader.js";
2
+ import { WorkspaceAnalysisError } from "../errors/WorkspaceAnalysisError.js";
3
+ import { AnalyzedWorkspace, WorkspaceAnalysis } from "../schemas/WorkspaceAnalysisSchemas.js";
4
+ import { ChangesetConfig } from "./ChangesetConfig.js";
5
+ import { SilkPublishability, readTargetsBinding } from "./SilkPublishability.js";
6
+ import { TagStrategy } from "./TagStrategy.js";
7
+ import { VersioningStrategy } from "./VersioningStrategy.js";
8
+ import { Context, Effect, Layer, Option } from "effect";
9
+ import { PackageManagerDetector, TopologicalSorter, WorkspaceDiscovery } from "workspaces-effect";
10
+ import { FileSystem } from "@effect/platform";
11
+
12
+ //#region src/services/SilkWorkspaceAnalyzer.ts
13
+ /**
14
+ * Service that performs a full workspace analysis — discovering packages,
15
+ * detecting publishability, computing versioning/tag strategies, and
16
+ * wiring up fixed/linked release groups.
17
+ *
18
+ * @remarks
19
+ * Orchestrates {@link WorkspaceDiscovery}, {@link PackageManagerDetector},
20
+ * {@link ChangesetConfigReader}, {@link VersioningStrategy}, and
21
+ * {@link TagStrategy} to produce a complete {@link WorkspaceAnalysis} for a
22
+ * given workspace root.
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const result = await Effect.runPromise(
27
+ * Effect.gen(function* () {
28
+ * const analyzer = yield* SilkWorkspaceAnalyzer;
29
+ * return yield* analyzer.analyze("/path/to/monorepo");
30
+ * }).pipe(
31
+ * Effect.provide(SilkWorkspaceAnalyzerLive),
32
+ * // ... provide all transitive layers
33
+ * )
34
+ * );
35
+ * ```
36
+ *
37
+ * @since 0.2.0
38
+ */
39
+ var SilkWorkspaceAnalyzer = class extends Context.Tag("@savvy-web/silk-effects/SilkWorkspaceAnalyzer")() {};
40
+ /**
41
+ * Read the raw package.json from disk as an untyped record.
42
+ *
43
+ * @remarks
44
+ * We read from disk rather than using WorkspacePackage.publishConfig because
45
+ * the upstream PublishConfig schema strips unknown fields (like Silk `targets`).
46
+ * SilkPublishability.detect needs the full raw publishConfig.
47
+ */
48
+ const readRawPkgJson = (fs, packageJsonPath) => fs.readFileString(packageJsonPath).pipe(Effect.flatMap((content) => Effect.try({
49
+ try: () => JSON.parse(content),
50
+ catch: () => new WorkspaceAnalysisError({
51
+ root: packageJsonPath,
52
+ reason: "Invalid JSON in package.json"
53
+ })
54
+ })), Effect.mapError((err) => err instanceof WorkspaceAnalysisError ? err : new WorkspaceAnalysisError({
55
+ root: packageJsonPath,
56
+ reason: `Failed to read package.json: ${String(err)}`
57
+ })));
58
+ /**
59
+ * Determine the release status flags (versioned, tagged, released) for a workspace
60
+ * based on the changeset config.
61
+ *
62
+ * @param pkgName - The package name.
63
+ * @param isPrivate - Whether package.json has `private: true`.
64
+ * @param isPublishable - Whether the package has resolved publish targets.
65
+ * A package with `private: true` + `publishConfig.access` is publishable
66
+ * in Silk convention and should be versioned/tagged like a public package.
67
+ * @param config - The changeset config, or null if no changesets.
68
+ */
69
+ function computeReleaseStatus(pkgName, isPrivate, isPublishable, config) {
70
+ if (config == null) return {
71
+ versioned: false,
72
+ tagged: false,
73
+ released: false
74
+ };
75
+ if ((config.ignore ?? []).some((p) => ChangesetConfig.matches(pkgName, p))) return {
76
+ versioned: false,
77
+ tagged: false,
78
+ released: false
79
+ };
80
+ if (!isPrivate || isPublishable) return {
81
+ versioned: true,
82
+ tagged: true,
83
+ released: true
84
+ };
85
+ const pp = config.privatePackages;
86
+ if (pp === void 0) return {
87
+ versioned: false,
88
+ tagged: false,
89
+ released: false
90
+ };
91
+ if (pp === false) return {
92
+ versioned: false,
93
+ tagged: false,
94
+ released: false
95
+ };
96
+ const versioned = pp.version === true;
97
+ const tagged = pp.tag === true;
98
+ return {
99
+ versioned,
100
+ tagged,
101
+ released: versioned && tagged
102
+ };
103
+ }
104
+ /**
105
+ * Live implementation of {@link SilkWorkspaceAnalyzer}.
106
+ *
107
+ * @remarks
108
+ * Requires {@link WorkspaceDiscovery}, {@link PackageManagerDetector},
109
+ * {@link ChangesetConfigReader}, {@link VersioningStrategy}, and {@link TagStrategy}.
110
+ *
111
+ * @since 0.2.0
112
+ */
113
+ const SilkWorkspaceAnalyzerLive = Layer.effect(SilkWorkspaceAnalyzer, Effect.gen(function* () {
114
+ const fs = yield* FileSystem.FileSystem;
115
+ const discovery = yield* WorkspaceDiscovery;
116
+ const sorter = yield* TopologicalSorter;
117
+ const pmDetector = yield* PackageManagerDetector;
118
+ const configReader = yield* ChangesetConfigReader;
119
+ const versioningStrategy = yield* VersioningStrategy;
120
+ const tagStrategy = yield* TagStrategy;
121
+ const analyze = (root) => Effect.gen(function* () {
122
+ const pm = yield* pmDetector.detect(root).pipe(Effect.mapError((err) => new WorkspaceAnalysisError({
123
+ root,
124
+ reason: `Package manager detection failed: ${String(err)}`
125
+ })));
126
+ const packages = yield* discovery.listPackages(root).pipe(Effect.mapError((err) => new WorkspaceAnalysisError({
127
+ root,
128
+ reason: `Workspace discovery failed: ${String(err)}`
129
+ })));
130
+ const topoOrder = yield* sorter.sort().pipe(Effect.mapError((err) => new WorkspaceAnalysisError({
131
+ root,
132
+ reason: `Cyclic dependency detected: ${String(err)}`
133
+ })));
134
+ const packagesByName = new Map(packages.map((p) => [p.name, p]));
135
+ const reordered = topoOrder.flatMap((name) => {
136
+ const pkg = packagesByName.get(name);
137
+ return pkg ? [pkg] : [];
138
+ });
139
+ const sortedPackages = reordered.length > 0 ? reordered : [...packages];
140
+ const changesetConfigOption = yield* configReader.read(root).pipe(Effect.option);
141
+ const changesetConfig = Option.getOrNull(changesetConfigOption);
142
+ const analyzedWorkspaces = [];
143
+ for (const pkg of sortedPackages) {
144
+ const pkgJson = yield* readRawPkgJson(fs, pkg.packageJsonPath);
145
+ const binding = yield* readTargetsBinding(fs, pkg.path);
146
+ const targets = SilkPublishability.detect(pkg.name, pkgJson, binding);
147
+ const isPublishable = targets.length > 0;
148
+ const isRoot = pkg.relativePath === ".";
149
+ const { versioned, tagged, released } = computeReleaseStatus(pkg.name, pkg.private, isPublishable, changesetConfig);
150
+ const analyzed = new AnalyzedWorkspace({
151
+ name: pkg.name,
152
+ version: { current: pkg.version },
153
+ path: pkg.path,
154
+ root: isRoot,
155
+ publishConfig: null,
156
+ publishable: isPublishable,
157
+ targets: [...targets],
158
+ versioned,
159
+ tagged,
160
+ released,
161
+ linked: [],
162
+ fixed: []
163
+ });
164
+ analyzedWorkspaces.push(analyzed);
165
+ }
166
+ if (changesetConfig) {
167
+ const fixedGroups = changesetConfig.fixed ?? [];
168
+ const linkedGroups = changesetConfig.linked ?? [];
169
+ const fixedByName = /* @__PURE__ */ new Map();
170
+ for (const group of fixedGroups) {
171
+ const members = analyzedWorkspaces.filter((w) => group.includes(w.name));
172
+ for (const member of members) fixedByName.set(member.name, members.filter((m) => m !== member));
173
+ }
174
+ const linkedByName = /* @__PURE__ */ new Map();
175
+ for (const group of linkedGroups) {
176
+ const members = analyzedWorkspaces.filter((w) => group.includes(w.name));
177
+ for (const member of members) linkedByName.set(member.name, members.filter((m) => m !== member));
178
+ }
179
+ for (let i = 0; i < analyzedWorkspaces.length; i++) {
180
+ const ws = analyzedWorkspaces[i];
181
+ const fixedRefs = fixedByName.get(ws.name) ?? [];
182
+ const linkedRefs = linkedByName.get(ws.name) ?? [];
183
+ if (fixedRefs.length > 0 || linkedRefs.length > 0) analyzedWorkspaces[i] = new AnalyzedWorkspace({
184
+ ...ws,
185
+ fixed: fixedRefs,
186
+ linked: linkedRefs
187
+ });
188
+ }
189
+ }
190
+ const publishableNames = analyzedWorkspaces.filter((w) => w.publishable).map((w) => w.name);
191
+ const versioning = yield* versioningStrategy.detect(publishableNames, root).pipe(Effect.mapError((err) => new WorkspaceAnalysisError({
192
+ root,
193
+ reason: `Versioning strategy detection failed: ${String(err)}`
194
+ })));
195
+ const tagStrategyType = yield* tagStrategy.determine(versioning);
196
+ return new WorkspaceAnalysis({
197
+ root,
198
+ runtime: pm.runtime,
199
+ packageManager: {
200
+ type: pm.type,
201
+ version: pm.version
202
+ },
203
+ workspaces: analyzedWorkspaces,
204
+ changesetConfig,
205
+ versioning,
206
+ tagStrategy: tagStrategyType
207
+ });
208
+ });
209
+ return { analyze };
210
+ }));
211
+
212
+ //#endregion
213
+ export { SilkWorkspaceAnalyzer, SilkWorkspaceAnalyzerLive };