@bridge-ds/core 0.8.0 → 0.10.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 (36) hide show
  1. package/README.md +27 -1
  2. package/bridge.config.schema.json +23 -0
  3. package/dist/audit/index.d.ts +1 -0
  4. package/dist/audit/index.js +269 -23
  5. package/dist/audit/index.js.map +1 -1
  6. package/dist/audit/missing-pieces.d.ts +43 -0
  7. package/dist/config.d.ts +28 -0
  8. package/dist/docs-proposal.d.ts +58 -10
  9. package/dist/figma-push-github.d.ts +143 -0
  10. package/dist/figma-push.d.ts +204 -0
  11. package/dist/github-client.d.ts +15 -1
  12. package/dist/index.d.ts +9 -0
  13. package/dist/index.js +1569 -3179
  14. package/dist/index.js.map +1 -1
  15. package/dist/node/git-local.d.ts +15 -0
  16. package/dist/node/index.d.ts +1 -0
  17. package/dist/node/index.js +201 -10
  18. package/dist/node/index.js.map +1 -1
  19. package/dist/node/suggestion-apply-node.d.ts +69 -0
  20. package/dist/{component-spec-SKuywZqr.js → prod-branch-BdMPKffE.js} +1134 -1438
  21. package/dist/prod-branch-BdMPKffE.js.map +1 -0
  22. package/dist/prod-branch-github.d.ts +160 -0
  23. package/dist/prod-branch.d.ts +267 -0
  24. package/dist/{report-iPSnsjYi.js → report-DOnKUw8G.js} +24 -24
  25. package/dist/{report-iPSnsjYi.js.map → report-DOnKUw8G.js.map} +1 -1
  26. package/dist/suggestion-apply-ByBEYtct.js +4802 -0
  27. package/dist/suggestion-apply-ByBEYtct.js.map +1 -0
  28. package/dist/suggestion-apply.d.ts +150 -0
  29. package/dist/suggestion-review.d.ts +70 -0
  30. package/dist/suggestions-github.d.ts +131 -0
  31. package/dist/suggestions.d.ts +236 -0
  32. package/dist/token-proposal.d.ts +123 -0
  33. package/package.json +1 -1
  34. package/dist/component-spec-SKuywZqr.js.map +0 -1
  35. package/dist/layered-BnbmToGs.js +0 -500
  36. package/dist/layered-BnbmToGs.js.map +0 -1
@@ -29,3 +29,18 @@ export declare function commitToNewLocalBranch(root: string, args: LocalCommitIn
29
29
  sha: string;
30
30
  diff: string;
31
31
  }>;
32
+ /**
33
+ * A commit with `files` on top of `base`, built in a throwaway index and pointed to by no branch: nothing in the working
34
+ * tree, the index or the refs changes. commitToNewLocalBranch names it; a patch (git format-patch) can be read from it.
35
+ */
36
+ export declare function commitTree(root: string, args: {
37
+ base: string;
38
+ message: string;
39
+ files: {
40
+ path: string;
41
+ content: string | null;
42
+ }[];
43
+ }): Promise<{
44
+ sha: string;
45
+ parent: string;
46
+ }>;
@@ -4,3 +4,4 @@ export * from './audit';
4
4
  export * from './component-specs';
5
5
  export * from './spec-css';
6
6
  export * from './spec-motion';
