tegami 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,45 +1,33 @@
1
1
  import { a as maxBump } from "./semver-C4vJ4SK8.js";
2
- import { n as generateChangelog } from "./generate-B3bcuKLY.js";
2
+ import { i as changelogFrontmatterSchema, n as generateFromCommits } from "./generate-BMlrn-2e.js";
3
3
  import { r as handlePluginError } from "./error-DNy8R5ue.js";
4
- import { i as readPlanStore, n as publishPlanStatus, r as createPlanStore, t as assertPublishPlanFinished } from "./checks-azjuIFt7.js";
5
- import { t as PackageGraph } from "./graph-DLKPUXSD.js";
4
+ import { t as PackageGraph } from "./graph-DrzluXw8.js";
6
5
  import { cargo } from "./providers/cargo.js";
7
6
  import { npm } from "./providers/npm.js";
8
7
  import { simpleGenerator } from "./generators/simple.js";
9
- import { a as readChangelogEntries, i as parseReplayCondition, r as publishFromPlan } from "./publish-DkRT0yh6.js";
10
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
11
- import path, { dirname, join } from "node:path";
8
+ import fs, { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
9
+ import path, { basename, dirname, join } from "node:path";
12
10
  import * as semver from "semver";
11
+ import z from "zod";
12
+ import { dump, load, visit } from "js-yaml";
13
+ import { fromMarkdown } from "mdast-util-from-markdown";
14
+ import { toMarkdown } from "mdast-util-to-markdown";
13
15
  //#region src/context.ts
14
16
  async function createTegamiContext(options = {}) {
15
17
  const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
16
18
  const changelogDir = path.resolve(cwd, options.changelogDir ?? ".tegami");
17
19
  const graph = new PackageGraph();
18
- const registryClients = /* @__PURE__ */ new Map();
19
20
  const ctx = {
20
21
  cwd,
21
22
  changelogDir,
22
- planPath: options.planPath ? path.resolve(cwd, options.planPath) : path.join(changelogDir, "publish-plan"),
23
+ lockPath: options.lockPath ? path.resolve(cwd, options.lockPath) : path.join(changelogDir, "publish-lock.yaml"),
23
24
  options,
24
25
  plugins: resolvePlugins([
25
26
  npm(options.npm),
26
27
  cargo(options.cargo),
27
28
  ...options.plugins ?? []
28
29
  ]),
29
- graph,
30
- getRegistryClient(pkgOrId) {
31
- let client;
32
- if (typeof pkgOrId === "string") client = registryClients.get(pkgOrId);
33
- else for (const item of registryClients.values()) if (item.supports && item.supports(pkgOrId)) {
34
- client = item;
35
- break;
36
- }
37
- if (!client) {
38
- const id = typeof pkgOrId === "string" ? pkgOrId : pkgOrId.manager;
39
- throw new Error(`No registry client is available for ${id}.`);
40
- }
41
- return client;
42
- }
30
+ graph
43
31
  };
44
32
  for (const plugin of ctx.plugins) await handlePluginError(plugin, "init", () => plugin.init?.call(ctx));
45
33
  for (const plugin of ctx.plugins) await handlePluginError(plugin, "resolve", () => plugin.resolve?.call(ctx));
@@ -58,11 +46,6 @@ async function createTegamiContext(options = {}) {
58
46
  pkg.setPackageOptions(packageOptions);
59
47
  if (packageOptions.group) graph.addGroupMember(packageOptions.group, pkg.id);
60
48
  }
61
- for (const plugin of ctx.plugins) {
62
- const clients = await handlePluginError(plugin, "createRegistryClient", () => plugin.createRegistryClient?.call(ctx));
63
- if (!clients) continue;
64
- for (const client of Array.isArray(clients) ? clients : [clients]) registryClients.set(client.id, client);
65
- }
66
49
  return ctx;
67
50
  }
68
51
  const PLUGIN_ORDER = {
@@ -74,18 +57,203 @@ function resolvePlugins(plugins = []) {
74
57
  return plugins.flat(Infinity).sort((a, b) => PLUGIN_ORDER[a.enforce ?? "default"] - PLUGIN_ORDER[b.enforce ?? "default"]);
75
58
  }
76
59
  //#endregion
60
+ //#region src/utils/frontmatter.ts
61
+ /**
62
+ * Inspired by https://github.com/jonschlinkert/gray-matter
63
+ */
64
+ const regex = /^---\r?\n(.+?)\r?\n---\r?\n?/s;
65
+ /**
66
+ * parse frontmatter, it supports only yaml format
67
+ */
68
+ function frontmatter(input) {
69
+ const output = {
70
+ matter: "",
71
+ data: {},
72
+ content: input
73
+ };
74
+ const match = regex.exec(input);
75
+ if (!match) return output;
76
+ output.matter = match[0];
77
+ output.content = input.slice(match[0].length);
78
+ output.data = load(match[1]) ?? {};
79
+ return output;
80
+ }
81
+ //#endregion
82
+ //#region src/changelog/parse.ts
83
+ async function getChangelogFiles(context) {
84
+ return (await readdir(context.changelogDir).catch(() => [])).filter((file) => file.endsWith(".md"));
85
+ }
86
+ async function readChangelogEntries(context) {
87
+ const dir = context.changelogDir;
88
+ const files = await getChangelogFiles(context);
89
+ return (await Promise.all(files.map(async (file) => {
90
+ const filePath = join(dir, file);
91
+ const content = await readFile(filePath, "utf8");
92
+ return parseChangelogFile(basename(filePath), content);
93
+ }))).filter((v) => v !== void 0);
94
+ }
95
+ /** Parse one changelog markdown file into release entries. */
96
+ function parseChangelogFile(filename, content) {
97
+ const parsed = frontmatter(content);
98
+ const { success, data } = changelogFrontmatterSchema.safeParse(parsed.data);
99
+ if (!success || !data.packages) return;
100
+ const tree = fromMarkdown(parsed.content);
101
+ let headingBump;
102
+ const packages = /* @__PURE__ */ new Map();
103
+ const sections = [];
104
+ for (const section of getHeadingSections(tree)) {
105
+ const sectionBumpType = headingToBump(section.heading.depth);
106
+ if (sectionBumpType) headingBump = headingBump ? maxBump(headingBump, sectionBumpType) : sectionBumpType;
107
+ sections.push({
108
+ depth: section.heading.depth,
109
+ title: sectionToMarkdown(section.heading.children),
110
+ content: sectionToMarkdown(section.children)
111
+ });
112
+ }
113
+ if (sections.length === 0) return;
114
+ if (Array.isArray(data.packages)) {
115
+ if (!headingBump) return;
116
+ for (const pkg of data.packages) packages.set(pkg, { type: headingBump });
117
+ } else for (const [k, v] of Object.entries(data.packages)) {
118
+ let config;
119
+ if (typeof v === "string") config = { type: v };
120
+ else if (v === null) config = { type: headingBump };
121
+ else config = v;
122
+ if (config.type || config.replay?.length) packages.set(k, config);
123
+ }
124
+ if (packages.size === 0) return;
125
+ const entry = {
126
+ id: filename,
127
+ filename,
128
+ subject: data.subject,
129
+ packages,
130
+ sections,
131
+ _raw_body: parsed.content,
132
+ getRawContent() {
133
+ return [
134
+ "---",
135
+ dump({
136
+ subject: this.subject,
137
+ packages: Object.fromEntries(this.packages.entries())
138
+ }).trim(),
139
+ "---",
140
+ "",
141
+ entry._raw_body.trim(),
142
+ ""
143
+ ].join("\n");
144
+ }
145
+ };
146
+ return entry;
147
+ }
148
+ function parseReplayCondition(condition) {
149
+ if (condition.startsWith("exit prerelease:")) return {
150
+ type: "on-exit-prerelease",
151
+ name: condition.slice(16).trimStart()
152
+ };
153
+ const idx = condition.lastIndexOf("@");
154
+ if (idx <= 0) return null;
155
+ return {
156
+ type: "on-version",
157
+ name: condition.slice(0, idx),
158
+ version: condition.slice(idx + 1)
159
+ };
160
+ }
161
+ function getHeadingSections(tree) {
162
+ const sections = [];
163
+ let current;
164
+ for (const child of tree.children) {
165
+ if (child.type === "heading") {
166
+ current = {
167
+ heading: child,
168
+ children: []
169
+ };
170
+ sections.push(current);
171
+ continue;
172
+ }
173
+ current?.children.push(child);
174
+ }
175
+ return sections;
176
+ }
177
+ function sectionToMarkdown(children) {
178
+ if (children.length === 0) return "";
179
+ return toMarkdown({
180
+ type: "root",
181
+ children
182
+ }, {
183
+ bullet: "-",
184
+ fence: "`"
185
+ }).trim();
186
+ }
187
+ function headingToBump(depth) {
188
+ switch (depth) {
189
+ case 1: return "major";
190
+ case 2: return "minor";
191
+ case 3: return "patch";
192
+ }
193
+ }
194
+ //#endregion
195
+ //#region src/plans/lock.ts
196
+ function inlineMultilineScalars(documents) {
197
+ visit(documents, (node) => {
198
+ if (node.kind !== "scalar" || !node.value.includes("\n")) return;
199
+ node.style.doubleQuoted = true;
200
+ node.style.literal = false;
201
+ node.style.folded = false;
202
+ node.style.singleQuoted = false;
203
+ });
204
+ }
205
+ const lockDumpOptions = {
206
+ sortKeys: true,
207
+ lineWidth: -1,
208
+ transform: inlineMultilineScalars
209
+ };
210
+ /**
211
+ * the data structure of `publish-lock.yaml` file.
212
+ */
213
+ var PublishLock = class {
214
+ data;
215
+ constructor(data = /* @__PURE__ */ new Map()) {
216
+ this.data = data;
217
+ }
218
+ /** write data to namespace, note that the `data` must be serializable in yaml */
219
+ write(namespace, data) {
220
+ let arr = this.data.get(namespace);
221
+ if (!arr) {
222
+ arr = [];
223
+ this.data.set(namespace, arr);
224
+ }
225
+ arr.push(data);
226
+ }
227
+ read(namespace) {
228
+ return this.data.get(namespace)?.shift();
229
+ }
230
+ size(namespace) {
231
+ return this.data.get(namespace)?.length ?? 0;
232
+ }
233
+ serialize() {
234
+ let res = dump(Object.fromEntries(this.data.entries()), lockDumpOptions);
235
+ if (!res.endsWith("\n")) res += "\n";
236
+ return res;
237
+ }
238
+ };
239
+ const baseSchema = z.record(z.string(), z.array(z.unknown()));
240
+ function parsePublishLock(content) {
241
+ const data = baseSchema.parse(load(content));
242
+ return new PublishLock(new Map(Object.entries(data)));
243
+ }
244
+ //#endregion
77
245
  //#region src/plans/policy.ts
78
246
  function groupPolicy({ graph }) {
79
247
  return {
80
248
  id: "group",
81
- onUpdate({ pkg, plan }) {
82
- if (!plan.type) return;
249
+ onUpdate({ pkg, packageDraft }) {
250
+ if (!packageDraft.type) return;
83
251
  const group = graph.getPackageGroup(pkg.id);
84
252
  if (!group || !group.options.syncBump) return;
85
253
  for (const member of group.packages) {
86
254
  if (member === pkg) continue;
87
255
  this.bumpPackage(member, {
88
- type: plan.type,
256
+ type: packageDraft.type,
89
257
  reason: `sync "${group.name}" group package versions`
90
258
  });
91
259
  }
@@ -94,57 +262,70 @@ function groupPolicy({ graph }) {
94
262
  }
95
263
  //#endregion
96
264
  //#region src/plans/draft.ts
97
- var DraftPlan = class {
265
+ const changelogStoreSchema = z.object({
266
+ v: z.literal("0.0.0"),
267
+ filename: z.string(),
268
+ content: z.string()
269
+ });
270
+ const packageStoreSchema = z.object({
271
+ id: z.string(),
272
+ updated: z.boolean(),
273
+ changelogIds: z.array(z.string()).optional()
274
+ });
275
+ /** a draft describes all operations to perform before the actual publishing, such as version bumps. */
276
+ var Draft = class {
98
277
  context;
99
278
  #applied = false;
279
+ /** package id -> draft */
100
280
  packages = /* @__PURE__ */ new Map();
281
+ /** id -> changelog */
101
282
  changelogs = /* @__PURE__ */ new Map();
102
283
  policies = [];
103
284
  constructor(context) {
104
285
  this.context = context;
105
286
  this.policies.push(groupPolicy(context));
106
287
  }
107
- getPackagePlans() {
288
+ getPackageDrafts() {
108
289
  return this.packages;
109
290
  }
110
- getPackagePlan(id) {
291
+ getPackageDraft(id) {
111
292
  return this.packages.get(id);
112
293
  }
113
294
  bumpPackage(pkg, { type, reason }) {
114
- return this.dispatchPackage(pkg, (plan) => {
115
- plan.type = plan.type ? maxBump(plan.type, type) : type;
295
+ return this.dispatchPackage(pkg, (draft) => {
296
+ draft.type = draft.type ? maxBump(draft.type, type) : type;
116
297
  if (reason) {
117
- plan.bumpReasons ??= /* @__PURE__ */ new Set();
118
- plan.bumpReasons.add(reason);
298
+ draft.bumpReasons ??= /* @__PURE__ */ new Set();
299
+ draft.bumpReasons.add(reason);
119
300
  }
120
301
  });
121
302
  }
122
303
  dispatchPackage(pkg, dispatch, onUpdate) {
123
- const plan = this.getOrInitPackage(pkg);
124
- const prevVersion = plan.bumpVersion(pkg);
125
- dispatch(plan);
126
- if (prevVersion !== plan.bumpVersion(pkg)) {
127
- onUpdate?.(plan);
304
+ const packageDraft = this.getOrInitPackage(pkg);
305
+ const prevVersion = packageDraft.bumpVersion(pkg);
306
+ dispatch(packageDraft);
307
+ if (prevVersion !== packageDraft.bumpVersion(pkg)) {
308
+ onUpdate?.(packageDraft);
128
309
  for (const policy of this.policies) policy.onUpdate?.call(this, {
129
- plan,
310
+ packageDraft,
130
311
  pkg
131
312
  });
132
313
  }
133
- return plan;
314
+ return packageDraft;
134
315
  }
135
316
  getOrInitPackage(pkg) {
136
317
  const existing = this.packages.get(pkg.id);
137
318
  if (existing) return existing;
138
- this.packages.set(pkg.id, pkg.initPlan());
139
- return this.dispatchPackage(pkg, (plan) => pkg.configurePlan(plan, this.context.graph.getPackageGroup(pkg.id)), (plan) => {
140
- (plan.bumpReasons ??= /* @__PURE__ */ new Set()).add("align with script-level configs");
319
+ this.packages.set(pkg.id, pkg.initDraft());
320
+ return this.dispatchPackage(pkg, (draft) => pkg.configureDraft(draft, this.context.graph.getPackageGroup(pkg.id)), (draft) => {
321
+ (draft.bumpReasons ??= /* @__PURE__ */ new Set()).add("align with script-level configs");
141
322
  });
142
323
  }
143
324
  hasPending() {
144
325
  const { graph } = this.context;
145
- for (const [id, plan] of this.packages) {
326
+ for (const [id, draft] of this.packages) {
146
327
  const pkg = graph.get(id);
147
- if (pkg && plan.bumpVersion(pkg) !== pkg.version) return true;
328
+ if (pkg && draft.bumpVersion(pkg) !== pkg.version) return true;
148
329
  }
149
330
  return false;
150
331
  }
@@ -167,30 +348,53 @@ var DraftPlan = class {
167
348
  deleteChangelog(id) {
168
349
  return this.changelogs.delete(id);
169
350
  }
170
- /** Apply the publish plan: update package versions, write the plan file, and consume changelog files. */
171
- async applyPlan() {
172
- if (this.#applied) throw new Error("This draft has already applied a publish plan.");
173
- await assertPublishPlanFinished(this.context);
351
+ /** Apply version bumps, lock file, and changelog files. */
352
+ async apply() {
353
+ if (this.#applied) throw new Error("This draft has already been applied.");
174
354
  const { graph } = this.context;
175
355
  this.#applied = true;
176
356
  const snapshots = /* @__PURE__ */ new Map();
177
357
  for (const pkg of graph.getPackages()) snapshots.set(pkg.id, { version: pkg.version });
178
- for (const plugin of this.context.plugins) await handlePluginError(plugin, "applyPlan", () => plugin.applyPlan?.call(this.context, this));
358
+ for (const plugin of this.context.plugins) await handlePluginError(plugin, "applyDraft", () => plugin.applyDraft?.call(this.context, this));
179
359
  const updatedChangelogs = this.applyReplays(snapshots);
180
360
  const writes = [];
181
- for (const [id, packagePlan] of this.packages) {
361
+ for (const [id, packageDraft] of this.packages) {
182
362
  const pkg = graph.get(id);
183
363
  if (!pkg) continue;
184
- writes.push(this.appendChangelog(pkg, packagePlan));
364
+ writes.push(this.appendChangelog(pkg, packageDraft));
185
365
  }
186
366
  for (const entry of this.changelogs.values()) {
187
367
  const updated = updatedChangelogs.get(entry.id);
188
368
  const filePath = path.join(this.context.changelogDir, entry.filename);
189
- writes.push(updated ? writeFile(filePath, updated.getRawContent()) : rm(filePath, { force: true }));
369
+ if (updated) writes.push(mkdir(this.context.changelogDir, { recursive: true }).then(() => writeFile(filePath, updated.getRawContent())));
370
+ else if (!entry.virtual) writes.push(rm(filePath, { force: true }));
190
371
  }
372
+ writes.push(this.writeLockFile(snapshots));
191
373
  await Promise.all(writes);
192
- await mkdir(dirname(this.context.planPath), { recursive: true });
193
- await writeFile(this.context.planPath, createPlanStore(this, this.context));
374
+ }
375
+ /** write persistent data to publish lock */
376
+ async writeLockFile(snapshots) {
377
+ const lock = new PublishLock();
378
+ for (const entry of this.getChangelogs()) lock.write("core:changelogs", {
379
+ v: "0.0.0",
380
+ filename: entry.filename,
381
+ content: entry.getRawContent()
382
+ });
383
+ for (const pkg of this.context.graph.getPackages()) {
384
+ const draft = this.getPackageDraft(pkg.id);
385
+ const snapshot = snapshots.get(pkg.id);
386
+ lock.write("core:packages", {
387
+ id: pkg.id,
388
+ updated: draft !== void 0 && (snapshot === void 0 || snapshot.version !== pkg.version),
389
+ changelogIds: draft?.changelogs?.map((entry) => entry.id)
390
+ });
391
+ }
392
+ for (const plugin of this.context.plugins) await handlePluginError(plugin, "initPublishLock", () => plugin.initPublishLock?.call(this.context, {
393
+ lock,
394
+ draft: this
395
+ }));
396
+ await mkdir(dirname(this.context.lockPath), { recursive: true });
397
+ await writeFile(this.context.lockPath, lock.serialize());
194
398
  }
195
399
  addPolicy(policy) {
196
400
  this.policies.push(policy);
@@ -242,53 +446,191 @@ var DraftPlan = class {
242
446
  }
243
447
  return updated;
244
448
  }
245
- async appendChangelog(pkg, plan) {
246
- if (!plan.changelogs || plan.changelogs.length === 0) return;
449
+ async appendChangelog(pkg, draft) {
450
+ if (!draft.changelogs || draft.changelogs.length === 0) return;
247
451
  const { generator = simpleGenerator() } = this.context.options;
248
452
  const generated = await generator.generate.call(this.context, {
249
- packageId: pkg.id,
250
- packageName: pkg.name,
251
- version: pkg.version,
252
- plan,
253
- changelogs: plan.changelogs,
254
- unstable_draft: this
453
+ pkg,
454
+ packageDraft: draft,
455
+ draft: this
255
456
  });
256
457
  const path = join(pkg.path, "CHANGELOG.md");
257
458
  const existing = await readFile(path, "utf8").catch(() => "");
258
459
  await writeFile(path, `${generated.trim()}\n\n${existing}`.trimEnd() + "\n");
259
460
  }
260
- /** {@link applyPlan} but for `await using` syntax */
461
+ /** {@link apply} but for `await using` syntax */
261
462
  async [Symbol.asyncDispose]() {
262
- return this.applyPlan();
463
+ return this.apply();
263
464
  }
264
465
  };
265
- async function cleanupPublishPlan(context) {
266
- const store = await readPlanStore(context);
267
- if (!store) return {
268
- state: "skipped",
269
- reason: "missing"
270
- };
271
- if ((await publishPlanStatus(store, context)).state !== "success") return {
272
- state: "skipped",
273
- reason: "pending"
274
- };
275
- await rm(context.planPath, { force: true });
276
- return { state: "removed" };
277
- }
278
- async function createDraftPlan(changelogs, context) {
279
- let draft = new DraftPlan(context);
466
+ async function createDraft(changelogs, context) {
467
+ let draft = new Draft(context);
280
468
  for (const plugin of context.plugins) {
281
- const result = await handlePluginError(plugin, "initPlan", () => plugin.initPlan?.call(context, draft));
469
+ const result = await handlePluginError(plugin, "initDraft", () => plugin.initDraft?.call(context, draft));
282
470
  if (result) draft = result;
283
471
  }
284
472
  for (const pkg of context.graph.getPackages()) draft.getOrInitPackage(pkg);
285
473
  for (const entry of changelogs) draft.addChangelog(entry);
286
474
  return draft;
287
475
  }
288
- function attachChangelog(plan, entry) {
289
- if (plan.changelogs?.some((item) => item.id === entry.id)) return;
290
- plan.changelogs ??= [];
291
- plan.changelogs.push(entry);
476
+ function attachChangelog(draft, entry) {
477
+ if (draft.changelogs?.some((item) => item.id === entry.id)) return;
478
+ draft.changelogs ??= [];
479
+ draft.changelogs.push(entry);
480
+ }
481
+ //#endregion
482
+ //#region src/plans/publish.ts
483
+ async function initPublishPlan(context, options) {
484
+ let lock;
485
+ try {
486
+ lock = parsePublishLock(await fs.readFile(context.lockPath, "utf8"));
487
+ } catch {
488
+ return;
489
+ }
490
+ let data;
491
+ const packages = /* @__PURE__ */ new Map();
492
+ const changelogs = /* @__PURE__ */ new Map();
493
+ while (data = lock.read("core:changelogs")) {
494
+ const entry = changelogStoreSchema.safeParse(data).data;
495
+ if (!entry) continue;
496
+ const parsed = parseChangelogFile(entry.filename, entry.content);
497
+ if (!parsed) continue;
498
+ changelogs.set(parsed.id, parsed);
499
+ }
500
+ while (data = lock.read("core:packages")) {
501
+ const parsed = packageStoreSchema.safeParse(data).data;
502
+ if (!parsed) continue;
503
+ if (!context.graph.get(parsed.id)) continue;
504
+ const pkgChangelogs = [];
505
+ for (const id of parsed.changelogIds ?? []) {
506
+ const entry = changelogs.get(id);
507
+ if (entry) pkgChangelogs.push(entry);
508
+ }
509
+ packages.set(parsed.id, {
510
+ changelogs: pkgChangelogs,
511
+ updated: parsed.updated
512
+ });
513
+ }
514
+ const plan = {
515
+ options,
516
+ changelogs,
517
+ packages
518
+ };
519
+ for (const plugin of context.plugins) await handlePluginError(plugin, "initPublishPlan", () => plugin.initPublishPlan?.call(context, {
520
+ lock,
521
+ plan
522
+ }));
523
+ return plan;
524
+ }
525
+ function resolvePublishTargets(plan) {
526
+ const ordered = [];
527
+ function scan(id, stack = /* @__PURE__ */ new Set()) {
528
+ const preflight = plan.packages.get(id)?.preflight;
529
+ if (!preflight || preflight.publish === false) return;
530
+ if (stack.has(id)) throw new Error(`circular reference of deps: ${[...stack, id].join(" -> ")}`);
531
+ if (ordered.includes(id)) return;
532
+ if (preflight.wait) {
533
+ stack.add(id);
534
+ for (const dep of preflight.wait) scan(dep, stack);
535
+ stack.delete(id);
536
+ }
537
+ ordered.push(id);
538
+ }
539
+ for (const id of plan.packages.keys()) scan(id);
540
+ return ordered;
541
+ }
542
+ async function runPublishPlan(context, plan) {
543
+ const { dryRun = false } = plan.options;
544
+ const onPublishResult = async (pkg, publishResult) => {
545
+ const packagePlan = plan.packages.get(pkg.id);
546
+ if (!packagePlan) return;
547
+ packagePlan.publishResult = publishResult;
548
+ for (const plugin of context.plugins) await handlePluginError(plugin, "afterPublish", () => plugin.afterPublish?.call(context, {
549
+ pkg,
550
+ plan
551
+ }));
552
+ };
553
+ for (const id of resolvePublishTargets(plan)) {
554
+ const pkg = context.graph.get(id);
555
+ if (dryRun) {
556
+ await onPublishResult(pkg, { type: "published" });
557
+ continue;
558
+ }
559
+ let isSkipped = false;
560
+ for (const plugin of context.plugins) if (await handlePluginError(plugin, "willPublish", () => plugin.willPublish?.call(context, { pkg })) === false) {
561
+ isSkipped = true;
562
+ break;
563
+ }
564
+ if (isSkipped) {
565
+ await onPublishResult(pkg, { type: "skipped" });
566
+ continue;
567
+ }
568
+ let published = false;
569
+ for (const plugin of context.plugins) {
570
+ const publishResult = await handlePluginError(plugin, "publish", () => plugin.publish?.call(context, {
571
+ pkg,
572
+ plan
573
+ }));
574
+ if (publishResult) {
575
+ published = true;
576
+ await onPublishResult(pkg, publishResult);
577
+ break;
578
+ }
579
+ }
580
+ if (!published) await onPublishResult(pkg, {
581
+ type: "failed",
582
+ error: `There is no plugin to publish package "${pkg.id}", please make sure the package has a supported provider plugin.`
583
+ });
584
+ }
585
+ await Promise.all(Array.from(plan.packages, async ([id, packagePlan]) => {
586
+ const pkg = context.graph.get(id);
587
+ if (!packagePlan.publishResult) await onPublishResult(pkg, { type: "skipped" });
588
+ }));
589
+ for (const plugin of context.plugins) await handlePluginError(plugin, "afterPublishAll", () => plugin.afterPublishAll?.call(context, { plan }));
590
+ }
591
+ async function runPreflights(context, plan) {
592
+ const promises = [];
593
+ const runPreflight = async (pkg) => {
594
+ const out = {};
595
+ for (const plugin of context.plugins) {
596
+ const res = await handlePluginError(plugin, "publishPreflight", () => plugin.publishPreflight?.call(context, {
597
+ pkg,
598
+ plan
599
+ }));
600
+ if (res?.publish !== void 0) out.publish = res.publish;
601
+ if (res?.wait) {
602
+ out.wait ??= [];
603
+ out.wait.push(...res.wait);
604
+ }
605
+ }
606
+ return out;
607
+ };
608
+ for (const [id, packagePlan] of plan.packages) {
609
+ const pkg = context.graph.get(id);
610
+ if (!packagePlan.updated) {
611
+ packagePlan.preflight = { publish: false };
612
+ continue;
613
+ }
614
+ promises.push(runPreflight(pkg).then((preflight) => {
615
+ packagePlan.preflight = preflight;
616
+ }));
617
+ }
618
+ await Promise.all(promises);
619
+ }
620
+ async function publishPlanStatus(plan, context) {
621
+ for (const pkg of plan.packages.values()) {
622
+ if (!pkg.preflight) throw new Error("Should perform preflight before checking plan status.");
623
+ if (pkg.preflight.publish ?? true) return "pending";
624
+ }
625
+ try {
626
+ await Promise.all(context.plugins.map(async (plugin) => {
627
+ if (await handlePluginError(plugin, "resolvePlanStatus", () => plugin.resolvePlanStatus?.call(context, { plan })) === "pending") throw "pending";
628
+ }));
629
+ return "success";
630
+ } catch (e) {
631
+ if (e === "pending") return "pending";
632
+ throw e;
633
+ }
292
634
  }
293
635
  //#endregion
294
636
  //#region src/index.ts
@@ -299,8 +641,15 @@ function tegami(options = {}) {
299
641
  return createTegamiContext(options);
300
642
  }
301
643
  return {
302
- async generateChangelog(createOptions = {}) {
303
- return generateChangelog(await $context, createOptions);
644
+ async generateChangelog(generateOptions = {}) {
645
+ const { write = true } = generateOptions;
646
+ const context = await $context;
647
+ const changelogs = await generateFromCommits(context, generateOptions);
648
+ if (write && changelogs.length > 0) {
649
+ await mkdir(context.changelogDir, { recursive: true });
650
+ await Promise.all(changelogs.map((entry) => writeFile(join(context.changelogDir, entry.filename), entry.content)));
651
+ }
652
+ return changelogs;
304
653
  },
305
654
  _internal: {
306
655
  options,
@@ -313,25 +662,49 @@ function tegami(options = {}) {
313
662
  },
314
663
  async draft() {
315
664
  const context = await $context;
316
- return createDraftPlan(await readChangelogEntries(context), context);
665
+ const changelogs = await readChangelogEntries(context);
666
+ if (context.options.conventionalCommits) {
667
+ const generated = await generateFromCommits(context);
668
+ for (const entry of generated) {
669
+ const parsed = parseChangelogFile(entry.filename, entry.content);
670
+ if (!parsed) continue;
671
+ parsed.virtual = true;
672
+ changelogs.push(parsed);
673
+ }
674
+ }
675
+ return createDraft(changelogs, context);
676
+ },
677
+ async publishStatus() {
678
+ const context = await $context;
679
+ const plan = await initPublishPlan(context, {});
680
+ if (!plan) return "idle";
681
+ await runPreflights(context, plan);
682
+ return publishPlanStatus(plan, context);
317
683
  },
318
684
  async publish(publishOptions = {}) {
319
685
  const context = await $context;
320
- const parsed = await readPlanStore(context);
321
- if (!parsed) return { state: "skipped" };
322
- let result = await publishFromPlan(context, parsed, publishOptions);
323
- const publishCtx = {
324
- ...context,
325
- publishOptions
326
- };
327
- for (const plugin of context.plugins) {
328
- const next = await handlePluginError(plugin, "afterPublishAll", () => plugin.afterPublishAll?.call(publishCtx, result));
329
- if (next) result = next;
330
- }
331
- return result;
686
+ const plan = await initPublishPlan(context, publishOptions);
687
+ if (!plan) return "skipped";
688
+ await runPreflights(context, plan);
689
+ if (await publishPlanStatus(plan, context) === "success") return "skipped";
690
+ await runPublishPlan(context, plan);
691
+ if (Array.from(plan.packages.values()).every((pkg) => pkg.publishResult.type === "skipped")) return "skipped";
692
+ return plan;
332
693
  },
333
694
  async cleanup() {
334
- return cleanupPublishPlan(await $context);
695
+ const context = await $context;
696
+ const plan = await initPublishPlan(context, {});
697
+ if (!plan) return {
698
+ state: "skipped",
699
+ reason: "no-plan"
700
+ };
701
+ await runPreflights(context, plan);
702
+ if (await publishPlanStatus(plan, context) !== "success") return {
703
+ state: "skipped",
704
+ reason: "pending"
705
+ };
706
+ await rm(context.lockPath, { force: true });
707
+ return { state: "removed" };
335
708
  }
336
709
  };
337
710
  }