@savvy-web/silk-effects 7.3.2 → 7.5.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.
@@ -1,6 +1,19 @@
1
1
  import { getHeadingText, getVersionBlocks } from "../../utils/version-blocks.js";
2
+ import { toString } from "mdast-util-to-string";
2
3
 
3
4
  //#region src/changesets/remark/plugins/maintenance-note.ts
5
+ /**
6
+ * Placeholder sentence `@changesets/apply-release-plan` (>= 8.1.0) emits as the
7
+ * body of a version block that would otherwise render as a bare heading. Older
8
+ * engine versions left such a block structurally empty.
9
+ */
10
+ const ENGINE_PLACEHOLDER = "No changes in this release.";
11
+ /** True when the block's only content is the engine's empty-release placeholder. */
12
+ function isPlaceholderOnly(tree, block) {
13
+ if (block.endIndex - block.startIndex !== 1) return false;
14
+ const node = tree.children[block.startIndex];
15
+ return node.type === "paragraph" && toString(node).trim() === ENGINE_PLACEHOLDER;
16
+ }
4
17
  function buildNoteChildren(reason) {
5
18
  if (reason.kind === "unspecified" || reason.triggers.length === 0) return [{
6
19
  type: "text",
@@ -31,7 +44,8 @@ const MaintenanceNotePlugin = (options) => {
31
44
  return (tree) => {
32
45
  const block = getVersionBlocks(tree).find((b) => getHeadingText(tree.children[b.headingIndex]) === options.version);
33
46
  if (!block) return;
34
- if (block.endIndex > block.startIndex) return;
47
+ const placeholderOnly = isPlaceholderOnly(tree, block);
48
+ if (block.endIndex > block.startIndex && !placeholderOnly) return;
35
49
  const heading = {
36
50
  type: "heading",
37
51
  depth: 3,
@@ -53,7 +67,7 @@ const MaintenanceNotePlugin = (options) => {
53
67
  }]
54
68
  }]
55
69
  };
56
- tree.children.splice(block.startIndex, 0, heading, list);
70
+ tree.children.splice(block.startIndex, placeholderOnly ? 1 : 0, heading, list);
57
71
  };
58
72
  };
59
73
 