7
+ export * from './suggestion-apply-node';
@@ -1,11 +1,11 @@
1
1
  import { execFile } from "node:child_process";
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { mkdtemp, rm, mkdir, writeFile, lstat, readFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join, resolve, relative, dirname, sep, posix } from "node:path";
5
- import { a as assertSafeBranch, c as collectHistory, p as packageDirCandidates, b as parseChangelog, m as mergeReleases, r as releasesFromTags, d as buildReport, e as prNumberFromSubject, l as loadConfig, s as specHash, v as validateComponentSpec, h as hashSpec, i as iconComponentName, f as variantKeyOf, C as CONFIG_FILE, g as layoutOptions, j as buildTokenSet, k as defaultContext, t as tokenCssVar, n as resolve$1, o as COMPONENT_SPECS_VERSION, q as COLOR_FIELDS } from "../component-spec-SKuywZqr.js";
6
- import { s as selectTokenFiles, g as globToRegExp, L as LayeredTokenSet } from "../layered-BnbmToGs.js";
5
+ import { aT as assertSafeBranch, c as collectHistory, p as packageDirCandidates, a as parseChangelog, m as mergeReleases, r as releasesFromTags, b as buildReport, cq as prNumberFromSubject, cN as specHash, d9 as validateComponentSpec, bM as hashSpec, bN as iconComponentName, C as variantKeyOf, d5 as tokenCssVar, ai as COMPONENT_SPECS_VERSION, ah as COLOR_FIELDS, cQ as suggestionAppliedComment, X as gitBlobSha, co as planSuggestionApply } from "../suggestion-apply-ByBEYtct.js";
6
+ import { K as selectTokenFiles, bl as globToRegExp, N as loadConfig, L as LayeredTokenSet, V as CONFIG_FILE, $ as layoutOptions, a0 as buildTokenSet, b3 as defaultContext, B as resolve$1, ae as slugify, bX as protectedBranchesOf, aM as baseBranchOf } from "../prod-branch-BdMPKffE.js";
7
7
  import { statSync, readFileSync, existsSync, writeFileSync, readdirSync } from "node:fs";
8
- import { v as isAuditable, Q as planCssFixes, O as parseAllowlist, c as DesignSystemIndex, k as buildAuditReport, n as compareWithBaseline, a as AUDIT_IGNORE_DIRS } from "../report-iPSnsjYi.js";
8
+ import { i as isAuditable, Q as planCssFixes, O as parseAllowlist, d as DesignSystemIndex, l as buildAuditReport, o as compareWithBaseline, b as AUDIT_IGNORE_DIRS } from "../report-DOnKUw8G.js";
9
9
  import ts from "typescript";
