@savvy-web/silk-effects 0.6.1 → 1.0.1
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 +48 -17
- package/_virtual/_rolldown/runtime.js +18 -0
- package/changesets/api/categories.js +247 -0
- package/changesets/api/changelog.js +134 -0
- package/changesets/api/dependency-table.js +163 -0
- package/changesets/api/linter.js +168 -0
- package/changesets/api/transformer.js +140 -0
- package/changesets/categories/index.js +299 -0
- package/changesets/categories/types.js +66 -0
- package/changesets/changelog/formatting.js +119 -0
- package/changesets/changelog/getDependencyReleaseLine.js +114 -0
- package/changesets/changelog/getReleaseLine.js +122 -0
- package/changesets/changelog/index.js +99 -0
- package/changesets/constants.js +43 -0
- package/changesets/errors.js +305 -0
- package/changesets/index.js +146 -0
- package/changesets/markdownlint/index.js +29 -0
- package/changesets/markdownlint/rules/content-structure.js +98 -0
- package/changesets/markdownlint/rules/dependency-table-format.js +170 -0
- package/changesets/markdownlint/rules/heading-hierarchy.js +61 -0
- package/changesets/markdownlint/rules/required-sections.js +54 -0
- package/changesets/markdownlint/rules/uncategorized-content.js +54 -0
- package/changesets/markdownlint/rules/utils.js +30 -0
- package/changesets/remark/plugins/aggregate-dependency-tables.js +47 -0
- package/changesets/remark/plugins/contributor-footnotes.js +123 -0
- package/changesets/remark/plugins/deduplicate-items.js +30 -0
- package/changesets/remark/plugins/issue-link-refs.js +58 -0
- package/changesets/remark/plugins/merge-sections.js +43 -0
- package/changesets/remark/plugins/normalize-format.js +47 -0
- package/changesets/remark/plugins/reorder-sections.js +34 -0
- package/changesets/remark/presets.js +119 -0
- package/changesets/remark/rules/content-structure.js +22 -0
- package/changesets/remark/rules/dependency-table-format.js +40 -0
- package/changesets/remark/rules/heading-hierarchy.js +19 -0
- package/changesets/remark/rules/required-sections.js +17 -0
- package/changesets/remark/rules/uncategorized-content.js +31 -0
- package/changesets/schemas/changeset.js +146 -0
- package/changesets/schemas/dependency-table.js +189 -0
- package/changesets/schemas/git.js +69 -0
- package/changesets/schemas/github.js +175 -0
- package/changesets/schemas/options.js +182 -0
- package/changesets/schemas/package-scope.js +128 -0
- package/changesets/schemas/primitives.js +72 -0
- package/changesets/schemas/version-files.js +151 -0
- package/changesets/services/branch-analyzer.js +278 -0
- package/changesets/services/changelog.js +50 -0
- package/changesets/services/config-inspector.js +390 -0
- package/changesets/services/github.js +178 -0
- package/changesets/services/markdown.js +106 -0
- package/changesets/services/workspace-snapshot.js +182 -0
- package/changesets/utils/commit-parser.js +80 -0
- package/changesets/utils/dep-diff.js +77 -0
- package/changesets/utils/dependency-table.js +347 -0
- package/changesets/utils/issue-refs.js +101 -0
- package/changesets/utils/jsonpath.js +175 -0
- package/changesets/utils/logger.js +50 -0
- package/changesets/utils/markdown-link.js +57 -0
- package/changesets/utils/publishability.js +39 -0
- package/changesets/utils/remark-pipeline.js +79 -0
- package/changesets/utils/section-parser.js +94 -0
- package/changesets/utils/strip-frontmatter.js +46 -0
- package/changesets/utils/version-blocks.js +108 -0
- package/changesets/utils/version-files.js +336 -0
- package/changesets/utils/worktree-snapshot.js +142 -0
- package/changesets/vendor/github-info.js +55 -0
- package/commitlint/config/factory.js +69 -0
- package/commitlint/config/plugins.js +227 -0
- package/commitlint/config/rules.js +155 -0
- package/commitlint/config/schema.js +46 -0
- package/commitlint/detection/dco.js +53 -0
- package/commitlint/detection/scopes.js +45 -0
- package/commitlint/formatter/format.js +85 -0
- package/commitlint/formatter/messages.js +79 -0
- package/commitlint/hook/diagnostics/branch.js +36 -0
- package/commitlint/hook/diagnostics/cache.js +37 -0
- package/commitlint/hook/diagnostics/commitlint-config.js +36 -0
- package/commitlint/hook/diagnostics/open-issues.js +56 -0
- package/commitlint/hook/diagnostics/package-manager.js +51 -0
- package/commitlint/hook/diagnostics/signing.js +107 -0
- package/commitlint/hook/envelope.js +46 -0
- package/commitlint/hook/output.js +45 -0
- package/commitlint/hook/parse-bash-command.js +105 -0
- package/commitlint/hook/rules/closes-trailer.js +31 -0
- package/commitlint/hook/rules/forbidden-content.js +32 -0
- package/commitlint/hook/rules/plan-leakage.js +36 -0
- package/commitlint/hook/rules/signing-flag-conflict.js +25 -0
- package/commitlint/hook/rules/soft-wrap.js +37 -0
- package/commitlint/hook/rules/types.js +14 -0
- package/commitlint/hook/rules/verbosity.js +31 -0
- package/commitlint/hook/silence-logger.js +39 -0
- package/commitlint/index.js +146 -0
- package/commitlint/prompt/config.js +91 -0
- package/commitlint/prompt/emojis.js +74 -0
- package/commitlint/prompt/prompter.js +135 -0
- package/commitlint/static.js +73 -0
- package/errors/BiomeSyncError.js +21 -0
- package/errors/ChangesetConfigError.js +20 -0
- package/errors/ConfigNotFoundError.js +21 -0
- package/errors/SectionParseError.js +16 -0
- package/errors/SectionValidationError.js +16 -0
- package/errors/SectionWriteError.js +16 -0
- package/errors/TagFormatError.js +20 -0
- package/errors/ToolNotFoundError.js +11 -0
- package/errors/ToolResolutionError.js +11 -0
- package/errors/ToolVersionMismatchError.js +11 -0
- package/errors/VersioningDetectionError.js +20 -0
- package/errors/WorkspaceAnalysisError.js +21 -0
- package/index.d.ts +9743 -8380
- package/index.js +36 -6657
- package/lint/Handler.js +39 -0
- package/lint/cli/sections.js +65 -0
- package/lint/cli/templates/markdownlint.gen.js +183 -0
- package/lint/config/Preset.js +152 -0
- package/lint/config/createConfig.js +89 -0
- package/lint/handlers/Biome.js +179 -0
- package/lint/handlers/Markdown.js +139 -0
- package/lint/handlers/PackageJson.js +130 -0
- package/lint/handlers/PnpmWorkspace.js +141 -0
- package/lint/handlers/ShellScripts.js +58 -0
- package/lint/handlers/TypeScript.js +134 -0
- package/lint/handlers/Yaml.js +167 -0
- package/lint/index.js +52 -0
- package/lint/utils/Command.js +285 -0
- package/lint/utils/Filter.js +100 -0
- package/lint/utils/Workspace.js +86 -0
- package/package.json +52 -63
- package/schemas/CommentStyle.js +16 -0
- package/schemas/ResolvedTool.js +63 -0
- package/schemas/SavvySections.js +113 -0
- package/schemas/SectionBlock.js +70 -0
- package/schemas/SectionDefinition.js +121 -0
- package/schemas/SectionResults.js +12 -0
- package/schemas/TagStrategySchemas.js +18 -0
- package/schemas/ToolDefinition.js +39 -0
- package/schemas/ToolResults.js +14 -0
- package/schemas/VersioningSchemas.js +95 -0
- package/schemas/WorkspaceAnalysisSchemas.js +190 -0
- package/services/BiomeSchemaSync.js +133 -0
- package/services/ChangesetConfig.js +78 -0
- package/services/ChangesetConfigReader.js +106 -0
- package/services/ConfigDiscovery.js +71 -0
- package/services/ManagedSection.js +288 -0
- package/services/SilkPublishability.js +193 -0
- package/services/SilkWorkspaceAnalyzer.js +213 -0
- package/services/TagStrategy.js +54 -0
- package/services/ToolDiscovery.js +229 -0
- package/services/VersioningStrategy.js +67 -0
- package/tsdoc-metadata.json +11 -11
- package/turbo/digest.js +127 -0
- package/turbo/errors.js +48 -0
- package/turbo/index.js +32 -0
- package/turbo/schemas/DryRun.js +57 -0
- package/turbo/schemas/results.js +61 -0
- package/turbo/services/TurboInspector.js +100 -0
- package/utils/ToolCommand.js +40 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { ConfigurationError } from "../errors.js";
|
|
2
|
+
import { ChangesetOptionsSchema } from "../schemas/options.js";
|
|
3
|
+
import { ChangesetConfigReader } from "../../services/ChangesetConfigReader.js";
|
|
4
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
5
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { globSync } from "tinyglobby";
|
|
7
|
+
import { WorkspaceDiscovery } from "workspaces-effect";
|
|
8
|
+
|
|
9
|
+
//#region src/changesets/services/config-inspector.ts
|
|
10
|
+
/**
|
|
11
|
+
* `ConfigInspector` service — surface the project's `.changeset/config.json`
|
|
12
|
+
* in a structured, validated form that agents and tooling can consume.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* The inspector handles three responsibilities that the schemas alone cannot:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Legacy normalization.** Configs from 0.8.x still use the flat
|
|
18
|
+
* top-level `versionFiles[]` array. When that field is populated, the
|
|
19
|
+
* inspector emits a one-line deprecation warning to stderr (naming the
|
|
20
|
+
* config path and the required edit) and folds each legacy entry into
|
|
21
|
+
* the equivalent `packages[entry.package].versionFiles` shape for
|
|
22
|
+
* downstream consumers. Removed in 1.0.0.
|
|
23
|
+
* 2. **Resolution against the workspace.** Each `packages` key must resolve
|
|
24
|
+
* to a known workspace package via `WorkspaceDiscovery`. Unknown keys
|
|
25
|
+
* surface as a {@link ConfigurationError}. The inspector returns the
|
|
26
|
+
* package's absolute workspace directory alongside the resolved scope.
|
|
27
|
+
* 3. **Overlap detection.** Cross-package validation rules that the schemas
|
|
28
|
+
* cannot enforce:
|
|
29
|
+
* - `additionalScopes` of two different packages must not overlap.
|
|
30
|
+
* - `additionalScopes` of one package must not shadow another package's
|
|
31
|
+
* workspace directory.
|
|
32
|
+
* - Two `versionFiles` entries (within or across packages) must not
|
|
33
|
+
* resolve to the same `(file, $.path)` tuple.
|
|
34
|
+
*
|
|
35
|
+
* Validation is performed eagerly on the first {@link ConfigInspectorShape.inspect}
|
|
36
|
+
* call. The resolved state is cached internally per `cwd` so subsequent
|
|
37
|
+
* {@link ConfigInspectorShape.classify} calls reuse it.
|
|
38
|
+
*
|
|
39
|
+
* @see {@link ConfigInspector} for the Effect service tag
|
|
40
|
+
* @see {@link ConfigInspectorLive} for the production layer
|
|
41
|
+
*
|
|
42
|
+
* @packageDocumentation
|
|
43
|
+
*/
|
|
44
|
+
const _tag = Context.Tag("ConfigInspector");
|
|
45
|
+
/**
|
|
46
|
+
* Base class for {@link ConfigInspector}.
|
|
47
|
+
*
|
|
48
|
+
* @privateRemarks
|
|
49
|
+
* Effect's `Context.Tag` creates an anonymous base class that api-extractor
|
|
50
|
+
* cannot follow without an explicit export. Do not delete.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
const ConfigInspectorBase = _tag();
|
|
55
|
+
/**
|
|
56
|
+
* Effect service tag for {@link ConfigInspectorShape}.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* import { Effect } from "effect";
|
|
61
|
+
* import { ConfigInspector, ConfigInspectorLive } from "@savvy-web/changesets";
|
|
62
|
+
*
|
|
63
|
+
* const program = Effect.gen(function* () {
|
|
64
|
+
* const inspector = yield* ConfigInspector;
|
|
65
|
+
* const config = yield* inspector.inspect(process.cwd());
|
|
66
|
+
* return config.packages.map((p) => p.name);
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* Effect.runPromise(program.pipe(Effect.provide(ConfigInspectorLive)));
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
var ConfigInspector = class extends ConfigInspectorBase {};
|
|
75
|
+
/**
|
|
76
|
+
* Pull the changelog formatter ID and its options object out of the raw
|
|
77
|
+
* `.changeset/config.json` shape (where `changelog` may be a tuple, a string,
|
|
78
|
+
* or absent).
|
|
79
|
+
*/
|
|
80
|
+
function extractChangelogOptions(config) {
|
|
81
|
+
const { changelog } = config;
|
|
82
|
+
if (Array.isArray(changelog)) return {
|
|
83
|
+
changelogId: typeof changelog[0] === "string" ? changelog[0] : null,
|
|
84
|
+
options: changelog[1] ?? {}
|
|
85
|
+
};
|
|
86
|
+
if (typeof changelog === "string") return {
|
|
87
|
+
changelogId: changelog,
|
|
88
|
+
options: {}
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
changelogId: null,
|
|
92
|
+
options: {}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Normalize the deprecated top-level `versionFiles[]` array into the
|
|
97
|
+
* equivalent `packages[entry.package].versionFiles` shape. Mutates a fresh
|
|
98
|
+
* copy of the options; never the input.
|
|
99
|
+
*
|
|
100
|
+
* Returns the normalized options and a flag indicating whether normalization
|
|
101
|
+
* happened (so the caller can emit a deprecation warning once per inspect).
|
|
102
|
+
*/
|
|
103
|
+
function normalizeLegacyOptions(options, configPath) {
|
|
104
|
+
const legacy = options.versionFiles;
|
|
105
|
+
if (!Array.isArray(legacy) || legacy.length === 0) return {
|
|
106
|
+
normalized: options,
|
|
107
|
+
legacyUsed: false
|
|
108
|
+
};
|
|
109
|
+
console.warn(`[changesets] DEPRECATION: ${configPath} uses the top-level \`versionFiles\` array. Migrate to \`changelog[1].packages[<name>].versionFiles\`. Removed in 1.0.0.`);
|
|
110
|
+
const existing = options.packages ?? {};
|
|
111
|
+
const next = {};
|
|
112
|
+
for (const [k, v] of Object.entries(existing)) next[k] = { ...v };
|
|
113
|
+
for (const entry of legacy) {
|
|
114
|
+
const ownerName = typeof entry.package === "string" ? entry.package : void 0;
|
|
115
|
+
if (!ownerName) throw new ConfigurationError({
|
|
116
|
+
field: "versionFiles",
|
|
117
|
+
reason: `Legacy versionFiles entry { glob: ${JSON.stringify(entry.glob)} } in ${configPath} has no \`package\` field. Path-based owner inference is removed during the 0.9.0 migration — add an explicit \`package\` field, or migrate the entry to \`packages[<name>].versionFiles\`.`
|
|
118
|
+
});
|
|
119
|
+
if (!next[ownerName]) next[ownerName] = {};
|
|
120
|
+
const slot = next[ownerName];
|
|
121
|
+
const arr = Array.isArray(slot.versionFiles) ? slot.versionFiles.slice() : [];
|
|
122
|
+
const cleaned = { glob: entry.glob };
|
|
123
|
+
if (Array.isArray(entry.paths)) cleaned.paths = entry.paths;
|
|
124
|
+
arr.push(cleaned);
|
|
125
|
+
slot.versionFiles = arr;
|
|
126
|
+
}
|
|
127
|
+
const normalized = {
|
|
128
|
+
...options,
|
|
129
|
+
packages: next
|
|
130
|
+
};
|
|
131
|
+
delete normalized.versionFiles;
|
|
132
|
+
return {
|
|
133
|
+
normalized,
|
|
134
|
+
legacyUsed: true
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Materialize a glob against `cwd` and return the matched file paths as
|
|
139
|
+
* repo-relative strings. Honors negation patterns and ignores `node_modules`.
|
|
140
|
+
*/
|
|
141
|
+
function materializeGlob(glob, cwd) {
|
|
142
|
+
return globSync(glob, {
|
|
143
|
+
cwd,
|
|
144
|
+
ignore: ["**/node_modules/**"],
|
|
145
|
+
dot: true
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Determine whether `child` is the same directory as `parent` or sits inside
|
|
150
|
+
* it. Both paths must be absolute.
|
|
151
|
+
*/
|
|
152
|
+
function isInside(parent, child) {
|
|
153
|
+
const rel = relative(parent, child);
|
|
154
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Build per-package resolved scopes by intersecting the (possibly normalized)
|
|
158
|
+
* options with workspace info.
|
|
159
|
+
*/
|
|
160
|
+
function buildResolvedScopes(params) {
|
|
161
|
+
const { options, workspaces, projectDir, configPath } = params;
|
|
162
|
+
const packages = options.packages ?? {};
|
|
163
|
+
const workspacesByName = new Map(workspaces.map((w) => [w.name, w]));
|
|
164
|
+
const scopes = [];
|
|
165
|
+
for (const [pkgName, scope] of Object.entries(packages)) {
|
|
166
|
+
const ws = workspacesByName.get(pkgName);
|
|
167
|
+
if (!ws) throw new ConfigurationError({
|
|
168
|
+
field: `packages["${pkgName}"]`,
|
|
169
|
+
reason: `Unknown package "${pkgName}" in ${configPath}. Known workspace packages: ${workspaces.map((w) => w.name).join(", ") || "(none)"}.`
|
|
170
|
+
});
|
|
171
|
+
const additionalScopes = scope.additionalScopes ?? [];
|
|
172
|
+
const additionalScopeFiles = additionalScopes.flatMap((g) => materializeGlob(g, projectDir));
|
|
173
|
+
const resolvedVersionFiles = (scope.versionFiles ?? []).map((entry) => ({
|
|
174
|
+
glob: entry.glob,
|
|
175
|
+
paths: entry.paths ?? ["$.version"],
|
|
176
|
+
matchedFiles: materializeGlob(entry.glob, projectDir).map((rel) => join(projectDir, rel))
|
|
177
|
+
}));
|
|
178
|
+
scopes.push({
|
|
179
|
+
name: pkgName,
|
|
180
|
+
workspaceDir: ws.path,
|
|
181
|
+
version: ws.version,
|
|
182
|
+
additionalScopes,
|
|
183
|
+
additionalScopeFiles: additionalScopeFiles.map((rel) => join(projectDir, rel)),
|
|
184
|
+
versionFiles: resolvedVersionFiles
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return scopes;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Cross-package validation: no overlap in `additionalScopes`, no shadowing
|
|
191
|
+
* of another workspace package's directory (regardless of whether that
|
|
192
|
+
* package is itself declared in `config.packages`), and no duplicate
|
|
193
|
+
* `(file, JSONPath)` tuples in `versionFiles`.
|
|
194
|
+
*/
|
|
195
|
+
function checkConflicts(scopes, allWorkspaces, projectDir, configPath) {
|
|
196
|
+
const scopeOwner = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const s of scopes) for (const f of s.additionalScopeFiles) {
|
|
198
|
+
const prev = scopeOwner.get(f);
|
|
199
|
+
if (prev && prev !== s.name) throw new ConfigurationError({
|
|
200
|
+
field: `packages["${s.name}"].additionalScopes`,
|
|
201
|
+
reason: `Overlap in ${configPath}: file ${JSON.stringify(f)} is matched by both "${prev}" and "${s.name}". additionalScopes must not overlap between packages.`
|
|
202
|
+
});
|
|
203
|
+
scopeOwner.set(f, s.name);
|
|
204
|
+
}
|
|
205
|
+
for (const s of scopes) for (const f of s.additionalScopeFiles) for (const ws of allWorkspaces) {
|
|
206
|
+
if (ws.name === s.name) continue;
|
|
207
|
+
if (ws.path === projectDir) continue;
|
|
208
|
+
if (isInside(ws.path, f)) throw new ConfigurationError({
|
|
209
|
+
field: `packages["${s.name}"].additionalScopes`,
|
|
210
|
+
reason: `Shadowing in ${configPath}: "${s.name}" claims ${JSON.stringify(f)} via additionalScopes, but that path is inside "${ws.name}"'s workspace directory (${ws.path}). A package's additionalScopes must not include another package's workspace files.`
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const seen = /* @__PURE__ */ new Map();
|
|
214
|
+
for (const s of scopes) for (const vf of s.versionFiles) for (const file of vf.matchedFiles) for (const path of vf.paths) {
|
|
215
|
+
const key = `${file}::${path}`;
|
|
216
|
+
const prev = seen.get(key);
|
|
217
|
+
if (prev && (prev.pkg !== s.name || prev.glob !== vf.glob)) throw new ConfigurationError({
|
|
218
|
+
field: `packages["${s.name}"].versionFiles`,
|
|
219
|
+
reason: `Conflict in ${configPath}: target (${JSON.stringify(file)}, ${path}) is claimed by both "${prev.pkg}" (glob ${JSON.stringify(prev.glob)}) and "${s.name}" (glob ${JSON.stringify(vf.glob)}). Two versionFiles entries must not resolve to the same (file, JSONPath) tuple.`
|
|
220
|
+
});
|
|
221
|
+
seen.set(key, {
|
|
222
|
+
pkg: s.name,
|
|
223
|
+
glob: vf.glob
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Build a `ConfigurationError` from a Schema `ParseError`. Captures the
|
|
229
|
+
* original message verbatim so the user sees exactly what the schema
|
|
230
|
+
* complained about.
|
|
231
|
+
*/
|
|
232
|
+
function configErrorFromParseError(parseError, configPath) {
|
|
233
|
+
return new ConfigurationError({
|
|
234
|
+
field: "options",
|
|
235
|
+
reason: `Invalid options in ${configPath}: ${String(parseError)}`
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Build a {@link ConfigInspectorShape} that closes over already-resolved
|
|
240
|
+
* service implementations. This keeps the public `inspect`/`classify`
|
|
241
|
+
* signatures requirement-free (`R = never`) while still allowing the
|
|
242
|
+
* implementation to use `ChangesetConfigReader` and `WorkspaceDiscovery`.
|
|
243
|
+
*
|
|
244
|
+
* Each shape carries a private cache keyed by absolute project dir so
|
|
245
|
+
* repeat `inspect`/`classify` calls reuse the materialized state.
|
|
246
|
+
*/
|
|
247
|
+
function makeShape(reader, discovery) {
|
|
248
|
+
const cache = /* @__PURE__ */ new Map();
|
|
249
|
+
const inspect = (cwd) => Effect.gen(function* () {
|
|
250
|
+
const projectDir = resolve(cwd);
|
|
251
|
+
const cached = cache.get(projectDir);
|
|
252
|
+
if (cached) return cached;
|
|
253
|
+
const config = yield* reader.read(projectDir).pipe(Effect.mapError((err) => new ConfigurationError({
|
|
254
|
+
field: "configFile",
|
|
255
|
+
reason: err.message
|
|
256
|
+
})));
|
|
257
|
+
const configPath = join(projectDir, ".changeset", "config.json");
|
|
258
|
+
const { changelogId, options: rawOptions } = extractChangelogOptions(config);
|
|
259
|
+
const optionsRecord = typeof rawOptions === "object" && rawOptions !== null ? rawOptions : {};
|
|
260
|
+
if (Array.isArray(optionsRecord.versionFiles) && typeof optionsRecord.packages === "object" && optionsRecord.packages !== null) return yield* Effect.fail(new ConfigurationError({
|
|
261
|
+
field: "options",
|
|
262
|
+
reason: `Configuration in ${configPath} declares both \`packages\` and the deprecated top-level \`versionFiles\` array. Migrate the legacy entries into \`packages[<name>].versionFiles\` and remove the top-level field.`
|
|
263
|
+
}));
|
|
264
|
+
let normalized;
|
|
265
|
+
let legacyUsed;
|
|
266
|
+
try {
|
|
267
|
+
const result = normalizeLegacyOptions(optionsRecord, configPath);
|
|
268
|
+
normalized = result.normalized;
|
|
269
|
+
legacyUsed = result.legacyUsed;
|
|
270
|
+
} catch (e) {
|
|
271
|
+
if (e instanceof ConfigurationError) return yield* Effect.fail(e);
|
|
272
|
+
throw e;
|
|
273
|
+
}
|
|
274
|
+
const decodedOptions = yield* Schema.decodeUnknown(ChangesetOptionsSchema)(normalized).pipe(Effect.mapError((parseError) => configErrorFromParseError(parseError, configPath)));
|
|
275
|
+
const workspaces = (yield* discovery.listPackages(projectDir).pipe(Effect.mapError((err) => new ConfigurationError({
|
|
276
|
+
field: "workspace",
|
|
277
|
+
reason: `Workspace discovery failed for ${projectDir}: ${err.message}`
|
|
278
|
+
})))).map((w) => ({
|
|
279
|
+
name: w.name,
|
|
280
|
+
path: w.path,
|
|
281
|
+
version: w.version
|
|
282
|
+
}));
|
|
283
|
+
let scopes;
|
|
284
|
+
try {
|
|
285
|
+
scopes = buildResolvedScopes({
|
|
286
|
+
options: decodedOptions,
|
|
287
|
+
workspaces,
|
|
288
|
+
projectDir,
|
|
289
|
+
configPath
|
|
290
|
+
});
|
|
291
|
+
checkConflicts(scopes, workspaces, projectDir, configPath);
|
|
292
|
+
} catch (e) {
|
|
293
|
+
if (e instanceof ConfigurationError) return yield* Effect.fail(e);
|
|
294
|
+
throw e;
|
|
295
|
+
}
|
|
296
|
+
const inspected = {
|
|
297
|
+
configPath,
|
|
298
|
+
projectDir,
|
|
299
|
+
changelog: changelogId,
|
|
300
|
+
baseBranch: config.baseBranch ?? "main",
|
|
301
|
+
access: config.access ?? "restricted",
|
|
302
|
+
ignore: config.ignore ?? [],
|
|
303
|
+
packages: scopes,
|
|
304
|
+
legacyVersionFilesUsed: legacyUsed
|
|
305
|
+
};
|
|
306
|
+
cache.set(projectDir, inspected);
|
|
307
|
+
return inspected;
|
|
308
|
+
});
|
|
309
|
+
const classify = (cwd, paths) => Effect.gen(function* () {
|
|
310
|
+
const inspected = yield* inspect(cwd);
|
|
311
|
+
return paths.map((p) => classifyOne(inspected, p));
|
|
312
|
+
});
|
|
313
|
+
return {
|
|
314
|
+
inspect,
|
|
315
|
+
classify
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Classify a single path against an inspected config.
|
|
320
|
+
*/
|
|
321
|
+
function classifyOne(inspected, path) {
|
|
322
|
+
const abs = resolve(inspected.projectDir, path);
|
|
323
|
+
let bestWorkspace = null;
|
|
324
|
+
for (const s of inspected.packages) if (isInside(s.workspaceDir, abs)) {
|
|
325
|
+
const depth = s.workspaceDir.length;
|
|
326
|
+
if (!bestWorkspace || depth > bestWorkspace.depth) bestWorkspace = {
|
|
327
|
+
pkg: s.name,
|
|
328
|
+
depth
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (bestWorkspace) return {
|
|
332
|
+
path,
|
|
333
|
+
package: bestWorkspace.pkg,
|
|
334
|
+
reason: "workspace"
|
|
335
|
+
};
|
|
336
|
+
for (const s of inspected.packages) if (s.additionalScopeFiles.includes(abs)) {
|
|
337
|
+
const glob = s.additionalScopes.find((g) => materializeGlob(g, inspected.projectDir).map((rel) => join(inspected.projectDir, rel)).includes(abs));
|
|
338
|
+
return {
|
|
339
|
+
path,
|
|
340
|
+
package: s.name,
|
|
341
|
+
reason: {
|
|
342
|
+
kind: "additionalScope",
|
|
343
|
+
glob: glob ?? s.additionalScopes[0] ?? ""
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
for (const s of inspected.packages) for (const vf of s.versionFiles) if (vf.matchedFiles.includes(abs)) return {
|
|
348
|
+
path,
|
|
349
|
+
package: s.name,
|
|
350
|
+
reason: {
|
|
351
|
+
kind: "versionFile",
|
|
352
|
+
glob: vf.glob
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
return {
|
|
356
|
+
path,
|
|
357
|
+
package: null,
|
|
358
|
+
reason: null
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Live layer for {@link ConfigInspector}.
|
|
363
|
+
*
|
|
364
|
+
* Requires {@link ChangesetConfigReader} and {@link WorkspaceDiscovery}
|
|
365
|
+
* in the environment.
|
|
366
|
+
*
|
|
367
|
+
* @public
|
|
368
|
+
*/
|
|
369
|
+
const ConfigInspectorLive = Layer.effect(ConfigInspector, Effect.gen(function* () {
|
|
370
|
+
return makeShape(yield* ChangesetConfigReader, yield* WorkspaceDiscovery);
|
|
371
|
+
}));
|
|
372
|
+
/**
|
|
373
|
+
* Test factory — build a {@link ConfigInspector} that returns a fixed
|
|
374
|
+
* {@link InspectedConfig} without touching the filesystem.
|
|
375
|
+
*
|
|
376
|
+
* Tests that need to exercise the inspect/classify logic against real files
|
|
377
|
+
* should compose `ConfigInspectorLive` with test layers for
|
|
378
|
+
* `ChangesetConfigReader` and `WorkspaceDiscovery` instead.
|
|
379
|
+
*
|
|
380
|
+
* @public
|
|
381
|
+
*/
|
|
382
|
+
function makeConfigInspectorTest(fixed) {
|
|
383
|
+
return Layer.succeed(ConfigInspector, {
|
|
384
|
+
inspect: () => Effect.succeed(fixed),
|
|
385
|
+
classify: (_cwd, paths) => Effect.succeed(paths.map((p) => classifyOne(fixed, p)))
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
//#endregion
|
|
390
|
+
export { ConfigInspector, ConfigInspectorBase, ConfigInspectorLive, makeConfigInspectorTest };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { GitHubApiError } from "../errors.js";
|
|
2
|
+
import { getGitHubInfo } from "../vendor/github-info.js";
|
|
3
|
+
import { Context, Effect, Layer } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/changesets/services/github.ts
|
|
6
|
+
/**
|
|
7
|
+
* GitHub service for fetching commit metadata.
|
|
8
|
+
*
|
|
9
|
+
* Defines the {@link GitHubService} Effect service tag, the
|
|
10
|
+
* {@link GitHubLive | production layer} backed by `\@changesets/get-github-info`,
|
|
11
|
+
* and the {@link makeGitHubTest} helper for constructing deterministic test
|
|
12
|
+
* layers.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* The GitHub service is consumed by the changelog formatters to resolve
|
|
16
|
+
* commit hashes into pull-request numbers, author usernames, and link URLs.
|
|
17
|
+
* In production, {@link GitHubLive} calls the GitHub REST API via the
|
|
18
|
+
* vendored `getGitHubInfo` wrapper. In tests, {@link makeGitHubTest}
|
|
19
|
+
* returns canned responses from a `Map` keyed by commit hash.
|
|
20
|
+
*
|
|
21
|
+
* @see {@link GitHubService} for the Effect service tag
|
|
22
|
+
* @see {@link GitHubServiceShape} for the service interface
|
|
23
|
+
* @see {@link GitHubLive} for the production layer
|
|
24
|
+
* @see {@link makeGitHubTest} for constructing test layers
|
|
25
|
+
*/
|
|
26
|
+
const _tag = Context.Tag("GitHubService");
|
|
27
|
+
/**
|
|
28
|
+
* Base class for GitHubService.
|
|
29
|
+
*
|
|
30
|
+
* @privateRemarks
|
|
31
|
+
* This export is required for api-extractor documentation generation.
|
|
32
|
+
* Effect's Context.Tag creates an anonymous base class that must be
|
|
33
|
+
* explicitly exported to avoid "forgotten export" warnings. Do not delete.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
const GitHubServiceBase = _tag();
|
|
38
|
+
/**
|
|
39
|
+
* Effect service tag for GitHub API operations.
|
|
40
|
+
*
|
|
41
|
+
* Provides dependency-injected access to GitHub commit metadata lookups.
|
|
42
|
+
* Use `yield* GitHubService` inside an `Effect.gen` block to obtain the
|
|
43
|
+
* service instance.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* This tag follows the standard Effect `Context.Tag` pattern. Two layers
|
|
47
|
+
* are provided out of the box:
|
|
48
|
+
*
|
|
49
|
+
* - {@link GitHubLive} — production layer backed by the GitHub REST API
|
|
50
|
+
* - {@link makeGitHubTest} — factory for deterministic test layers
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* import { Effect, Layer } from "effect";
|
|
55
|
+
* import { GitHubService, GitHubLive } from "\@savvy-web/changesets";
|
|
56
|
+
*
|
|
57
|
+
* const program = Effect.gen(function* () {
|
|
58
|
+
* const github = yield* GitHubService;
|
|
59
|
+
* const info = yield* github.getInfo({
|
|
60
|
+
* commit: "abc1234567890",
|
|
61
|
+
* repo: "savvy-web/changesets",
|
|
62
|
+
* });
|
|
63
|
+
* console.log(info.user, info.pull, info.links);
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* // Provide the live layer and run
|
|
67
|
+
* Effect.runPromise(program.pipe(Effect.provide(GitHubLive)));
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example Creating a test layer with canned responses
|
|
71
|
+
* ```typescript
|
|
72
|
+
* import { Effect } from "effect";
|
|
73
|
+
* import type { GitHubCommitInfo } from "\@savvy-web/changesets";
|
|
74
|
+
* import { GitHubService, makeGitHubTest } from "\@savvy-web/changesets";
|
|
75
|
+
*
|
|
76
|
+
* const testResponses = new Map<string, GitHubCommitInfo>([
|
|
77
|
+
* ["abc1234", { user: "octocat", pull: 42, links: { pull: "#42", user: "\@octocat" } }],
|
|
78
|
+
* ]);
|
|
79
|
+
*
|
|
80
|
+
* const TestLayer = makeGitHubTest(testResponses);
|
|
81
|
+
*
|
|
82
|
+
* const program = Effect.gen(function* () {
|
|
83
|
+
* const github = yield* GitHubService;
|
|
84
|
+
* return yield* github.getInfo({ commit: "abc1234", repo: "owner/repo" });
|
|
85
|
+
* });
|
|
86
|
+
*
|
|
87
|
+
* Effect.runPromise(program.pipe(Effect.provide(TestLayer)));
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @see {@link GitHubServiceShape} for the service interface
|
|
91
|
+
* @see {@link GitHubLive} for the production layer
|
|
92
|
+
* @see {@link makeGitHubTest} for creating test layers
|
|
93
|
+
* @see {@link GitHubServiceBase} for the api-extractor base class
|
|
94
|
+
*
|
|
95
|
+
* @public
|
|
96
|
+
*/
|
|
97
|
+
var GitHubService = class extends GitHubServiceBase {};
|
|
98
|
+
/**
|
|
99
|
+
* Production layer for {@link GitHubService}.
|
|
100
|
+
*
|
|
101
|
+
* Delegates to `\@changesets/get-github-info` to fetch commit metadata
|
|
102
|
+
* from the GitHub REST API. Requires a `GITHUB_TOKEN` environment variable
|
|
103
|
+
* to be set for authenticated requests.
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* This layer is used by the `\@savvy-web/changesets/changelog` entry point
|
|
107
|
+
* to resolve commit hashes into PR numbers and author attribution. It is
|
|
108
|
+
* composed with {@link MarkdownLive} in the changelog formatter's
|
|
109
|
+
* `MainLayer`.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* import { Effect } from "effect";
|
|
114
|
+
* import { GitHubService, GitHubLive } from "\@savvy-web/changesets";
|
|
115
|
+
*
|
|
116
|
+
* const program = Effect.gen(function* () {
|
|
117
|
+
* const github = yield* GitHubService;
|
|
118
|
+
* return yield* github.getInfo({ commit: "abc1234", repo: "owner/repo" });
|
|
119
|
+
* });
|
|
120
|
+
*
|
|
121
|
+
* Effect.runPromise(program.pipe(Effect.provide(GitHubLive)));
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @public
|
|
125
|
+
*/
|
|
126
|
+
const GitHubLive = Layer.succeed(GitHubService, { getInfo: getGitHubInfo });
|
|
127
|
+
/**
|
|
128
|
+
* Create a test layer for {@link GitHubService} with pre-configured responses.
|
|
129
|
+
*
|
|
130
|
+
* Returns a `Layer` that resolves commit hashes from the provided `Map`.
|
|
131
|
+
* Lookups for commits not present in the map fail with a
|
|
132
|
+
* {@link GitHubApiError}.
|
|
133
|
+
*
|
|
134
|
+
* @remarks
|
|
135
|
+
* This helper is the recommended way to test code that depends on
|
|
136
|
+
* `GitHubService` without making real API calls. Provide the layer
|
|
137
|
+
* via `Effect.provide` in your test setup.
|
|
138
|
+
*
|
|
139
|
+
* @param responses - A `Map` of full commit hash to {@link GitHubCommitInfo} objects
|
|
140
|
+
* @returns A `Layer` providing the {@link GitHubService} with deterministic responses
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* import { Effect } from "effect";
|
|
145
|
+
* import type { GitHubCommitInfo } from "\@savvy-web/changesets";
|
|
146
|
+
* import { GitHubService, makeGitHubTest } from "\@savvy-web/changesets";
|
|
147
|
+
*
|
|
148
|
+
* const responses = new Map<string, GitHubCommitInfo>([
|
|
149
|
+
* ["abc1234", { user: "octocat", pull: 42, links: { pull: "#42", user: "\@octocat" } }],
|
|
150
|
+
* ]);
|
|
151
|
+
*
|
|
152
|
+
* const TestGitHub = makeGitHubTest(responses);
|
|
153
|
+
*
|
|
154
|
+
* const program = Effect.gen(function* () {
|
|
155
|
+
* const github = yield* GitHubService;
|
|
156
|
+
* return yield* github.getInfo({ commit: "abc1234", repo: "owner/repo" });
|
|
157
|
+
* });
|
|
158
|
+
*
|
|
159
|
+
* // In a Vitest test:
|
|
160
|
+
* const result = await Effect.runPromise(program.pipe(Effect.provide(TestGitHub)));
|
|
161
|
+
* // result.user === "octocat"
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* @public
|
|
165
|
+
*/
|
|
166
|
+
function makeGitHubTest(responses) {
|
|
167
|
+
return Layer.succeed(GitHubService, { getInfo: (params) => {
|
|
168
|
+
const info = responses.get(params.commit);
|
|
169
|
+
if (info) return Effect.succeed(info);
|
|
170
|
+
return Effect.fail(new GitHubApiError({
|
|
171
|
+
operation: "getInfo",
|
|
172
|
+
reason: `No mock response for commit ${params.commit}`
|
|
173
|
+
}));
|
|
174
|
+
} });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
//#endregion
|
|
178
|
+
export { GitHubLive, GitHubService, GitHubServiceBase, makeGitHubTest };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { parseMarkdown, stringifyMarkdown } from "../utils/remark-pipeline.js";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/changesets/services/markdown.ts
|
|
5
|
+
/**
|
|
6
|
+
* Markdown service for parsing and stringifying mdast trees.
|
|
7
|
+
*
|
|
8
|
+
* Defines the {@link MarkdownService} Effect service tag and the
|
|
9
|
+
* {@link MarkdownLive | production layer} backed by the remark/unified
|
|
10
|
+
* pipeline. This service provides a pure, side-effect-free interface to
|
|
11
|
+
* markdown parsing and stringification used throughout the changelog
|
|
12
|
+
* formatter and remark transform pipeline.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* The service wraps the `remark-pipeline` utility functions (`parseMarkdown`
|
|
16
|
+
* and `stringifyMarkdown`) behind an Effect interface. The `parse` operation
|
|
17
|
+
* returns an mdast `Root` node; the `stringify` operation serializes an
|
|
18
|
+
* mdast `Root` back to a markdown string. Both operations are synchronous
|
|
19
|
+
* under the hood but are lifted into `Effect.sync` for composability.
|
|
20
|
+
*
|
|
21
|
+
* @see {@link MarkdownService} for the Effect service tag
|
|
22
|
+
* @see {@link MarkdownServiceShape} for the service interface
|
|
23
|
+
* @see {@link MarkdownLive} for the production layer
|
|
24
|
+
*/
|
|
25
|
+
const _tag = Context.Tag("MarkdownService");
|
|
26
|
+
/**
|
|
27
|
+
* Base class for MarkdownService.
|
|
28
|
+
*
|
|
29
|
+
* @privateRemarks
|
|
30
|
+
* This export is required for api-extractor documentation generation.
|
|
31
|
+
* Effect's Context.Tag creates an anonymous base class that must be
|
|
32
|
+
* explicitly exported to avoid "forgotten export" warnings. Do not delete.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
const MarkdownServiceBase = _tag();
|
|
37
|
+
/**
|
|
38
|
+
* Effect service tag for markdown parsing and stringification.
|
|
39
|
+
*
|
|
40
|
+
* Provides dependency-injected access to markdown parse/stringify operations.
|
|
41
|
+
* Use `yield* MarkdownService` inside an `Effect.gen` block to obtain the
|
|
42
|
+
* service instance.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* This tag follows the standard Effect `Context.Tag` pattern. The
|
|
46
|
+
* {@link MarkdownLive} layer provides the default implementation backed by
|
|
47
|
+
* the remark/unified pipeline. For testing, you can supply a custom layer
|
|
48
|
+
* via `Layer.succeed(MarkdownService, { parse: ..., stringify: ... })`.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* import { Effect } from "effect";
|
|
53
|
+
* import { MarkdownService, MarkdownLive } from "\@savvy-web/changesets";
|
|
54
|
+
*
|
|
55
|
+
* const program = Effect.gen(function* () {
|
|
56
|
+
* const md = yield* MarkdownService;
|
|
57
|
+
* const tree = yield* md.parse("# Hello\n\nWorld");
|
|
58
|
+
* const output = yield* md.stringify(tree);
|
|
59
|
+
* console.log(output); // "# Hello\n\nWorld\n"
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* Effect.runPromise(program.pipe(Effect.provide(MarkdownLive)));
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @see {@link MarkdownServiceShape} for the service interface
|
|
66
|
+
* @see {@link MarkdownLive} for the production layer
|
|
67
|
+
* @see {@link MarkdownServiceBase} for the api-extractor base class
|
|
68
|
+
*
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
var MarkdownService = class extends MarkdownServiceBase {};
|
|
72
|
+
/**
|
|
73
|
+
* Production layer for {@link MarkdownService}.
|
|
74
|
+
*
|
|
75
|
+
* Wraps the remark-pipeline utility functions (`parseMarkdown` and
|
|
76
|
+
* `stringifyMarkdown`) in `Effect.sync` for use in Effect programs.
|
|
77
|
+
* Both operations are synchronous and infallible.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* This layer is composed with {@link GitHubLive} in the
|
|
81
|
+
* `\@savvy-web/changesets/changelog` entry point to form the `MainLayer`
|
|
82
|
+
* that powers the Changesets API integration.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* import { Effect, Layer } from "effect";
|
|
87
|
+
* import { MarkdownService, MarkdownLive } from "\@savvy-web/changesets";
|
|
88
|
+
*
|
|
89
|
+
* const program = Effect.gen(function* () {
|
|
90
|
+
* const md = yield* MarkdownService;
|
|
91
|
+
* const tree = yield* md.parse("**bold** text");
|
|
92
|
+
* return yield* md.stringify(tree);
|
|
93
|
+
* });
|
|
94
|
+
*
|
|
95
|
+
* Effect.runPromise(program.pipe(Effect.provide(MarkdownLive)));
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* @public
|
|
99
|
+
*/
|
|
100
|
+
const MarkdownLive = Layer.succeed(MarkdownService, {
|
|
101
|
+
parse: (content) => Effect.sync(() => parseMarkdown(content)),
|
|
102
|
+
stringify: (tree) => Effect.sync(() => stringifyMarkdown(tree))
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
export { MarkdownLive, MarkdownService, MarkdownServiceBase };
|