@@ -52,7 +52,12 @@ const ResolvedVersionFileSchema = Schema.Struct({
52
52
  const ResolvedPackageScopeSchema = Schema.Struct({
53
53
  name: Schema.String,
54
54
  workspaceDir: Schema.String,
55
- version: Schema.String,
55
+ /**
56
+ * The package's declared version, absent when its manifest carries none — legal for a private
57
+ * package and the ordinary shape for a private monorepo root. A scope with no version has
58
+ * nothing to stamp into its `versionFiles` targets, so `VersionFiles.applyScopes` skips it.
59
+ */
60
+ version: Schema.optional(Schema.String),
56
61
  additionalScopes: Schema.Array(Schema.String),
57
62
  additionalScopeFiles: Schema.Array(Schema.String),
58
63
  versionFiles: Schema.Array(ResolvedVersionFileSchema)
@@ -368,11 +368,15 @@ var VersionFiles = class VersionFiles {
368
368
  static processResolvedVersionFiles(scopes, dryRun = false) {
369
369
  return Effect.gen(function* () {
370
370
  const updates = [];
371
- for (const scope of scopes) for (const vf of scope.versionFiles) {
372
- const jsonPaths = vf.paths.length > 0 ? vf.paths : ["$.version"];
373
- for (const filePath of vf.matchedFiles) {
374
- const result = yield* VersionFiles.applyOne(filePath, jsonPaths, scope.version, dryRun);
375
- if (result) updates.push(result);
371
+ for (const scope of scopes) {
372
+ if (scope.version === void 0) continue;
373
+ const version = scope.version;
374
+ for (const vf of scope.versionFiles) {
375
+ const jsonPaths = vf.paths.length > 0 ? vf.paths : ["$.version"];
376
+ for (const filePath of vf.matchedFiles) {
377
+ const result = yield* VersionFiles.applyOne(filePath, jsonPaths, version, dryRun);
378
+ if (result) updates.push(result);
379
+ }
376
380
  }
377
381
  }
378
382
  return updates;
package/index.d.ts CHANGED
@@ -2805,7 +2805,7 @@ declare const ChangesetConfigError_base: new <A extends Record<string, any> = {}
2805
2805
  * @since 0.1.0
2806
2806
  * @public
2807
2807
  */
2808
- declare class ChangesetConfigError extends ChangesetConfigError_base<{
2808
+ export declare class ChangesetConfigError extends ChangesetConfigError_base<{
2809
2809
  readonly path: string;
2810
2810
  readonly reason: string;
2811
2811
  }> {
@@ -2932,7 +2932,7 @@ declare const ChangesetConfigReader_base: Context.ServiceClass<ChangesetConfigRe
2932
2932
  * @since 0.1.0
2933
2933
  * @public
2934
2934
  */
2935
- declare class ChangesetConfigReader extends ChangesetConfigReader_base {
2935
+ export declare class ChangesetConfigReader extends ChangesetConfigReader_base {
2936
2936
  /**
2937
2937
  * Production implementation of {@link ChangesetConfigReader}.
2938
2938
  *
@@ -2959,7 +2959,12 @@ type ResolvedVersionFile = typeof ResolvedVersionFileSchema.Type;
2959
2959
  declare const ResolvedPackageScopeSchema: Schema.Struct<{
2960
2960
  readonly name: Schema.String;
2961
2961
  readonly workspaceDir: Schema.String;
2962
- readonly version: Schema.String;
2962
+ /**
2963
+ * The package's declared version, absent when its manifest carries none — legal for a private
2964
+ * package and the ordinary shape for a private monorepo root. A scope with no version has
2965
+ * nothing to stamp into its `versionFiles` targets, so `VersionFiles.applyScopes` skips it.
2966
+ */
2967
+ readonly version: Schema.optional<Schema.String>;
2963
2968
  readonly additionalScopes: Schema.$Array<Schema.String>;
2964
2969
  readonly additionalScopeFiles: Schema.$Array<Schema.String>;
2965
2970
  readonly versionFiles: Schema.$Array<Schema.Struct<{
@@ -2981,7 +2986,12 @@ declare const InspectedConfigSchema: Schema.Struct<{
2981
2986
  readonly packages: Schema.$Array<Schema.Struct<{
2982
2987
  readonly name: Schema.String;
2983
2988
  readonly workspaceDir: Schema.String;
2984
- readonly version: Schema.String;
2989
+ /**
2990
+ * The package's declared version, absent when its manifest carries none — legal for a private
2991
+ * package and the ordinary shape for a private monorepo root. A scope with no version has
2992
+ * nothing to stamp into its `versionFiles` targets, so `VersionFiles.applyScopes` skips it.
2993
+ */
2994
+ readonly version: Schema.optional<Schema.String>;
2985
2995
  readonly additionalScopes: Schema.$Array<Schema.String>;
2986
2996
  readonly additionalScopeFiles: Schema.$Array<Schema.String>;
2987
2997
  readonly versionFiles: Schema.$Array<Schema.Struct<{
@@ -3670,7 +3680,7 @@ declare const ChangesetConfig_base: Context.ServiceClass<ChangesetConfig, "@savv
3670
3680
  * @since 0.4.0
3671
3681
  * @public
3672
3682
  */
3673
- declare class ChangesetConfig extends ChangesetConfig_base {
3683
+ export declare class ChangesetConfig extends ChangesetConfig_base {
3674
3684
  /**
3675
3685
  * The one ignore matcher: exact name match, or `@scope/*` wildcard.
3676
3686
  *
@@ -6801,7 +6811,7 @@ declare const BiomeSyncError_base: new <A extends Record<string, any> = {}>(args
6801
6811
  * @since 0.1.0
6802
6812
  * @public
6803
6813
  */
6804
- declare class BiomeSyncError extends BiomeSyncError_base<{
6814
+ export declare class BiomeSyncError extends BiomeSyncError_base<{
6805
6815
  readonly path: string;
6806
6816
  readonly reason: string;
6807
6817
  }> {
@@ -6823,7 +6833,7 @@ declare const ConfigNotFoundError_base: new <A extends Record<string, any> = {}>
6823
6833
  * @since 0.1.0
6824
6834
  * @public
6825
6835
  */
6826
- declare class ConfigNotFoundError extends ConfigNotFoundError_base<{
6836
+ export declare class ConfigNotFoundError extends ConfigNotFoundError_base<{
6827
6837
  readonly name: string;
6828
6838
  readonly searchedPaths: ReadonlyArray<string>;
6829
6839
  }> {
@@ -6853,7 +6863,7 @@ declare const PublishTargetBindingError_base: new <A extends Record<string, any>
6853
6863
  * @since 3.1.0
6854
6864
  * @public
6855
6865
  */
6856
- declare class PublishTargetBindingError extends PublishTargetBindingError_base<{
6866
+ export declare class PublishTargetBindingError extends PublishTargetBindingError_base<{
6857
6867
  /** The package whose targets were being resolved. */
6858
6868
  readonly pkg: string;
6859
6869
  /** The directory detection selected, relative to the package root. */
@@ -6879,7 +6889,7 @@ declare const WorkspaceAnalysisError_base: new <A extends Record<string, any> =
6879
6889
  * @since 0.2.0
6880
6890
  * @public
6881
6891
  */
6882
- declare class WorkspaceAnalysisError extends WorkspaceAnalysisError_base<{
6892
+ export declare class WorkspaceAnalysisError extends WorkspaceAnalysisError_base<{
6883
6893
  readonly root: string;
6884
6894
  readonly reason: string;
6885
6895
  }> {
@@ -9832,6 +9842,148 @@ declare const ConfigDiscoveryOptions: Schema.Struct<{
9832
9842
  */
9833
9843
  type ConfigDiscoveryOptions = typeof ConfigDiscoveryOptions.Type;
9834
9844
  //#endregion
9845
+ //#region src/schemas/SavvyInstallSection.d.ts
9846
+ /**
9847
+ * The hooks {@link savvyInstallDeps} can be generated for.
9848
+ *
9849
+ * @remarks
9850
+ * Both are needed, and `post-merge` is the one that matters most in practice:
9851
+ * a fast-forward `git pull` — the batch-alignment case this exists for — fires
9852
+ * `post-merge` and never `post-checkout`. They are separate spellings rather
9853
+ * than one argument-sniffing block because the two hooks disagree on both
9854
+ * halves of the decision: `post-checkout` is handed `<prev> <new> <flag>` and
9855
+ * must ignore a file checkout, while `post-merge` is handed only a squash flag
9856
+ * and has to recover its own range from `ORIG_HEAD`.
9857
+ *
9858
+ * @since 7.5.0
9859
+ * @public
9860
+ */
9861
+ type SavvyInstallHook = "post-checkout" | "post-merge";
9862
+ /**
9863
+ * Section identity for the dependency auto-install block.
9864
+ *
9865
+ * `toolName` is `"savvy-install"`; pair with {@link savvyInstallDeps}.
9866
+ *
9867
+ * @since 7.5.0
9868
+ * @public
9869
+ */
9870
+ export declare const SavvyInstallSection: SectionId;
9871
+ /**
9872
+ * Dependency auto-install shared across Silk Suite hook files.
9873
+ *
9874
+ * @remarks
9875
+ * Brings `node_modules` back in line after a branch switch or a pull, so a batch
9876
+ * of repos pulled to align their dependencies does not each need a manual
9877
+ * install. Installs with lifecycle scripts skipped — `--ignore-scripts`, or
9878
+ * `--mode=skip-build` for Yarn Berry, which dropped the former — because the goal
9879
+ * is to get the dependency tree on disk, not to run a full postinstall.
9880
+ *
9881
+ * **That flag has a cost, and one shape of repo cannot pay it.** Where a package
9882
+ * publishes through a built link directory — `publishConfig.directory` with
9883
+ * `linkDirectory: true`, which is how this monorepo wires `dist/dev/pkg` — the
9884
+ * workspace resolves its own dependencies through a directory that a `prepare`
9885
+ * script has to build, and skipping scripts yields a populated `node_modules`
9886
+ * pointing at nothing.
9887
+ *
9888
+ * Those repos ask for a full install through the LOCAL git config key
9889
+ * `savvy.installLifecycleScripts`, which `savvy init` sets when it finds the
9890
+ * shape. The decision deliberately does NOT read the checked-out tree. Doing so
9891
+ * failed in both directions at once: a branch that merely declared the shape
9892
+ * could turn lifecycle scripts back on just by being checked out, making
9893
+ * `git checkout` of an untrusted revision a code-execution path; and the `jq`
9894
+ * the scan needed is absent on stock macOS and Ubuntu, where the missing answer
9895
+ * silently skipped scripts in precisely the repos that cannot survive it.
9896
+ * `.git/config` is neither checked out nor parsed with `jq`, so it has neither
9897
+ * failure mode.
9898
+ *
9899
+ * The package managers gate dependency scripts themselves on top of this —
9900
+ * pnpm's `strictDepBuilds` with an `allowBuilds` allowlist being the case in
9901
+ * point — but workspace and root lifecycle scripts still run, which is why the
9902
+ * opt-in is local rather than inferred. With the flag on, the hook says on the
9903
+ * way out that scripts were skipped rather than leaving it to be discovered.
9904
+ *
9905
+ * Deliberately self-contained, like `savvyToolchainCheck`: its homes carry
9906
+ * `SavvyHooksSection` but no `SavvyBaseSection`, so it defines its own root, CI
9907
+ * and package-manager lookups rather than depending on `ROOT`, `in_ci` or `PM`.
9908
+ *
9909
+ * Four guards keep it from firing on the many checkouts that are not dependency
9910
+ * events, each a silent no-op:
9911
+ *
9912
+ * - CI, where the runtime action owns installation by construction.
9913
+ * - `SAVVY_SKIP_INSTALL`, the escape hatch for a bisect or a scripted sweep that
9914
+ * does not want an install between steps.
9915
+ * - `post-checkout` only: a branch-flag of `0`, which is `git checkout -- <file>`
9916
+ * and not a move between commits.
9917
+ * - **The gate that makes this affordable:** nothing dependency-related actually
9918
+ * changed across the move. Without it every branch switch would pay for a full
9919
+ * install. A missing `node_modules` overrides it, since there is nothing to be
9920
+ * stale.
9921
+ *
9922
+ * A fresh clone is not among the cases it handles, and cannot be: husky sets
9923
+ * `core.hooksPath` from its own `prepare` script, so until the first manual
9924
+ * install has run there is no hook installed to fire.
9925
+ *
9926
+ * The install's exit status is swallowed and its output goes to stderr. Git
9927
+ * ignores what `post-checkout` and `post-merge` return, so a failure here must
9928
+ * not look like a failed checkout; the hint names the escape hatch instead.
9929
+ *
9930
+ * Every probe that can legitimately fail — no `ORIG_HEAD` to compare against, an
9931
+ * unresolvable range — is neutralised with `|| true` rather than left to its own
9932
+ * status. These blocks are co-owned and consumer content can sit below them, so
9933
+ * under a hook running `set -e` a bare failing substitution would abort the whole
9934
+ * file and take those later sections with it.
9935
+ *
9936
+ * @param hook - Which hook the block is being generated for; decides the
9937
+ * argument guard and how the comparison range is recovered.
9938
+ * @returns The install shell, with no surrounding markers or trailing newline.
9939
+ *
9940
+ * @since 7.5.0
9941
+ * @public
9942
+ */
9943
+ export declare function savvyInstallDeps(hook: SavvyInstallHook): string;
9944
+ /**
9945
+ * The local git config key that authorizes lifecycle scripts during a hook install.
9946
+ *
9947
+ * @remarks
9948
+ * Local scope only. It lives in `.git/config`, which is never checked out, so no
9949
+ * incoming revision can set it — that is the whole point of reading the decision
9950
+ * from here rather than from a manifest in the tree.
9951
+ *
9952
+ * @since 7.5.0
9953
+ * @public
9954
+ */
9955
+ export declare const LIFECYCLE_SCRIPTS_CONFIG_KEY = "savvy.installLifecycleScripts";
9956
+ /**
9957
+ * Whether `manifest` publishes through a built link directory.
9958
+ *
9959
+ * @remarks
9960
+ * `publishConfig.directory` with `linkDirectory: true` means consumers of this
9961
+ * package resolve it through a directory that a `prepare` script has to produce,
9962
+ * so an install that skips lifecycle scripts leaves the link pointing at nothing.
9963
+ * A workspace containing any such package is one whose owner probably wants
9964
+ * {@link LIFECYCLE_SCRIPTS_CONFIG_KEY} set.
9965
+ *
9966
+ * Reporting the shape is deliberately separate from acting on it: this answers
9967
+ * "does this repo need scripts", and a human still decides whether hook-time
9968
+ * installs may run them.
9969
+ *
9970
+ * @param manifest - A parsed `package.json`; any non-object reads as `false`.
9971
+ *
9972
+ * @since 7.5.0
9973
+ * @public
9974
+ */
9975
+ export declare function publishesBuiltLinkDirectory(manifest: unknown): boolean;
9976
+ /**
9977
+ * Build the dependency auto-install block for `hook`.
9978
+ *
9979
+ * @param hook - Which hook the section is destined for.
9980
+ * @returns A shell `Section` (`commentStyle: hash`) keyed `SAVVY-INSTALL`.
9981
+ *
9982
+ * @since 7.5.0
9983
+ * @public
9984
+ */
9985
+ export declare function savvyInstallBlock(hook: SavvyInstallHook): Section;
9986
+ //#endregion
9835
9987
  //#region src/schemas/SavvySections.d.ts
9836
9988
  /**
9837
9989
  * Section identity for the shared package-manager preamble.
@@ -9846,7 +9998,7 @@ type ConfigDiscoveryOptions = typeof ConfigDiscoveryOptions.Type;
9846
9998
  * @since 0.5.0
9847
9999
  * @public
9848
10000
  */
9849
- declare const SavvyBaseSection: SectionId;
10001
+ export declare const SavvyBaseSection: SectionId;
9850
10002
  /**
9851
10003
  * Section identity for the shared repo-hygiene block.
9852
10004
  *
@@ -9855,7 +10007,7 @@ declare const SavvyBaseSection: SectionId;
9855
10007
  * @since 0.5.0
9856
10008
  * @public
9857
10009
  */
9858
- declare const SavvyHooksSection: SectionId;
10010
+ export declare const SavvyHooksSection: SectionId;
9859
10011
  /**
9860
10012
  * Package-manager detection preamble shared across Silk Suite hook files.
9861
10013
  *
@@ -9870,7 +10022,7 @@ declare const SavvyHooksSection: SectionId;
9870
10022
  * @since 0.5.0
9871
10023
  * @public
9872
10024
  */
9873
- declare function savvyBasePreamble(): string;
10025
+ export declare function savvyBasePreamble(): string;
9874
10026
  /**
9875
10027
  * Repo-hygiene block shared across Silk Suite hook files.
9876
10028
  *
@@ -9883,7 +10035,7 @@ declare function savvyBasePreamble(): string;
9883
10035
  * @since 0.5.0
9884
10036
  * @public
9885
10037
  */
9886
- declare function savvyHooksHygiene(): string;
10038
+ export declare function savvyHooksHygiene(): string;
9887
10039
  /**
9888
10040
  * Build a consumer's one-line tool section so every consumer calls the shared base
9889
10041
  * helpers identically.
@@ -9912,7 +10064,7 @@ declare function savvyHooksHygiene(): string;
9912
10064
  * @since 0.5.0
9913
10065
  * @public
9914
10066
  */
9915
- declare function savvyToolSection(toolName: string, command: string): Section;
10067
+ export declare function savvyToolSection(toolName: string, command: string): Section;
9916
10068
  /**
9917
10069
  * Section identity for the package-manager toolchain drift check.
9918
10070
  *
@@ -9921,7 +10073,7 @@ declare function savvyToolSection(toolName: string, command: string): Section;
9921
10073
  * @since 7.3.0
9922
10074
  * @public
9923
10075
  */
9924
- declare const SavvyToolchainSection: SectionId;
10076
+ export declare const SavvyToolchainSection: SectionId;
9925
10077
  /**
9926
10078
  * Package-manager drift check shared across Silk Suite hook files.
9927
10079
  *
@@ -9949,7 +10101,7 @@ declare const SavvyToolchainSection: SectionId;
9949
10101
  * @since 7.3.0
9950
10102
  * @public
9951
10103
  */
9952
- declare function savvyToolchainCheck(): string;
10104
+ export declare function savvyToolchainCheck(): string;
9953
10105
  //#endregion
9954
10106
  //#region src/schemas/WorkspaceAnalysisSchemas.d.ts
9955
10107
  declare const SilkPublishConfig_base: Schema.Class<SilkPublishConfig, Schema.Struct<{
@@ -9979,11 +10131,11 @@ declare const SilkPublishConfig_base: Schema.Class<SilkPublishConfig, Schema.Str
9979
10131
  * @since 0.2.0
9980
10132
  * @public
9981
10133
  */
9982
- declare class SilkPublishConfig extends SilkPublishConfig_base {}
10134
+ export declare class SilkPublishConfig extends SilkPublishConfig_base {}
9983
10135
  declare const AnalyzedWorkspace_base: Schema.Class<AnalyzedWorkspace, Schema.TaggedStruct<"AnalyzedWorkspace", {
9984
10136
  readonly name: Schema.String;
9985
10137
  readonly version: Schema.Struct<{
9986
- readonly current: Schema.String;
10138
+ readonly current: Schema.optional<Schema.String>;
9987
10139
  }>;
9988
10140
  readonly path: Schema.String;
9989
10141
  readonly root: Schema.Boolean;
@@ -10003,7 +10155,7 @@ declare const AnalyzedWorkspace_base: Schema.Class<AnalyzedWorkspace, Schema.Tag
10003
10155
  * @since 0.2.0
10004
10156
  * @public
10005
10157
  */
10006
- declare class AnalyzedWorkspace extends AnalyzedWorkspace_base {
10158
+ export declare class AnalyzedWorkspace extends AnalyzedWorkspace_base {
10007
10159
  get isRoot(): boolean;
10008
10160
  get isPublishable(): boolean;
10009
10161
  get isReleasable(): boolean;
@@ -10085,7 +10237,7 @@ declare const WorkspaceAnalysis_base: Schema.Class<WorkspaceAnalysis, Schema.Tag
10085
10237
  * @since 0.2.0
10086
10238
  * @public
10087
10239
  */
10088
- declare class WorkspaceAnalysis extends WorkspaceAnalysis_base {
10240
+ export declare class WorkspaceAnalysis extends WorkspaceAnalysis_base {
10089
10241
  findWorkspace(name: string): Option.Option<AnalyzedWorkspace>;
10090
10242
  get rootWorkspace(): Option.Option<AnalyzedWorkspace>;
10091
10243
  get publishableWorkspaces(): ReadonlyArray<AnalyzedWorkspace>;
@@ -10111,7 +10263,7 @@ declare class WorkspaceAnalysis extends WorkspaceAnalysis_base {
10111
10263
  * @since 0.1.0
10112
10264
  * @public
10113
10265
  */
10114
- declare function extractSemver(version: string): string;
10266
+ export declare function extractSemver(version: string): string;
10115
10267
  /**
10116
10268
  * Build the expected Biome JSON schema URL for a given version.
10117
10269
  *
@@ -10121,7 +10273,7 @@ declare function extractSemver(version: string): string;
10121
10273
  * @since 0.1.0
10122
10274
  * @public
10123
10275
  */
10124
- declare function buildSchemaUrl(version: string): string;
10276
+ export declare function buildSchemaUrl(version: string): string;
10125
10277
  /**
10126
10278
  * The {@link BiomeSchemaSync} service shape.
10127
10279
  *
@@ -10182,7 +10334,7 @@ declare const BiomeSchemaSync_base: Context.ServiceClass<BiomeSchemaSync, "@savv
10182
10334
  * @since 0.1.0
10183
10335
  * @public
10184
10336
  */
10185
- declare class BiomeSchemaSync extends BiomeSchemaSync_base {
10337
+ export declare class BiomeSchemaSync extends BiomeSchemaSync_base {
10186
10338
  /**
10187
10339
  * Production implementation of {@link BiomeSchemaSync}.
10188
10340
  *
@@ -10258,7 +10410,7 @@ declare const ConfigDiscovery_base: Context.ServiceClass<ConfigDiscovery, "@savv
10258
10410
  * @since 0.1.0
10259
10411
  * @public
10260
10412
  */
10261
- declare class ConfigDiscovery extends ConfigDiscovery_base {
10413
+ export declare class ConfigDiscovery extends ConfigDiscovery_base {
10262
10414
  /**
10263
10415
  * Production implementation of {@link ConfigDiscovery}.
10264
10416
  *
@@ -10366,7 +10518,12 @@ interface RawPackageJson {
10366
10518
  */
10367
10519
  interface PublishablePackage {
10368
10520
  readonly name: string;
10369
- readonly version: string;
10521
+ /**
10522
+ * The declared version, absent when the manifest carries none. Publishability is decided by
10523
+ * `publishConfig`, not by `version`, so a version-less package can still resolve targets —
10524
+ * it simply has no version to publish under until one is declared.
10525
+ */
10526
+ readonly version: string | undefined;
10370
10527
  readonly path: string;
10371
10528
  readonly targetCount: number;
10372
10529
  }
@@ -10381,7 +10538,7 @@ interface PublishablePackage {
10381
10538
  * @since 0.4.0
10382
10539
  * @public
10383
10540
  */
10384
- declare class SilkPublishability {
10541
+ export declare class SilkPublishability {
10385
10542
  /**
10386
10543
  * Apply silk publishability rules to a raw `package.json` and the bundler's resolved
10387
10544
  * target binding. Targets-first precedence:
@@ -10464,7 +10621,7 @@ declare class SilkPublishability {
10464
10621
  * @since 1.0.0
10465
10622
  * @public
10466
10623
  */
10467
- declare const readTargetsBinding: (fs: FileSystem.FileSystem, pkgPath: string) => Effect.Effect<TargetsBinding | null>;
10624
+ export declare const readTargetsBinding: (fs: FileSystem.FileSystem, pkgPath: string) => Effect.Effect<TargetsBinding | null>;
10468
10625
  //#endregion
10469
10626
  //#region src/services/SilkWorkspaceAnalyzer.d.ts
10470
10627
  /**
@@ -10515,7 +10672,7 @@ declare const SilkWorkspaceAnalyzer_base: Context.ServiceClass<SilkWorkspaceAnal
10515
10672
  * @since 0.2.0
10516
10673
  * @public
10517
10674
  */
10518
- declare class SilkWorkspaceAnalyzer extends SilkWorkspaceAnalyzer_base {
10675
+ export declare class SilkWorkspaceAnalyzer extends SilkWorkspaceAnalyzer_base {
10519
10676
  /**
10520
10677
  * Production implementation of {@link SilkWorkspaceAnalyzer}.
10521
10678
  *
@@ -10885,5 +11042,5 @@ declare namespace index_d_exports$5 {
10885
11042
  export { AffectedResult, AffectedResultType, CacheDiagnosis, CacheDiagnosisType, DryRunParseError, GlobalHashSummary, GraphNode, MissExplanation, NotATurboRepoError, PackageCacheStatus, TaskGraphResult, TaskGraphResultType, TurboCache, TurboDigest, TurboDryRun, TurboDryRunType, TurboDryTask, TurboDryTaskType, TurboEnvVars, TurboError, TurboExecError, TurboGlobalCacheInputs, TurboInspector, TurboInspectorShape, TurboNotInstalledError };
10886
11043
  }
10887
11044
  //#endregion
10888
- export { AnalyzedWorkspace, BiomeSchemaSync, type BiomeSchemaSyncShape, BiomeSyncError, type BiomeSyncOptions, type BiomeSyncResult, ChangesetConfig, ChangesetConfigError, type ChangesetConfigFile, ChangesetConfigReader, type ChangesetConfigReaderShape, type ChangesetConfigShape, type ChangesetMode, index_d_exports as Changesets, index_d_exports$1 as Commitlint, type CommitlintPlugin, type CommitlintUserConfig, ConfigDiscovery, type ConfigDiscoveryOptions, type ConfigDiscoveryShape, type ConfigLocation, ConfigNotFoundError, type ConfigSource, index_d_exports$2 as Lint, index_d_exports$3 as PrBody, type PromptConfig, type PromptSettings, PublishTargetBindingError, type PublishablePackage, type RawPackageJson, type RawPublishConfig, type RawPublishTargets, type RawTargetObject, type RawTargetValue, index_d_exports$4 as Repos, type RuleApplicability, type RuleConfigTuple, type RuleSeverity, type RulesConfig, SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, type SilkChangesetConfigFile, SilkPublishConfig, SilkPublishability, SilkWorkspaceAnalyzer, type SilkWorkspaceAnalyzerShape, type TargetBinding, type TargetGroupBinding, type TargetsBinding, index_d_exports$5 as Turbo, WorkspaceAnalysis, WorkspaceAnalysisError, buildSchemaUrl, extractSemver, readTargetsBinding, savvyBasePreamble, savvyHooksHygiene, savvyToolSection, savvyToolchainCheck };
11045
+ export { type BiomeSchemaSyncShape, type BiomeSyncOptions, type BiomeSyncResult, type ChangesetConfigFile, type ChangesetConfigReaderShape, type ChangesetConfigShape, type ChangesetMode, index_d_exports as Changesets, index_d_exports$1 as Commitlint, type CommitlintPlugin, type CommitlintUserConfig, type ConfigDiscoveryOptions, type ConfigDiscoveryShape, type ConfigLocation, type ConfigSource, index_d_exports$2 as Lint, index_d_exports$3 as PrBody, type PromptConfig, type PromptSettings, type PublishablePackage, type RawPackageJson, type RawPublishConfig, type RawPublishTargets, type RawTargetObject, type RawTargetValue, index_d_exports$4 as Repos, type RuleApplicability, type RuleConfigTuple, type RuleSeverity, type RulesConfig, type SavvyInstallHook, type SilkChangesetConfigFile, type SilkWorkspaceAnalyzerShape, type TargetBinding, type TargetGroupBinding, type TargetsBinding, index_d_exports$5 as Turbo };
10889
11046
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -12,10 +12,11 @@ import { WorkspaceAnalysisError } from "./errors/WorkspaceAnalysisError.js";
12
12
  import { lint_exports } from "./lint/index.js";
13
13
  import { pr_body_exports } from "./pr-body/index.js";
14
14
  import { repos_exports } from "./repos/index.js";
15
+ import { LIFECYCLE_SCRIPTS_CONFIG_KEY, SavvyInstallSection, publishesBuiltLinkDirectory, savvyInstallBlock, savvyInstallDeps } from "./schemas/SavvyInstallSection.js";
15
16
  import { AnalyzedWorkspace, SilkPublishConfig, WorkspaceAnalysis } from "./schemas/WorkspaceAnalysisSchemas.js";
16
17
  import { BiomeSchemaSync, buildSchemaUrl, extractSemver } from "./services/BiomeSchemaSync.js";
17
18
  import { ConfigDiscovery } from "./services/ConfigDiscovery.js";
18
19
  import { SilkWorkspaceAnalyzer } from "./services/SilkWorkspaceAnalyzer.js";
19
20
  import { turbo_exports } from "./turbo/index.js";
20
21
 
21
- export { AnalyzedWorkspace, BiomeSchemaSync, BiomeSyncError, ChangesetConfig, ChangesetConfigError, ChangesetConfigReader, changesets_exports as Changesets, commitlint_exports as Commitlint, ConfigDiscovery, ConfigNotFoundError, lint_exports as Lint, pr_body_exports as PrBody, PublishTargetBindingError, repos_exports as Repos, SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, SilkPublishConfig, SilkPublishability, SilkWorkspaceAnalyzer, turbo_exports as Turbo, WorkspaceAnalysis, WorkspaceAnalysisError, buildSchemaUrl, extractSemver, readTargetsBinding, savvyBasePreamble, savvyHooksHygiene, savvyToolSection, savvyToolchainCheck };
22
+ export { AnalyzedWorkspace, BiomeSchemaSync, BiomeSyncError, ChangesetConfig, ChangesetConfigError, ChangesetConfigReader, changesets_exports as Changesets, commitlint_exports as Commitlint, ConfigDiscovery, ConfigNotFoundError, LIFECYCLE_SCRIPTS_CONFIG_KEY, lint_exports as Lint, pr_body_exports as PrBody, PublishTargetBindingError, repos_exports as Repos, SavvyBaseSection, SavvyHooksSection, SavvyInstallSection, SavvyToolchainSection, SilkPublishConfig, SilkPublishability, SilkWorkspaceAnalyzer, turbo_exports as Turbo, WorkspaceAnalysis, WorkspaceAnalysisError, buildSchemaUrl, extractSemver, publishesBuiltLinkDirectory, readTargetsBinding, savvyBasePreamble, savvyHooksHygiene, savvyInstallBlock, savvyInstallDeps, savvyToolSection, savvyToolchainCheck };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/silk-effects",
3
- "version": "7.3.2",
3
+ "version": "7.5.0",
4
4
  "private": false,
5
5
  "description": "Shared Effect library for Silk Suite conventions",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/silk-effects",
@@ -29,19 +29,19 @@
29
29
  "./package.json": "./package.json"
30
30
  },
31
31
  "dependencies": {
32
- "@changesets/apply-release-plan": "^8.0.0",
32
+ "@changesets/apply-release-plan": "^8.1.0",
33
33
  "@changesets/changelog-git": "^1.0.0",
34
34
  "@changesets/config": "^4.0.0",
35
- "@changesets/get-github-info": "^1.0.0",
36
- "@changesets/get-release-plan": "^5.0.0",
37
- "@effected/github-references": "^0.1.0",
38
- "@effected/glob": "^0.4.0",
39
- "@effected/jsonc": "^0.8.1",
40
- "@effected/markdown": "^0.8.0",
41
- "@effected/package-json": "^0.13.0",
42
- "@effected/templates": "^0.4.0",
43
- "@effected/walker": "^0.5.0",
44
- "@effected/yaml": "^0.12.0",
35
+ "@changesets/get-github-info": "^1.0.1",
36
+ "@changesets/get-release-plan": "^5.0.1",
37
+ "@effected/github-references": "^0.2.0",
38
+ "@effected/glob": "^0.5.0",
39
+ "@effected/jsonc": "^0.9.0",
40
+ "@effected/markdown": "^0.9.0",
41
+ "@effected/package-json": "^0.14.0",
42
+ "@effected/templates": "^0.5.0",
43
+ "@effected/walker": "^0.6.0",
44
+ "@effected/yaml": "^0.13.0",
45
45
  "@manypkg/get-packages": "^3.1.0",
46
46
  "mdast-util-heading-range": "^4.0.0",
47
47
  "mdast-util-to-string": "^4.0.0",
@@ -54,9 +54,9 @@
54
54
  "unist-util-visit": "^5.1.0"
55
55
  },
56
56
  "peerDependencies": {
57
- "@effected/commands": "^0.5.0",
58
- "@effected/git": "^0.10.0",
59
- "@effected/workspaces": "^0.18.0",
60
- "effect": "4.0.0-rc.109"
57
+ "@effected/commands": "^0.6.0",
58
+ "@effected/git": "^0.11.0",
59
+ "@effected/workspaces": "^0.20.0",
60
+ "effect": "4.0.0-rc.112"
61
61
  }
62
62
  }
@@ -0,0 +1,232 @@
1
+ import { CommentStyle, SectionId } from "@effected/templates";
2
+
3
+ //#region src/schemas/SavvyInstallSection.ts
4
+ /**
5
+ * Section identity for the dependency auto-install block.
6
+ *
7
+ * `toolName` is `"savvy-install"`; pair with {@link savvyInstallDeps}.
8
+ *
9
+ * @since 7.5.0
10
+ * @public
11
+ */
12
+ const SavvyInstallSection = SectionId.make({
13
+ key: "SAVVY-INSTALL",
14
+ commentStyle: CommentStyle.hash
15
+ });
16
+ /**
17
+ * Pathspecs whose change across a checkout or merge means `node_modules` is stale.
18
+ *
19
+ * @remarks
20
+ * Git's default pathspec globbing does not stop a wildcard at a slash, so the
21
+ * one nested-manifest entry below covers every workspace manifest at any depth
22
+ * without needing a recursive spelling. Manifests are listed alongside lockfiles
23
+ * because a dependency edit that has not been installed yet moves the manifest
24
+ * first and the lockfile not at all.
25
+ */
26
+ const DEPENDENCY_PATHS = [
27
+ "package.json",
28
+ "*/package.json",
29
+ "pnpm-lock.yaml",
30
+ "pnpm-workspace.yaml",
31
+ "package-lock.json",
32
+ "npm-shrinkwrap.json",
33
+ "yarn.lock",
34
+ "bun.lock",
35
+ "bun.lockb"
36
+ ].map((path) => `'${path}'`).join(" ");
37
+ /**
38
+ * Dependency auto-install shared across Silk Suite hook files.
39
+ *
40
+ * @remarks
41
+ * Brings `node_modules` back in line after a branch switch or a pull, so a batch
42
+ * of repos pulled to align their dependencies does not each need a manual
43
+ * install. Installs with lifecycle scripts skipped — `--ignore-scripts`, or
44
+ * `--mode=skip-build` for Yarn Berry, which dropped the former — because the goal
45
+ * is to get the dependency tree on disk, not to run a full postinstall.
46
+ *
47
+ * **That flag has a cost, and one shape of repo cannot pay it.** Where a package
48
+ * publishes through a built link directory — `publishConfig.directory` with
49
+ * `linkDirectory: true`, which is how this monorepo wires `dist/dev/pkg` — the
50
+ * workspace resolves its own dependencies through a directory that a `prepare`
51
+ * script has to build, and skipping scripts yields a populated `node_modules`
52
+ * pointing at nothing.
53
+ *
54
+ * Those repos ask for a full install through the LOCAL git config key
55
+ * `savvy.installLifecycleScripts`, which `savvy init` sets when it finds the
56
+ * shape. The decision deliberately does NOT read the checked-out tree. Doing so
57
+ * failed in both directions at once: a branch that merely declared the shape
58
+ * could turn lifecycle scripts back on just by being checked out, making
59
+ * `git checkout` of an untrusted revision a code-execution path; and the `jq`
60
+ * the scan needed is absent on stock macOS and Ubuntu, where the missing answer
61
+ * silently skipped scripts in precisely the repos that cannot survive it.
62
+ * `.git/config` is neither checked out nor parsed with `jq`, so it has neither
63
+ * failure mode.
64
+ *
65
+ * The package managers gate dependency scripts themselves on top of this —
66
+ * pnpm's `strictDepBuilds` with an `allowBuilds` allowlist being the case in
67
+ * point — but workspace and root lifecycle scripts still run, which is why the
68
+ * opt-in is local rather than inferred. With the flag on, the hook says on the
69
+ * way out that scripts were skipped rather than leaving it to be discovered.
70
+ *
71
+ * Deliberately self-contained, like `savvyToolchainCheck`: its homes carry
72
+ * `SavvyHooksSection` but no `SavvyBaseSection`, so it defines its own root, CI
73
+ * and package-manager lookups rather than depending on `ROOT`, `in_ci` or `PM`.
74
+ *
75
+ * Four guards keep it from firing on the many checkouts that are not dependency
76
+ * events, each a silent no-op:
77
+ *
78
+ * - CI, where the runtime action owns installation by construction.
79
+ * - `SAVVY_SKIP_INSTALL`, the escape hatch for a bisect or a scripted sweep that
80
+ * does not want an install between steps.
81
+ * - `post-checkout` only: a branch-flag of `0`, which is `git checkout -- <file>`
82
+ * and not a move between commits.
83
+ * - **The gate that makes this affordable:** nothing dependency-related actually
84
+ * changed across the move. Without it every branch switch would pay for a full
85
+ * install. A missing `node_modules` overrides it, since there is nothing to be
86
+ * stale.
87
+ *
88
+ * A fresh clone is not among the cases it handles, and cannot be: husky sets
89
+ * `core.hooksPath` from its own `prepare` script, so until the first manual
90
+ * install has run there is no hook installed to fire.
91
+ *
92
+ * The install's exit status is swallowed and its output goes to stderr. Git
93
+ * ignores what `post-checkout` and `post-merge` return, so a failure here must
94
+ * not look like a failed checkout; the hint names the escape hatch instead.
95
+ *
96
+ * Every probe that can legitimately fail — no `ORIG_HEAD` to compare against, an
97
+ * unresolvable range — is neutralised with `|| true` rather than left to its own
98
+ * status. These blocks are co-owned and consumer content can sit below them, so
99
+ * under a hook running `set -e` a bare failing substitution would abort the whole
100
+ * file and take those later sections with it.
101
+ *
102
+ * @param hook - Which hook the block is being generated for; decides the
103
+ * argument guard and how the comparison range is recovered.
104
+ * @returns The install shell, with no surrounding markers or trailing newline.
105
+ *
106
+ * @since 7.5.0
107
+ * @public
108
+ */
109
+ function savvyInstallDeps(hook) {
110
+ const skip = `! { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ] || [ -n "$SAVVY_SKIP_INSTALL" ]; }`;
111
+ return `if ${hook === "post-checkout" ? `${skip} && [ "$3" = "1" ]` : skip}; then
112
+ install_root=$(git rev-parse --show-toplevel 2>/dev/null) || true
113
+ ${hook === "post-checkout" ? ` install_from="$1"
114
+ install_to="$2"` : ` install_from=$(git rev-parse --verify --quiet ORIG_HEAD 2>/dev/null) || true
115
+ install_to="HEAD"`}
116
+ install_pm=""
117
+ if [ -n "$install_root" ] && [ -f "$install_root/package.json" ]; then
118
+ if command -v jq >/dev/null 2>&1; then
119
+ install_pm=$(jq -r '.packageManager // empty' "$install_root/package.json" 2>/dev/null | cut -d'@' -f1)
120
+ fi
121
+ if [ -z "$install_pm" ]; then
122
+ if [ -f "$install_root/pnpm-lock.yaml" ]; then install_pm="pnpm"
123
+ elif [ -f "$install_root/yarn.lock" ]; then install_pm="yarn"
124
+ elif [ -f "$install_root/bun.lock" ] || [ -f "$install_root/bun.lockb" ]; then install_pm="bun"
125
+ else install_pm="npm"; fi
126
+ fi
127
+ fi
128
+ # packageManager comes from the checked-out revision, so it names an executable
129
+ # the tree controls. 'command -v' proves a binary exists, not that it is a
130
+ # package manager, so the name is checked against the supported four first.
131
+ case "$install_pm" in
132
+ npm|pnpm|yarn|bun) ;;
133
+ *) install_pm="" ;;
134
+ esac
135
+ # Nothing to bring up to date unless a manifest or lockfile actually moved.
136
+ # A missing node_modules skips the diff outright: there is no tree to be stale.
137
+ install_stale=""
138
+ if [ -n "$install_pm" ]; then
139
+ if [ ! -d "$install_root/node_modules" ]; then
140
+ install_stale=1
141
+ elif [ -n "$install_from" ] && [ -n "$install_to" ]; then
142
+ install_stale=$(git diff --name-only "$install_from" "$install_to" -- ${DEPENDENCY_PATHS} 2>/dev/null | head -n 1) || true
143
+ fi
144
+ fi
145
+ if [ -n "$install_stale" ] && command -v "$install_pm" >/dev/null 2>&1; then
146
+ # Whether lifecycle scripts run is a LOCAL decision, read from .git/config,
147
+ # which no checkout can rewrite. Default off. 'savvy init' turns it on for a
148
+ # workspace that publishes through built link directories, where the links
149
+ # point at directories a prepare script has to produce.
150
+ install_scripts=$(git -C "$install_root" config --bool --get savvy.installLifecycleScripts 2>/dev/null) || true
151
+ if [ "$install_scripts" = "true" ]; then
152
+ install_flag=""
153
+ else
154
+ # Berry dropped --ignore-scripts for the install mode; Classic never knew the
155
+ # mode. An unreadable major reads as Berry, the likelier of the two today.
156
+ install_flag="--ignore-scripts"
157
+ if [ "$install_pm" = "yarn" ]; then
158
+ case "$(yarn --version 2>/dev/null | cut -d. -f1)" in
159
+ 1) ;;
160
+ *) install_flag="--mode=skip-build" ;;
161
+ esac
162
+ fi
163
+ fi
164
+ # ':+' so a full install announces itself as "pnpm install", not "pnpm install ".
165
+ printf '↻ dependencies changed, running %s install%s\\n' "$install_pm" "\${install_flag:+ $install_flag}" >&2
166
+ # Unquoted on purpose: empty must expand to NO argument rather than an empty
167
+ # one. Safe because every value is a static flag literal from this generator.
168
+ if ( cd "$install_root" && "$install_pm" install $install_flag ) >&2; then
169
+ if [ -n "$install_flag" ]; then
170
+ printf ' Lifecycle scripts were skipped. Run a full install if you need build outputs.\\n' >&2
171
+ fi
172
+ else
173
+ printf '⚠ %s install failed; run it yourself to see why.\\n' "$install_pm" >&2
174
+ printf ' Set SAVVY_SKIP_INSTALL=1 to stop this hook from trying.\\n' >&2
175
+ fi
176
+ fi
177
+ unset install_root install_from install_to install_pm install_stale install_scripts install_flag
178
+ fi`;
179
+ }
180
+ /**
181
+ * The local git config key that authorizes lifecycle scripts during a hook install.
182
+ *
183
+ * @remarks
184
+ * Local scope only. It lives in `.git/config`, which is never checked out, so no
185
+ * incoming revision can set it — that is the whole point of reading the decision
186
+ * from here rather than from a manifest in the tree.
187
+ *
188
+ * @since 7.5.0
189
+ * @public
190
+ */
191
+ const LIFECYCLE_SCRIPTS_CONFIG_KEY = "savvy.installLifecycleScripts";
192
+ /**
193
+ * Whether `manifest` publishes through a built link directory.
194
+ *
195
+ * @remarks
196
+ * `publishConfig.directory` with `linkDirectory: true` means consumers of this
197
+ * package resolve it through a directory that a `prepare` script has to produce,
198
+ * so an install that skips lifecycle scripts leaves the link pointing at nothing.
199
+ * A workspace containing any such package is one whose owner probably wants
200
+ * {@link LIFECYCLE_SCRIPTS_CONFIG_KEY} set.
201
+ *
202
+ * Reporting the shape is deliberately separate from acting on it: this answers
203
+ * "does this repo need scripts", and a human still decides whether hook-time
204
+ * installs may run them.
205
+ *
206
+ * @param manifest - A parsed `package.json`; any non-object reads as `false`.
207
+ *
208
+ * @since 7.5.0
209
+ * @public
210
+ */
211
+ function publishesBuiltLinkDirectory(manifest) {
212
+ if (typeof manifest !== "object" || manifest === null) return false;
213
+ const { publishConfig } = manifest;
214
+ if (typeof publishConfig !== "object" || publishConfig === null) return false;
215
+ const { directory, linkDirectory } = publishConfig;
216
+ return typeof directory === "string" && directory.length > 0 && linkDirectory === true;
217
+ }
218
+ /**
219
+ * Build the dependency auto-install block for `hook`.
220
+ *
221
+ * @param hook - Which hook the section is destined for.
222
+ * @returns A shell `Section` (`commentStyle: hash`) keyed `SAVVY-INSTALL`.
223
+ *
224
+ * @since 7.5.0
225
+ * @public
226
+ */
227
+ function savvyInstallBlock(hook) {
228
+ return SavvyInstallSection.section(savvyInstallDeps(hook));
229
+ }
230
+
231
+ //#endregion
232
+ export { LIFECYCLE_SCRIPTS_CONFIG_KEY, SavvyInstallSection, publishesBuiltLinkDirectory, savvyInstallBlock, savvyInstallDeps };
@@ -45,7 +45,13 @@ const KNOWN_REGISTRIES = {
45
45
  * carries (binding-driven, placeholder, or access-branch fallback).
46
46
  */
47
47
  const sameRegistry = (a, b) => trimTrailingSlashes(a) === trimTrailingSlashes(b);
48
- const WorkspaceVersion = Schema.Struct({ current: Schema.String });
48
+ /**
49
+ * The package's declared version. `current` is absent for a member whose manifest carries no
50
+ * `version` — legal for a private package and the ordinary shape for a private monorepo root,
51
+ * which `@effected/workspaces` discovers as a member rather than rejecting (its `missingVersion`
52
+ * failure kind was retired in 0.19.0). Such a package has no version to bump, tag or stamp.
53
+ */
54
+ const WorkspaceVersion = Schema.Struct({ current: Schema.optional(Schema.String) });
49
55
  /**
50
56
  * A fully analyzed workspace with publish targets, versioning status,
51
57
  * and release group membership.
@@ -101,7 +107,7 @@ var AnalyzedWorkspace = class AnalyzedWorkspace extends Schema.TaggedClass()("An
101
107
  return Hash.optimize(Hash.combine(Hash.hash(this.name), Hash.hash(this.path)));
102
108
  }
103
109
  toString() {
104
- return `${this.name}@${this.version.current}`;
110
+ return this.version.current === void 0 ? this.name : `${this.name}@${this.version.current}`;
105
111
  }
106
112
  toJSON() {
107
113
  return {