10
10
  function git(root, args, opts = {}) {
11
11
  return new Promise((resolve2, reject) => {
@@ -59,11 +59,17 @@ async function commitToNewLocalBranch(root, args) {
59
59
  throw new Error(`Refusing to write branch "${args.branch}": ${err.message}`);
60
60
  }
61
61
  if (await branchExists(root, args.branch)) throw new Error(`Branch "${args.branch}" already exists. Pick another title or delete it first.`);
62
- const baseSha = (await git(root, ["rev-parse", "--verify", `${args.base}^{commit}`])).trim();
62
+ const { sha, parent } = await commitTree(root, args);
63
+ await git(root, ["update-ref", `refs/heads/${args.branch}`, sha, ""]);
64
+ const diff = await git(root, ["diff", parent, sha]);
65
+ return { sha, diff };
66
+ }
67
+ async function commitTree(root, args) {
68
+ const parent = (await git(root, ["rev-parse", "--verify", `${args.base}^{commit}`])).trim();
63
69
  const dir = await mkdtemp(join(tmpdir(), "bridge-index-"));
64
70
  const env = { GIT_INDEX_FILE: join(dir, "index") };
65
71
  try {
66
- await git(root, ["read-tree", baseSha], { env });
72
+ await git(root, ["read-tree", parent], { env });
67
73
  for (const f of args.files) {
68
74
  if (f.content === null) {
69
75
  await git(root, ["update-index", "--force-remove", f.path], { env });
@@ -74,10 +80,8 @@ async function commitToNewLocalBranch(root, args) {
74
80
  }
75
81
  const tree = (await git(root, ["write-tree"], { env })).trim();
76
82
  const ident = await identityEnv(root);
77
- const sha = (await git(root, ["commit-tree", tree, "-p", baseSha, "-F", "-"], { input: args.message, env: ident })).trim();
78
- await git(root, ["update-ref", `refs/heads/${args.branch}`, sha, ""]);
79
- const diff = await git(root, ["diff", baseSha, sha]);
80
- return { sha, diff };
83
+ const sha = (await git(root, ["commit-tree", tree, "-p", parent, "-F", "-"], { input: args.message, env: ident })).trim();
84
+ return { sha, parent };
81
85
  } finally {
82
86
  await rm(dir, { recursive: true, force: true });
83
87
  }
@@ -3611,6 +3615,188 @@ function generateRepoComponentSpecs(root, names) {
3611
3615
  }
3612
3616
  const serializeComponentSpecs = (f) => `${JSON.stringify(f, null, 2)}
3613
3617
  `;
3618
+ function gitCode(root, args) {
3619
+ return new Promise((resolve2) => {
3620
+ execFile("git", args, { cwd: root, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
3621
+ const code = err ? typeof err.code === "number" ? err.code : -1 : 0;
3622
+ resolve2({ code, stdout, stderr });
3623
+ });
3624
+ });
3625
+ }
3626
+ async function treeEntry(root, ref, path) {
3627
+ const r = await gitCode(root, ["ls-tree", ref, "--", path]);
3628
+ const m = /^(\d+) (\w+) ([0-9a-f]+)\t/.exec(r.stdout);
3629
+ return r.code === 0 && m ? { mode: m[1], type: m[2], sha: m[3] } : null;
3630
+ }
3631
+ const prefixes = (path) => path.split("/").map((_, i, segs) => segs.slice(0, i + 1).join("/"));
3632
+ async function mergeText(root, input) {
3633
+ const dir = await mkdtemp(join(tmpdir(), "bridge-merge-"));
3634
+ try {
3635
+ const [o, b, t] = ["current", "base", "suggested"].map((n) => join(dir, n));
3636
+ await Promise.all([writeFile(o, input.ours), writeFile(b, input.base), writeFile(t, input.theirs)]);
3637
+ const r = await gitCode(root, ["merge-file", "-p", "-L", "current", "-L", "base", "-L", "suggested", o, b, t]);
3638
+ if (r.code < 0 || r.code > 127) throw new Error(`git merge-file failed: ${r.stderr.trim() || `exit ${r.code}`}`);
3639
+ return { clean: r.code === 0, text: r.stdout, conflicts: r.code };
3640
+ } finally {
3641
+ await rm(dir, { recursive: true, force: true });
3642
+ }
3643
+ }
3644
+ async function fileDiff(path, before, after) {
3645
+ if (before === after) return "";
3646
+ const dir = await mkdtemp(join(tmpdir(), "bridge-diff-"));
3647
+ try {
3648
+ const side = async (name, text) => {
3649
+ if (text === null) return "/dev/null";
3650
+ const p = join(dir, name, path);
3651
+ await mkdir(dirname(p), { recursive: true });
3652
+ await writeFile(p, text);
3653
+ return `${name}/${path}`;
3654
+ };
3655
+ const a = await side("a", before);
3656
+ const b = await side("b", after);
3657
+ const r = await new Promise((resolve2) => execFile("git", ["diff", "--no-index", "--no-color", "--src-prefix=", "--dst-prefix=", "--", a, b], { cwd: dir, maxBuffer: 32 * 1024 * 1024 }, (_e, stdout) => resolve2({ stdout })));
3658
+ return r.stdout;
3659
+ } finally {
3660
+ await rm(dir, { recursive: true, force: true });
3661
+ }
3662
+ }
3663
+ function gitWorkspace(root, opts = {}) {
3664
+ const { ref } = opts;
3665
+ let fetched = false;
3666
+ return {
3667
+ async read(path) {
3668
+ if (ref) {
3669
+ const e = await treeEntry(root, ref, path);
3670
+ if (!e || e.type !== "blob" || e.mode === "120000") return null;
3671
+ return { text: await git(root, ["cat-file", "blob", e.sha]), sha: e.sha };
3672
+ }
3673
+ try {
3674
+ const st = await lstat(join(root, path));
3675
+ if (!st.isFile()) return null;
3676
+ const text = await readFile(join(root, path), "utf8");
3677
+ return { text, sha: gitBlobSha(text) };
3678
+ } catch {
3679
+ return null;
3680
+ }
3681
+ },
3682
+ async readAt(at, path) {
3683
+ if ((await gitCode(root, ["cat-file", "-e", `${at}^{commit}`])).code !== 0 && !fetched) {
3684
+ fetched = true;
3685
+ await gitCode(root, ["fetch", "--no-tags", "--quiet", "origin", at]);
3686
+ }
3687
+ const e = await treeEntry(root, at, path);
3688
+ if (e?.type === "blob") return git(root, ["cat-file", "blob", e.sha]);
3689
+ return opts.readBase ? opts.readBase(path, at) : null;
3690
+ },
3691
+ async list() {
3692
+ if (ref) return listFilesAt(root, ref);
3693
+ const out = await git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
3694
+ const all = [...new Set(out.split("\0").filter(Boolean))];
3695
+ const present = await Promise.all(all.map((p) => lstat(join(root, p)).then((s) => s.isFile(), () => false)));
3696
+ return all.filter((_, i) => present[i]);
3697
+ },
3698
+ async isSymlink(path) {
3699
+ for (const p of prefixes(path)) {
3700
+ if (ref) {
3701
+ const e = await treeEntry(root, ref, p);
3702
+ if (!e) return false;
3703
+ if (e.mode === "120000") return true;
3704
+ } else {
3705
+ try {
3706
+ if ((await lstat(join(root, p))).isSymbolicLink()) return true;
3707
+ } catch {
3708
+ return false;
3709
+ }
3710
+ }
3711
+ }
3712
+ return false;
3713
+ },
3714
+ merge: (input) => mergeText(root, input)
3715
+ };
3716
+ }
3717
+ const COMMIT_TYPE = { "token-change": "feat(tokens)", docs: "docs", "component-spec": "feat", brand: "feat(tokens)", revert: "revert", config: "chore(config)" };
3718
+ function commitMessage(plan, n) {
3719
+ const p = plan.payload;
3720
+ const type = `${COMMIT_TYPE[p.kind] ?? "chore"}${p.tier.bump === "major" ? "!" : ""}`;
3721
+ return [`${type}: ${p.title.trim()}`, "", p.summary.trim(), "", `Applies Bridge suggestion #${n} (version ${plan.payloadSha.slice(0, 7)}).`].filter((l, i, a) => !(l === "" && a[i - 1] === "")).join("\n");
3722
+ }
3723
+ async function readConfigAt(ws) {
3724
+ const text = (await ws.read(CONFIG_FILE))?.text;
3725
+ if (!text) throw new Error(`No ${CONFIG_FILE} here: run \`bridge init\` first, or apply from the repository root.`);
3726
+ return loadConfig(text);
3727
+ }
3728
+ async function applySuggestionLocally(input) {
3729
+ const mode = input.mode ?? "branch";
3730
+ const base = input.base ?? "HEAD";
3731
+ const { root } = input;
3732
+ const wsOpts = { readBase: input.readBase };
3733
+ const ws = gitWorkspace(root, mode === "write" ? wsOpts : { ...wsOpts, ref: base });
3734
+ const config = input.config ?? await readConfigAt(ws);
3735
+ const planFor = (workspace) => planSuggestionApply({ ...input, config, workspace });
3736
+ const plan = await planFor(ws);
3737
+ const at = (input.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
3738
+ const result = (status, rest = {}) => ({ status, exitCode: plan.exitCode, message: plan.message, plan, ...rest });
3739
+ if (plan.outcome !== "ready") return result(plan.outcome);
3740
+ const readNow = async (path) => (await ws.read(path))?.text ?? null;
3741
+ const diffOf = async () => (await Promise.all(plan.writes.map(async (w) => fileDiff(w.path, await readNow(w.path), w.content)))).join("");
3742
+ if (input.dryRun) return result("dry-run", { message: `Dry run: ${plan.message.replace(/^Ready: /, "")} Nothing written.`, diff: await diffOf() });
3743
+ if (mode === "patch") {
3744
+ const { sha } = await commitTree(root, { base, message: commitMessage(plan, input.number), files: plan.writes });
3745
+ const patch = await git(root, ["format-patch", "-1", "--stdout", sha]);
3746
+ return result("patch", { message: "Patch ready: apply it with `git am`.", patch, commit: sha });
3747
+ }
3748
+ if (mode === "write") {
3749
+ const paths = plan.writes.map((w) => w.path);
3750
+ const dirty = (await git(root, ["status", "--porcelain", "-z", "--", ...paths])).split("\0").filter((l) => l.length > 3).map((l) => l.slice(3));
3751
+ if (dirty.length) {
3752
+ return result("refused", {
3753
+ exitCode: 1,
3754
+ message: `Not written: these files have uncommitted changes: ${dirty.join(", ")}. Commit or stash them, or apply on a branch instead.`
3755
+ });
3756
+ }
3757
+ const diff = await diffOf();
3758
+ for (const w of plan.writes) {
3759
+ const full = join(root, w.path);
3760
+ if (w.content === null) await rm(full, { force: true });
3761
+ else {
3762
+ await mkdir(dirname(full), { recursive: true });
3763
+ await writeFile(full, w.content);
3764
+ }
3765
+ }
3766
+ return result("written", {
3767
+ message: `Written: ${paths.length} file(s) in the working tree. Review them, commit, and say "Closes #${input.number}" in the pull request.`,
3768
+ written: paths,
3769
+ diff,
3770
+ appliedComment: suggestionAppliedComment(plan, { by: input.by, at })
3771
+ });
3772
+ }
3773
+ const prefix = config.governance.branchPrefix;
3774
+ const wanted = input.branch ?? `${prefix}suggestion-${input.number}-${slugify(plan.payload.title, 40)}`;
3775
+ let branch = wanted;
3776
+ for (let i = 2; await branchExists(root, branch); i++) {
3777
+ const there = await planFor(gitWorkspace(root, { ...wsOpts, ref: branch }));
3778
+ if (there.outcome === "already-applied") {
3779
+ const commit = (await git(root, ["rev-parse", `refs/heads/${branch}`])).trim();
3780
+ return result("branch-exists", {
3781
+ exitCode: 0,
3782
+ message: `Already on ${branch}: that branch has every change of this suggestion. Push it and open a pull request that says "Closes #${input.number}".`,
3783
+ branch,
3784
+ commit,
3785
+ appliedComment: suggestionAppliedComment(plan, { by: input.by, branch, commit, at })
3786
+ });
3787
+ }
3788
+ branch = `${wanted}-${i}`;
3789
+ }
3790
+ const protectedBranches = [.../* @__PURE__ */ new Set([...protectedBranchesOf(config), baseBranchOf(config)])];
3791
+ const res = await commitToNewLocalBranch(root, { base, branch, message: commitMessage(plan, input.number), files: plan.writes, protectedBranches, branchPrefix: prefix });
3792
+ return result("branch-created", {
3793
+ message: `Applied on ${branch}, your working tree is untouched. Push it and open a pull request that says "Closes #${input.number}".`,
3794
+ branch,
3795
+ commit: res.sha,
3796
+ diff: res.diff,
3797
+ appliedComment: suggestionAppliedComment(plan, { by: input.by, branch, commit: res.sha, at })
3798
+ });
3799
+ }
3614
3800
  export {
3615
3801
  COMMON_ICONS,
3616
3802
  DEFAULT_STATE,
@@ -3618,10 +3804,12 @@ export {
3618
3804
  INHERITED,
3619
3805
  MEDIA_SLOT,
3620
3806
  UnsupportedComponentError,
3807
+ applySuggestionLocally,
3621
3808
  branchExists,
3622
3809
  changedProperties,
3623
3810
  collectAuditFiles,
3624
3811
  commitToNewLocalBranch,
3812
+ commitTree,
3625
3813
  computeValues,
3626
3814
  currentBranch,
3627
3815
  declaredFor,
@@ -3629,6 +3817,7 @@ export {
3629
3817
  draftedSpec,
3630
3818
  entryFor,
3631
3819
  expandDecl,
3820
+ fileDiff,
3632
3821
  findDesignSystemRoot,
3633
3822
  fixCss,
3634
3823
  generateAllComponentSpecs,
@@ -3638,6 +3827,7 @@ export {
3638
3827
  generateRepoComponentSpecs,
3639
3828
  generateSlots,
3640
3829
  git,
3830
+ gitWorkspace,
3641
3831
  isGitRepo,
3642
3832
  jsxText,
3643
3833
  listFilesAt,
@@ -3648,6 +3838,7 @@ export {
3648
3838
  localTags,
3649
3839
  matchComplex,
3650
3840
  mediaApplies,
3841
+ mergeText,
3651
3842
  motionNotes,
3652
3843
  openingTransitions,
3653
3844
  originRepo,