@yahoo/uds-create-config 2.45.0 → 3.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 +129 -0
- package/dist/ai-lanes.d.ts +23 -0
- package/dist/ai-lanes.js +34 -0
- package/dist/authoring/define-component.d.ts +85 -0
- package/dist/configs/CanvasConfig.d.ts +3237 -0
- package/dist/configs/CanvasConfig.js +332 -0
- package/dist/configs/build-options.d.ts +10 -0
- package/dist/configs/build-options.js +68 -0
- package/dist/configs/platform.d.ts +15 -0
- package/dist/configs/platform.js +17 -0
- package/dist/configs/react-native-system.d.ts +3026 -0
- package/dist/configs/react-native-system.js +64 -0
- package/dist/configs/system.d.ts +2771 -0
- package/dist/configs/system.js +74 -0
- package/dist/css/properties.generated.d.ts +14 -0
- package/dist/css/properties.generated.js +6260 -0
- package/dist/css/value-type.d.ts +48 -0
- package/dist/css/value-type.js +131 -0
- package/dist/css/values.d.ts +33 -0
- package/dist/css/values.js +116 -0
- package/dist/entities/native/NativeModifier.d.ts +25 -0
- package/dist/entities/native/NativeModifier.js +41 -0
- package/dist/entities/native/NativeSettings.d.ts +44 -0
- package/dist/entities/native/NativeSettings.js +39 -0
- package/dist/entities/native/NativeStyleProperty.d.ts +355 -0
- package/dist/entities/native/NativeStyleProperty.js +28 -0
- package/dist/entities/native/NativeToken.d.ts +31 -0
- package/dist/entities/native/NativeToken.js +49 -0
- package/dist/entities/system/CanvasRole.d.ts +34 -0
- package/dist/entities/system/CanvasRole.js +45 -0
- package/dist/entities/system/Component.d.ts +1287 -0
- package/dist/entities/system/Component.js +1840 -0
- package/dist/entities/system/Composite.d.ts +12 -0
- package/dist/entities/system/Composite.js +26 -0
- package/dist/entities/system/Device.d.ts +11 -0
- package/dist/entities/system/Device.js +18 -0
- package/dist/entities/system/File.d.ts +72 -0
- package/dist/entities/system/File.js +136 -0
- package/dist/entities/system/Font.d.ts +48 -0
- package/dist/entities/system/Font.js +56 -0
- package/dist/entities/system/GlobalStyle.d.ts +26 -0
- package/dist/entities/system/GlobalStyle.js +54 -0
- package/dist/entities/system/Guidance.d.ts +85 -0
- package/dist/entities/system/Guidance.js +59 -0
- package/dist/entities/system/GuidanceStyle.d.ts +14 -0
- package/dist/entities/system/GuidanceStyle.js +8 -0
- package/dist/entities/system/Icon.d.ts +46 -0
- package/dist/entities/system/Icon.js +46 -0
- package/dist/entities/system/LinkedSystem.d.ts +504 -0
- package/dist/entities/system/LinkedSystem.js +814 -0
- package/dist/entities/system/Modifier.d.ts +92 -0
- package/dist/entities/system/Modifier.js +120 -0
- package/dist/entities/system/Motion.d.ts +28 -0
- package/dist/entities/system/Motion.js +48 -0
- package/dist/entities/system/Operation.d.ts +100 -0
- package/dist/entities/system/Operation.js +236 -0
- package/dist/entities/system/Package.d.ts +42 -0
- package/dist/entities/system/Package.js +75 -0
- package/dist/entities/system/Settings.d.ts +94 -0
- package/dist/entities/system/Settings.js +166 -0
- package/dist/entities/system/StyleProperty.d.ts +135 -0
- package/dist/entities/system/StyleProperty.js +438 -0
- package/dist/entities/system/Token.d.ts +67 -0
- package/dist/entities/system/Token.js +129 -0
- package/dist/entities/system/Tool.d.ts +134 -0
- package/dist/entities/system/Tool.js +124 -0
- package/dist/entities/system/color.d.ts +64 -0
- package/dist/entities/system/color.js +123 -0
- package/dist/entities/system/component-module.d.ts +63 -0
- package/dist/entities/system/component-module.js +258 -0
- package/dist/entities/system/copy-plan.d.ts +98 -0
- package/dist/entities/system/copy-plan.js +451 -0
- package/dist/entities/system/defineComponent.d.ts +428 -0
- package/dist/entities/system/defineComponent.js +337 -0
- package/dist/entities/system/element.d.ts +101 -0
- package/dist/entities/system/element.js +171 -0
- package/dist/entities/system/icon-library.d.ts +64 -0
- package/dist/entities/system/icon-library.js +112 -0
- package/dist/entities/system/icon-metadata.d.ts +72 -0
- package/dist/entities/system/icon-metadata.js +111 -0
- package/dist/entities/system/link-declaration.d.ts +27 -0
- package/dist/entities/system/link-declaration.js +64 -0
- package/dist/entities/system/link-plan.d.ts +160 -0
- package/dist/entities/system/link-plan.js +770 -0
- package/dist/entities/system/runtime-modules.d.ts +25 -0
- package/dist/entities/system/runtime-modules.js +38 -0
- package/dist/entities/system/style-bag.d.ts +20 -0
- package/dist/entities/system/style-bag.js +246 -0
- package/dist/entities/system/token-index.d.ts +89 -0
- package/dist/entities/system/token-index.js +112 -0
- package/dist/framework/Config.d.ts +2472 -0
- package/dist/framework/Config.js +5023 -0
- package/dist/framework/Entity.d.ts +127 -0
- package/dist/framework/Entity.js +125 -0
- package/dist/framework/authoring.d.ts +24 -0
- package/dist/framework/authoring.js +7 -0
- package/dist/framework/change-hooks.d.ts +119 -0
- package/dist/framework/change-hooks.js +131 -0
- package/dist/framework/changes.d.ts +88 -0
- package/dist/framework/changes.js +246 -0
- package/dist/framework/class-names.d.ts +260 -0
- package/dist/framework/class-names.js +630 -0
- package/dist/framework/coalesce.d.ts +25 -0
- package/dist/framework/coalesce.js +60 -0
- package/dist/framework/config-op.d.ts +76 -0
- package/dist/framework/config-op.js +43 -0
- package/dist/framework/config-operation-kind.d.ts +14 -0
- package/dist/framework/config-operation-kind.js +14 -0
- package/dist/framework/config-ops.d.ts +34 -0
- package/dist/framework/config-ops.js +187 -0
- package/dist/framework/defineConfig.d.ts +196 -0
- package/dist/framework/defineConfig.js +100 -0
- package/dist/framework/defineDerivedEntity.d.ts +74 -0
- package/dist/framework/defineDerivedEntity.js +83 -0
- package/dist/framework/defineEntity.d.ts +318 -0
- package/dist/framework/defineEntity.js +183 -0
- package/dist/framework/defineSubEntity.d.ts +148 -0
- package/dist/framework/defineSubEntity.js +121 -0
- package/dist/framework/derived-mutations.d.ts +20 -0
- package/dist/framework/derived-mutations.js +38 -0
- package/dist/framework/entity-search.d.ts +29 -0
- package/dist/framework/entity-search.js +117 -0
- package/dist/framework/inline-sources.d.ts +49 -0
- package/dist/framework/inline-sources.js +388 -0
- package/dist/framework/layer-styles.d.ts +25 -0
- package/dist/framework/layer-styles.js +49 -0
- package/dist/framework/memo.d.ts +48 -0
- package/dist/framework/memo.js +84 -0
- package/dist/framework/overlay.d.ts +49 -0
- package/dist/framework/overlay.js +17 -0
- package/dist/framework/plan.d.ts +49 -0
- package/dist/framework/plan.js +73 -0
- package/dist/framework/projections.d.ts +480 -0
- package/dist/framework/projections.js +974 -0
- package/dist/framework/prop-surface.d.ts +132 -0
- package/dist/framework/prop-surface.js +278 -0
- package/dist/framework/ref-graph.d.ts +71 -0
- package/dist/framework/ref-graph.js +158 -0
- package/dist/framework/ref-integrity.d.ts +55 -0
- package/dist/framework/ref-integrity.js +171 -0
- package/dist/framework/registered.d.ts +95 -0
- package/dist/framework/registry-dir.d.ts +42 -0
- package/dist/framework/registry-dir.js +48 -0
- package/dist/framework/registry.d.ts +300 -0
- package/dist/framework/registry.js +104 -0
- package/dist/framework/rejection.d.ts +58 -0
- package/dist/framework/rejection.js +111 -0
- package/dist/framework/rename-inference.d.ts +26 -0
- package/dist/framework/rename-inference.js +50 -0
- package/dist/framework/render-spec.d.ts +296 -0
- package/dist/framework/render-spec.js +809 -0
- package/dist/framework/schema-version.d.ts +140 -0
- package/dist/framework/schema-version.js +214 -0
- package/dist/framework/schemas.d.ts +109 -0
- package/dist/framework/schemas.js +615 -0
- package/dist/framework/session.d.ts +196 -0
- package/dist/framework/session.js +367 -0
- package/dist/framework/signature.d.ts +15 -0
- package/dist/framework/signature.js +83 -0
- package/dist/framework/snapshot.d.ts +115 -0
- package/dist/framework/snapshot.js +277 -0
- package/dist/framework/source-integrity.d.ts +226 -0
- package/dist/framework/source-integrity.js +478 -0
- package/dist/framework/sources.d.ts +211 -0
- package/dist/framework/sources.js +89 -0
- package/dist/framework/utils/compiled-schema.d.ts +23 -0
- package/dist/framework/utils/compiled-schema.js +46 -0
- package/dist/framework/utils/content-key.d.ts +15 -0
- package/dist/framework/utils/content-key.js +33 -0
- package/dist/framework/utils/enumerated.d.ts +13 -0
- package/dist/framework/utils/enumerated.js +19 -0
- package/dist/framework/utils/field-path.d.ts +95 -0
- package/dist/framework/utils/field-path.js +136 -0
- package/dist/framework/utils/field-schema.d.ts +55 -0
- package/dist/framework/utils/field-schema.js +183 -0
- package/dist/framework/utils/group.d.ts +37 -0
- package/dist/framework/utils/group.js +13 -0
- package/dist/framework/utils/package-path.d.ts +37 -0
- package/dist/framework/utils/package-path.js +61 -0
- package/dist/framework/utils/refs.d.ts +194 -0
- package/dist/framework/utils/refs.js +403 -0
- package/dist/framework/validate-spec.d.ts +27 -0
- package/dist/framework/validate-spec.js +45 -0
- package/dist/framework/value-domain.d.ts +26 -0
- package/dist/framework/value-domain.js +202 -0
- package/dist/framework/views-facade.d.ts +15 -0
- package/dist/framework/views-facade.js +19 -0
- package/dist/index.d.ts +90 -42
- package/dist/index.js +86 -37
- package/dist/migrations/2.0.0/raw-css.d.ts +11 -0
- package/dist/migrations/2.0.0/raw-css.js +72 -0
- package/dist/migrations/2.0.0/v1-artifact.d.ts +1559 -0
- package/dist/migrations/2.0.0/v1-artifact.js +1018 -0
- package/dist/migrations/20260908171448_settings_as_kind.d.ts +22 -0
- package/dist/migrations/20260908171448_settings_as_kind.js +89 -0
- package/dist/migrations/20260908194245_visible_state_pointer.d.ts +19 -0
- package/dist/migrations/20260908194245_visible_state_pointer.js +115 -0
- package/dist/migrations/20260909183001_icon_knockout_variants.d.ts +18 -0
- package/dist/migrations/20260909183001_icon_knockout_variants.js +8 -0
- package/dist/migrations/20260910212453_linked_system_link_ops.d.ts +13 -0
- package/dist/migrations/20260910212453_linked_system_link_ops.js +8 -0
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.js +2 -0
- package/dist/react-native/style-keys.generated.d.ts +10 -0
- package/dist/react-native/style-keys.generated.js +167 -0
- package/dist/renderer/RendererErrorBoundary.d.ts +32 -5
- package/dist/renderer/RendererErrorBoundary.js +59 -10
- package/dist/renderer/UdsRenderer.d.ts +11 -10
- package/dist/renderer/UdsRenderer.js +1 -1
- package/dist/renderer/assetEntries.d.ts +37 -0
- package/dist/renderer/assetEntries.js +39 -0
- package/dist/renderer/assetRenderable.d.ts +14 -6
- package/dist/renderer/assetRenderable.js +8 -2
- package/dist/renderer/componentEntry.d.ts +58 -0
- package/dist/renderer/componentEntry.js +71 -0
- package/dist/renderer/icon-library.d.ts +20 -0
- package/dist/renderer/index.d.ts +7 -7
- package/dist/renderer/index.js +5 -5
- package/dist/renderer/primitives/SlotRenderer.d.ts +1 -1
- package/dist/renderer/primitives/SlotRenderer.js +1 -1
- package/dist/renderer/runtimeComponent.d.ts +33 -0
- package/dist/renderer/runtimeComponent.js +113 -0
- package/dist/renderer/spec-content.d.ts +43 -0
- package/dist/renderer/spec-content.js +95 -0
- package/dist/renderer/wrapRegistry.d.ts +22 -37
- package/dist/renderer/wrapRegistry.js +10 -20
- package/dist/renderer/wrappers/event-bridge.js +6 -5
- package/dist/renderer/wrappers/hex-normalize.js +6 -5
- package/dist/renderer/wrappers/html-aliases.d.ts +7 -2
- package/dist/renderer/wrappers/html-aliases.js +36 -19
- package/dist/renderer/wrappers/inline-styles.js +7 -6
- package/dist/spec/asset-jsx.d.ts +18 -5
- package/dist/spec/asset-jsx.js +27 -16
- package/dist/spec/collapse-text-labels.d.ts +5 -15
- package/dist/spec/collapse-text-labels.js +26 -19
- package/dist/spec/empty-node-slots.d.ts +56 -34
- package/dist/spec/empty-node-slots.js +87 -47
- package/dist/spec/index.d.ts +21 -10
- package/dist/spec/index.js +21 -10
- package/dist/spec/jsxToSpec.d.ts +14 -9
- package/dist/spec/jsxToSpec.js +20 -3
- package/dist/spec/preview-elements.d.ts +63 -0
- package/dist/spec/preview-elements.js +89 -0
- package/dist/spec/rewrite-refs.d.ts +58 -0
- package/dist/spec/rewrite-refs.js +250 -0
- package/dist/spec/specToJsx.js +18 -29
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +39 -52
- package/dist/AssetGroup.d.ts +0 -77
- package/dist/AssetGroup.js +0 -125
- package/dist/Component.d.ts +0 -333
- package/dist/Component.js +0 -1061
- package/dist/ComponentGroup.d.ts +0 -22
- package/dist/ComponentGroup.js +0 -51
- package/dist/CompositeStyle.d.ts +0 -30
- package/dist/CompositeStyle.js +0 -59
- package/dist/Config.d.ts +0 -528
- package/dist/Config.js +0 -1712
- package/dist/Mode.d.ts +0 -41
- package/dist/Mode.js +0 -81
- package/dist/Modifier.d.ts +0 -51
- package/dist/Modifier.js +0 -97
- package/dist/MotionDef.d.ts +0 -81
- package/dist/MotionDef.js +0 -212
- package/dist/Props.d.ts +0 -332
- package/dist/Props.js +0 -35
- package/dist/Provider.d.ts +0 -20
- package/dist/Provider.js +0 -14
- package/dist/StyleProp.d.ts +0 -145
- package/dist/StyleProp.js +0 -239
- package/dist/Token.d.ts +0 -76
- package/dist/Token.js +0 -133
- package/dist/TokenGroup.d.ts +0 -33
- package/dist/TokenGroup.js +0 -69
- package/dist/asset-kind.d.ts +0 -55
- package/dist/asset-kind.js +0 -29
- package/dist/asset-value.d.ts +0 -23
- package/dist/asset-value.js +0 -40
- package/dist/brands.d.ts +0 -30
- package/dist/brands.js +0 -20
- package/dist/captureCallerPath.d.ts +0 -48
- package/dist/captureCallerPath.js +0 -95
- package/dist/colorExpressions.d.ts +0 -131
- package/dist/colorExpressions.js +0 -148
- package/dist/component-referrers.d.ts +0 -30
- package/dist/component-referrers.js +0 -40
- package/dist/config-diagnostics.d.ts +0 -21
- package/dist/config-diagnostics.js +0 -12
- package/dist/defineAssetGroup.d.ts +0 -200
- package/dist/defineAssetGroup.js +0 -323
- package/dist/defineProvider.d.ts +0 -29
- package/dist/defineProvider.js +0 -60
- package/dist/element-marker.d.ts +0 -63
- package/dist/element-marker.js +0 -113
- package/dist/entity-utils.d.ts +0 -56
- package/dist/entity-utils.js +0 -105
- package/dist/extension.d.ts +0 -55
- package/dist/extension.js +0 -214
- package/dist/factories.d.ts +0 -863
- package/dist/factories.js +0 -475
- package/dist/font-face.d.ts +0 -25
- package/dist/font-face.js +0 -72
- package/dist/foreign-component-name.d.ts +0 -21
- package/dist/foreign-component-name.js +0 -42
- package/dist/interpolate.d.ts +0 -20
- package/dist/interpolate.js +0 -10
- package/dist/jsx/__fixtures__/cross-component-preview.d.ts +0 -3
- package/dist/jsx/__fixtures__/cross-component-preview.js +0 -15
- package/dist/jsx/jsx-dev-runtime.d.ts +0 -15
- package/dist/jsx/jsx-dev-runtime.js +0 -11
- package/dist/jsx/jsx-runtime.d.ts +0 -48
- package/dist/jsx/jsx-runtime.js +0 -305
- package/dist/linked-system-pins.d.ts +0 -30
- package/dist/linked-system-pins.js +0 -45
- package/dist/linkedNodePrefixes.d.ts +0 -73
- package/dist/linkedNodePrefixes.js +0 -99
- package/dist/markers.d.ts +0 -235
- package/dist/markers.js +0 -70
- package/dist/motion-constants.d.ts +0 -37
- package/dist/motion-constants.js +0 -61
- package/dist/native/NativeConfig.d.ts +0 -56
- package/dist/native/NativeConfig.js +0 -162
- package/dist/native/assets.d.ts +0 -20
- package/dist/native/assets.js +0 -17
- package/dist/native/components.d.ts +0 -139
- package/dist/native/components.js +0 -67
- package/dist/native/index.d.ts +0 -18
- package/dist/native/index.js +0 -17
- package/dist/native/modes.d.ts +0 -10
- package/dist/native/modes.js +0 -11
- package/dist/native/types.d.ts +0 -95
- package/dist/native/types.js +0 -0
- package/dist/native/validation.d.ts +0 -8
- package/dist/native/validation.js +0 -885
- package/dist/native/values.d.ts +0 -7
- package/dist/native/values.js +0 -15
- package/dist/native.d.ts +0 -12
- package/dist/native.js +0 -10
- package/dist/refs.d.ts +0 -204
- package/dist/refs.js +0 -129
- package/dist/registry-aliases.d.ts +0 -33
- package/dist/registry-aliases.js +0 -35
- package/dist/registry-dir.d.ts +0 -22
- package/dist/registry-dir.js +0 -10
- package/dist/renderer/makeRegistry.d.ts +0 -34
- package/dist/renderer/makeRegistry.js +0 -52
- package/dist/renderer/makeUdsRenderer.d.ts +0 -13
- package/dist/renderer/makeUdsRenderer.js +0 -51
- package/dist/renderer/wrappers/component-slots.d.ts +0 -41
- package/dist/renderer/wrappers/component-slots.js +0 -66
- package/dist/renderer/wrappers/slot-resolution.d.ts +0 -25
- package/dist/renderer/wrappers/slot-resolution.js +0 -68
- package/dist/renderer/wrappers/void-elements.d.ts +0 -37
- package/dist/renderer/wrappers/void-elements.js +0 -40
- package/dist/spec/layer-props.d.ts +0 -52
- package/dist/spec/layer-props.js +0 -149
- package/dist/spec/preview-controls.d.ts +0 -44
- package/dist/spec/preview-controls.js +0 -139
- package/dist/spec/slot-refs.d.ts +0 -39
- package/dist/spec/slot-refs.js +0 -56
- package/dist/token-override-rows.d.ts +0 -68
- package/dist/token-override-rows.js +0 -233
- package/dist/tokenValueType.d.ts +0 -34
- package/dist/tokenValueType.js +0 -138
- package/dist/types/css-properties.d.ts +0 -233
- package/dist/types/css-properties.js +0 -14
- package/dist/types/css-property-keywords.d.ts +0 -156
- package/dist/types/css-property-keywords.js +0 -616
- package/dist/types/css-values.d.ts +0 -63
- package/dist/types/css-values.js +0 -16
- package/dist/types.d.ts +0 -1138
- package/dist/types.js +0 -12
- package/dist/units.d.ts +0 -14
- package/dist/units.js +0 -16
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.js +0 -4
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
import { rejectData } from "../../framework/rejection.js";
|
|
2
|
+
import { isPlainObject } from "../../framework/utils/field-path.js";
|
|
3
|
+
import { pathInput } from "../../framework/schemas.js";
|
|
4
|
+
import { kindBorrowable } from "../../framework/registry.js";
|
|
5
|
+
import { defineEntity } from "../../framework/defineEntity.js";
|
|
6
|
+
import { sourceUnreachable } from "../../framework/sources.js";
|
|
7
|
+
import { planCopy } from "./copy-plan.js";
|
|
8
|
+
import { planBreak, planLink, planUnlink, sourceRowStates } from "./link-plan.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
//#region src/entities/system/LinkedSystem.ts
|
|
11
|
+
/**
|
|
12
|
+
* `LinkedSystem` (`yahoo-os`) — another team's design system, drawn from at an exact version.
|
|
13
|
+
*
|
|
14
|
+
* Two questions kept apart, and this entity answers only the first:
|
|
15
|
+
*
|
|
16
|
+
* • **What can I see?** — you link a system. Everything in it becomes visible and resolvable.
|
|
17
|
+
* • **What am I using?** — you write a ref (`token:color/brand@yahoo-os`). That's the only thing
|
|
18
|
+
* stored.
|
|
19
|
+
*
|
|
20
|
+
* Linking copies nothing and makes nothing yours; it puts a system within reach. That is what makes
|
|
21
|
+
* partial use the normal case rather than a mode: link 400 tokens, use 12, store 12 refs. The other
|
|
22
|
+
* 388 stay visible, for browsing and for later, without being in your system.
|
|
23
|
+
*
|
|
24
|
+
* **The key is a local slug**, not the source's database id. It is what every ref spells, so it has to
|
|
25
|
+
* be readable and to survive the source renaming itself. `systemId` carries durable identity
|
|
26
|
+
* underneath, and `name` is display only, so a source that renames strands no refs.
|
|
27
|
+
*
|
|
28
|
+
* **A repo-local link round-trips.** Push writes the resolved `systemId`/`version` beside the authored
|
|
29
|
+
* `configPath`, so Studio reads an ordinary link while `uds pull` can still restore the path form.
|
|
30
|
+
* Dropping `configPath` at push would make a pull rewrite `'../design-system'` into a published pin.
|
|
31
|
+
* The path is inert server-side, as `playground.pagesDir` is.
|
|
32
|
+
*
|
|
33
|
+
* **Being an entity means it needs no special support.** Create, update, delete, validation and
|
|
34
|
+
* `dependents` all derive from these three fields as they do for a token, so an upgrade is an ordinary
|
|
35
|
+
* update of one field with no separate manifest to keep in sync. It is also the install list: `uds
|
|
36
|
+
* pull` reads these entries and materializes each one.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* An exact version — `2.4.0`, never `^2.4.0` or `latest`.
|
|
40
|
+
*
|
|
41
|
+
* A link is always pinned: you draw from Yahoo OS 2.4.0 rather than from "Yahoo OS", so their next
|
|
42
|
+
* release can't change what you render until you move the pin. A range would give that away, and it
|
|
43
|
+
* would need a resolution step and a lockfile to mean anything, neither of which exists. Spelled as a
|
|
44
|
+
* pattern rather than pulled from `semver`, so this package keeps its three dependencies.
|
|
45
|
+
*/
|
|
46
|
+
const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
47
|
+
/**
|
|
48
|
+
* A link's slug, as a single path segment.
|
|
49
|
+
*
|
|
50
|
+
* The slug is more than a key: `uds install` writes one registry folder per link and names it this, so
|
|
51
|
+
* a slug that isn't a path segment is one the install step can't use, and `../..` would be a traversal
|
|
52
|
+
* handed to a file write. Stated here on the entity so the rule has one home — the install refuses
|
|
53
|
+
* with it, an authoring UI can say so before submitting, and an AI tool creating a link is bound by it
|
|
54
|
+
* too. Enforced only at install, a config would commit fine and break for whoever runs the build.
|
|
55
|
+
*/
|
|
56
|
+
const LINK_SLUG = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/i;
|
|
57
|
+
/** Whether `slug` can be a link's local name — {@link LINK_SLUG}, as a predicate for a caller that wants
|
|
58
|
+
* to ask rather than to match. */
|
|
59
|
+
function isLinkSlug(slug) {
|
|
60
|
+
return LINK_SLUG.test(slug);
|
|
61
|
+
}
|
|
62
|
+
/** What a refused slug should say, in one place, so the install and a form word it identically. */
|
|
63
|
+
const LINK_SLUG_MESSAGE = "A link's slug is a single path segment — letters, digits, dot, dash and underscore — because it becomes a folder in the registry.";
|
|
64
|
+
/**
|
|
65
|
+
* Where a linked system resolved from, carrying the identity that origin actually has.
|
|
66
|
+
*
|
|
67
|
+
* Discriminated on `from` rather than a flat `{ systemId, version, from }`. A repo-local source has no
|
|
68
|
+
* published identity to report: its identity is the config path, and a built artifact carries no
|
|
69
|
+
* `systemId` or `version` at all. A flat shape would require two fields that one of the three origins
|
|
70
|
+
* can never fill, so a resolver would have to invent an id to satisfy it.
|
|
71
|
+
*
|
|
72
|
+
* Each origin therefore says what it knows, which is also what makes "what did this build against"
|
|
73
|
+
* answerable for both kinds of source.
|
|
74
|
+
*
|
|
75
|
+
* Declared here beside the kind and handed to the framework through `sourceResolution`. These three
|
|
76
|
+
* origins are what a linked system has; a `linkedPackages` kind would resolve from a lockfile and
|
|
77
|
+
* couldn't fill this union, which is why the framework holds only the `status` tag.
|
|
78
|
+
*/
|
|
79
|
+
const resolvedSource = z.discriminatedUnion("from", [
|
|
80
|
+
z.object({
|
|
81
|
+
from: z.literal("disk"),
|
|
82
|
+
configPath: z.string().min(1)
|
|
83
|
+
}),
|
|
84
|
+
z.object({
|
|
85
|
+
from: z.literal("registry"),
|
|
86
|
+
systemId: z.string().min(1),
|
|
87
|
+
version: z.string().min(1)
|
|
88
|
+
}),
|
|
89
|
+
z.object({
|
|
90
|
+
from: z.literal("storage"),
|
|
91
|
+
systemId: z.string().min(1),
|
|
92
|
+
version: z.string().min(1)
|
|
93
|
+
})
|
|
94
|
+
]);
|
|
95
|
+
/**
|
|
96
|
+
* Why a declared linked system couldn't be met — a code plus the data that case needs, and no prose.
|
|
97
|
+
*
|
|
98
|
+
* A closed set, so a surface switches exhaustively and adding a case is a compile error at every
|
|
99
|
+
* consumer. Collapsed into `undefined`, these all look alike, which is how a rail ends up saying a
|
|
100
|
+
* value "isn't loaded here" when it is loaded and the ref is broken.
|
|
101
|
+
*
|
|
102
|
+
* **No `message`.** The fix depends on the surface rather than the code: a build tells you to run
|
|
103
|
+
* `uds install` or to build the sibling first, and neither sentence means anything in a browser where
|
|
104
|
+
* `stale` is an update button. One string per code would put CLI instructions into Studio. What such a
|
|
105
|
+
* message really carries is the data here — `stale` can't be phrased at all without both versions.
|
|
106
|
+
*
|
|
107
|
+
* **`not-found` covers three situations on purpose.** A system the caller can't see and a version
|
|
108
|
+
* never published have to be indistinguishable, or the difference is a probe: ask for an id you
|
|
109
|
+
* suspect exists, and a separate code confirms it. A resolver may know which internally; it must not
|
|
110
|
+
* cross out in the value.
|
|
111
|
+
*/
|
|
112
|
+
const sourceUnavailable = z.discriminatedUnion("code", [
|
|
113
|
+
z.object({ code: z.literal("not-installed") }),
|
|
114
|
+
z.object({
|
|
115
|
+
code: z.literal("unbuilt"),
|
|
116
|
+
configPath: z.string().min(1)
|
|
117
|
+
}),
|
|
118
|
+
z.object({
|
|
119
|
+
code: z.literal("stale"),
|
|
120
|
+
installed: z.string().min(1),
|
|
121
|
+
pinned: z.string().min(1)
|
|
122
|
+
}),
|
|
123
|
+
z.object({ code: z.literal("not-found") }),
|
|
124
|
+
z.object({ code: z.literal("unmaterializable") }),
|
|
125
|
+
sourceUnreachable
|
|
126
|
+
]);
|
|
127
|
+
/** What one accessor of a copy names: every entry of the kind, or the source's paths — items or folders. */
|
|
128
|
+
const copyPaths = z.union([z.literal("*"), z.array(z.string().min(1)).min(1)]);
|
|
129
|
+
const copyInput = z.object({
|
|
130
|
+
path: pathInput("linked-system"),
|
|
131
|
+
/**
|
|
132
|
+
* What to take, keyed by accessor the way the pin's `link` is — `{ tokens: ['color/brand', 'color'],
|
|
133
|
+
* icons: '*' }` — or `'*'` for every kind the source offers and this config can hold. A path that
|
|
134
|
+
* names a folder in the source takes its members. Copied rows keep the source's own names.
|
|
135
|
+
*/
|
|
136
|
+
data: z.union([z.literal("*"), z.record(z.string(), copyPaths)]),
|
|
137
|
+
/**
|
|
138
|
+
* What to do at a path this config already holds. `'skip'` (the default) keeps what is there and
|
|
139
|
+
* binds copied refs to it; `'replace'` writes the source's body over a row of yours, and over a row
|
|
140
|
+
* held through another pin when `data` names it. A row the copy only reaches through a ref is kept
|
|
141
|
+
* either way.
|
|
142
|
+
*/
|
|
143
|
+
onConflict: z.enum(["skip", "replace"]).optional()
|
|
144
|
+
});
|
|
145
|
+
const linkInput = z.object({
|
|
146
|
+
path: pathInput("linked-system"),
|
|
147
|
+
/** What to draw, keyed by accessor the way the pin's `link` is, or `'*'` for everything the source
|
|
148
|
+
* offers. A path that names a folder in the source covers its members. */
|
|
149
|
+
data: z.union([z.literal("*"), z.record(z.string(), copyPaths)])
|
|
150
|
+
});
|
|
151
|
+
const unlinkInput = z.object({
|
|
152
|
+
path: pathInput("linked-system"),
|
|
153
|
+
/** What to stop drawing, in `link`'s shape; everything when omitted. */
|
|
154
|
+
data: z.union([z.literal("*"), z.record(z.string(), copyPaths)]).optional()
|
|
155
|
+
});
|
|
156
|
+
const breakInput = z.object({
|
|
157
|
+
path: pathInput("linked-system"),
|
|
158
|
+
/** Whether every value drawn through the pin becomes this config's own first. */
|
|
159
|
+
keep: z.boolean()
|
|
160
|
+
});
|
|
161
|
+
const breakPlanInput = z.object({
|
|
162
|
+
path: pathInput("linked-system"),
|
|
163
|
+
keep: z.boolean().optional(),
|
|
164
|
+
/** One entity or folder detached through `copy` rather than the whole pin. */
|
|
165
|
+
data: z.union([z.literal("*"), z.record(z.string(), copyPaths)]).optional()
|
|
166
|
+
});
|
|
167
|
+
const rowsInput = z.object({ path: pathInput("linked-system") });
|
|
168
|
+
/** The parsed op input as the plan takes it. */
|
|
169
|
+
function copyInputOf(input) {
|
|
170
|
+
return {
|
|
171
|
+
path: String(input.path),
|
|
172
|
+
data: input.data,
|
|
173
|
+
...input.onConflict === void 0 ? {} : { onConflict: input.onConflict }
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** A `link` or `unlink` example: one entity the source offers, under the accessor this config files it by. */
|
|
177
|
+
function linkExample({ config, path }) {
|
|
178
|
+
const target = exampleBorrowable(config, path);
|
|
179
|
+
if (!target) return void 0;
|
|
180
|
+
return {
|
|
181
|
+
path,
|
|
182
|
+
data: { [config.accessorOf(target.kind) ?? target.kind]: [target.from] }
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** An example for an op that takes the pin alone, shown once the pin has a source to read. */
|
|
186
|
+
function pinExample({ config, path }) {
|
|
187
|
+
return exampleBorrowable(config, path) ? { path } : void 0;
|
|
188
|
+
}
|
|
189
|
+
/** A break's example, shown once the pin has a source to read. */
|
|
190
|
+
function breakExample({ config, path }) {
|
|
191
|
+
return exampleBorrowable(config, path) ? {
|
|
192
|
+
path,
|
|
193
|
+
keep: true
|
|
194
|
+
} : void 0;
|
|
195
|
+
}
|
|
196
|
+
/** A copy's example: one entity the source offers, under the accessor this config files it by. */
|
|
197
|
+
function copyExample({ config, path }) {
|
|
198
|
+
const target = exampleBorrowable(config, path);
|
|
199
|
+
if (!target) return void 0;
|
|
200
|
+
return {
|
|
201
|
+
path,
|
|
202
|
+
data: { [config.accessorOf(target.kind) ?? target.kind]: [target.from] }
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A real borrow target on the other side of a pin, for the custom ops' examples.
|
|
207
|
+
*
|
|
208
|
+
* The derived default example is built from the standard `path`/`to` address shape, which describes a
|
|
209
|
+
* rename and satisfies none of these ops' own `required`. So each declares one, and each declares it
|
|
210
|
+
* from the catalogue rather than from a canned string, because an example naming a token the source
|
|
211
|
+
* doesn't have teaches a caller to write a refusal.
|
|
212
|
+
*
|
|
213
|
+
* A pin with nothing attached has no catalogue to read, so the op shows no example rather than an
|
|
214
|
+
* invented one.
|
|
215
|
+
*/
|
|
216
|
+
function exampleBorrowable(config, slug) {
|
|
217
|
+
const source = config.sourceFor(slug);
|
|
218
|
+
if (!source) return void 0;
|
|
219
|
+
for (const kind of source.ownedKinds()) {
|
|
220
|
+
if (!config.entityOf(kind)) continue;
|
|
221
|
+
if (!kindBorrowable(config.entityOf(kind), {
|
|
222
|
+
config,
|
|
223
|
+
source
|
|
224
|
+
})) continue;
|
|
225
|
+
const [first] = source.list(kind);
|
|
226
|
+
if (first) return {
|
|
227
|
+
kind,
|
|
228
|
+
from: first.path
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const pinOptions = z.record(z.string(), z.record(z.string(), z.strictObject({
|
|
233
|
+
override: z.record(z.string(), z.unknown()).optional(),
|
|
234
|
+
extend: z.record(z.string(), z.unknown()).optional(),
|
|
235
|
+
hidden: z.boolean().optional()
|
|
236
|
+
}).refine((entry) => entry.override !== void 0 || entry.extend !== void 0 || entry.hidden !== void 0, { message: "An `options` entry needs `override`, `extend` or `hidden`." }))).optional();
|
|
237
|
+
const LinkedSystem = defineEntity({
|
|
238
|
+
kind: "linked-system",
|
|
239
|
+
label: "Linked system",
|
|
240
|
+
labelPlural: "Linked systems",
|
|
241
|
+
schemas: {
|
|
242
|
+
/**
|
|
243
|
+
* Draw entities from a linked system: the pin's `link` declaration grows to cover them and the
|
|
244
|
+
* build mints a pointer row per entity, in this one op. A row of this config's own at a covered
|
|
245
|
+
* path is deleted so theirs shows through; a row held through another pin becomes an extension
|
|
246
|
+
* through this one. `link-plan` says what that replaces before anything is written.
|
|
247
|
+
*/
|
|
248
|
+
link: {
|
|
249
|
+
input: linkInput,
|
|
250
|
+
readOnly: false,
|
|
251
|
+
label: "Link from",
|
|
252
|
+
description: "Draw entities from a linked system, following the source: `data` names them by accessor — `{ tokens: ['color/brand', 'color'] }` takes an entity and a folder's members, `{ tokens: '*' }` every token, and `'*'` the whole system. What this config already holds at those paths is replaced by theirs, so run `linked-system/link-plan` with the same input first to see what that is. To hold their values as your own instead, `copy`.",
|
|
253
|
+
scope: "item",
|
|
254
|
+
approval: "reference-change",
|
|
255
|
+
title: (entity) => `Link something from ${entity}`,
|
|
256
|
+
example: linkExample,
|
|
257
|
+
handler: (input, config) => {
|
|
258
|
+
const { patches } = planLink({
|
|
259
|
+
config,
|
|
260
|
+
input: {
|
|
261
|
+
path: String(input.path),
|
|
262
|
+
data: input.data
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
if (patches.length === 0) return config;
|
|
266
|
+
return config.applyAll(patches).withDerivedRows();
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
"link-plan": {
|
|
270
|
+
input: linkInput,
|
|
271
|
+
readOnly: true,
|
|
272
|
+
label: "Plan a link",
|
|
273
|
+
description: "What `link` with the same input replaces — rows of yours deleted so the source shows through, rows held through another pin turned into extensions, and members left alone because they are already here from the same origin. Writes nothing.",
|
|
274
|
+
scope: "item",
|
|
275
|
+
title: (entity) => `Plan a link from ${entity}`,
|
|
276
|
+
example: linkExample,
|
|
277
|
+
handler: (input, config) => {
|
|
278
|
+
const { patches: _patches, ...plan } = planLink({
|
|
279
|
+
config,
|
|
280
|
+
input: {
|
|
281
|
+
path: String(input.path),
|
|
282
|
+
data: input.data
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
return plan;
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
/**
|
|
289
|
+
* Stop drawing from a linked system, whole or in part. The declaration narrows and the rows it
|
|
290
|
+
* minted retire in the same op; the pin stays, so it can be linked again.
|
|
291
|
+
*/
|
|
292
|
+
unlink: {
|
|
293
|
+
input: unlinkInput,
|
|
294
|
+
readOnly: false,
|
|
295
|
+
label: "Unlink from",
|
|
296
|
+
description: "Stop following a linked system's entities, in `link`'s shape or everything when `data` is omitted. The rows the declaration minted go with it, and so do the linked rows that point at them, since left linked they would bring them back; nothing of yours moves. A row of yours pointing at one of theirs refuses this — `copy` what is used first, or `break` with `keep: true`. `unlink-plan` names everything that goes.",
|
|
297
|
+
scope: "item",
|
|
298
|
+
approval: "reference-change",
|
|
299
|
+
title: (entity) => `Unlink from ${entity}`,
|
|
300
|
+
example: pinExample,
|
|
301
|
+
handler: (input, config) => {
|
|
302
|
+
const { patches, blocked } = planUnlink({
|
|
303
|
+
config,
|
|
304
|
+
input: {
|
|
305
|
+
path: String(input.path),
|
|
306
|
+
...input.data === void 0 ? {} : { data: input.data }
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
if (blocked !== void 0) rejectData(blocked);
|
|
310
|
+
if (patches.length === 0) return config;
|
|
311
|
+
return config.applyAll(patches);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
"unlink-plan": {
|
|
315
|
+
input: unlinkInput,
|
|
316
|
+
readOnly: true,
|
|
317
|
+
label: "Plan an unlink",
|
|
318
|
+
description: "What `unlink` with the same input removes — the minted rows the narrower declaration no longer reaches, with the linked rows taken along because they point at them named in `cascade` — and why the config would refuse it. Writes nothing.",
|
|
319
|
+
scope: "item",
|
|
320
|
+
title: (entity) => `Plan unlinking from ${entity}`,
|
|
321
|
+
example: pinExample,
|
|
322
|
+
handler: (input, config) => {
|
|
323
|
+
const { patches: _patches, ...plan } = planUnlink({
|
|
324
|
+
config,
|
|
325
|
+
input: {
|
|
326
|
+
path: String(input.path),
|
|
327
|
+
...input.data === void 0 ? {} : { data: input.data }
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
return plan;
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
/**
|
|
334
|
+
* Break the link: the pin goes, and everything drawn through it either becomes this config's own
|
|
335
|
+
* or goes with it.
|
|
336
|
+
*/
|
|
337
|
+
break: {
|
|
338
|
+
input: breakInput,
|
|
339
|
+
readOnly: false,
|
|
340
|
+
label: "Break link",
|
|
341
|
+
description: "Remove a linked system. With `keep: true` every value drawn through it is copied in as your own first, and a row of yours that pointed through the pin points at the copies; with `keep: false` the rows it minted go, and a row of yours still pointing through it refuses the break. Run `linked-system/break-plan` first to see every row it reaches.",
|
|
342
|
+
scope: "item",
|
|
343
|
+
approval: "destructive",
|
|
344
|
+
title: (entity) => `Break the link to ${entity}`,
|
|
345
|
+
example: breakExample,
|
|
346
|
+
handler: (input, config) => {
|
|
347
|
+
const { patches } = planBreak({
|
|
348
|
+
config,
|
|
349
|
+
input: {
|
|
350
|
+
path: String(input.path),
|
|
351
|
+
keep: Boolean(input.keep)
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
return config.applyAll(patches);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
"break-plan": {
|
|
358
|
+
input: breakPlanInput,
|
|
359
|
+
readOnly: true,
|
|
360
|
+
label: "Plan a break",
|
|
361
|
+
description: "Every row a break reaches, and why the config would refuse it. `keep` plans `break` with the same input; `data` in `copy`'s shape plans detaching those entities alone — the rows the copy lands, the rows of yours it points at the copies, and the rows still linked that reference what moves. Writes nothing.",
|
|
362
|
+
scope: "item",
|
|
363
|
+
title: (entity) => `Plan breaking the link to ${entity}`,
|
|
364
|
+
example: breakExample,
|
|
365
|
+
handler: (input, config) => {
|
|
366
|
+
const { patches: _patches, ...plan } = planBreak({
|
|
367
|
+
config,
|
|
368
|
+
input: {
|
|
369
|
+
path: String(input.path),
|
|
370
|
+
...input.keep === void 0 ? {} : { keep: Boolean(input.keep) },
|
|
371
|
+
...input.data === void 0 ? {} : { data: input.data }
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
return plan;
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
/**
|
|
378
|
+
* The source's catalogue read against this config: one row per top-level folder or item the
|
|
379
|
+
* source offers, saying whether it is linked here and what linking it would replace.
|
|
380
|
+
*/
|
|
381
|
+
rows: {
|
|
382
|
+
input: rowsInput,
|
|
383
|
+
readOnly: true,
|
|
384
|
+
label: "Source rows",
|
|
385
|
+
description: "Every top-level folder and item the linked system offers, read against this config: whether every member is linked through this pin, how many rows of yours a `link` would replace, and how many members are already here from the source's own origin. Needs the source attached; empty otherwise.",
|
|
386
|
+
scope: "item",
|
|
387
|
+
title: (entity) => `What ${entity} offers`,
|
|
388
|
+
example: pinExample,
|
|
389
|
+
handler: (input, config) => sourceRowStates({
|
|
390
|
+
config,
|
|
391
|
+
slug: String(input.path)
|
|
392
|
+
})
|
|
393
|
+
},
|
|
394
|
+
/**
|
|
395
|
+
* Take entities from a linked system as this config's own — the source's bodies, written once.
|
|
396
|
+
* A pin puts a system within reach and its `link` mints pointers; `copy` stores the value. A copied
|
|
397
|
+
* row is an ordinary authored row from the moment it lands: the source's later versions never reach
|
|
398
|
+
* it, and it renames and deletes like anything else here.
|
|
399
|
+
*
|
|
400
|
+
* A copy stands on its own. What a copied row points at comes with it — a component brings the
|
|
401
|
+
* style properties and tokens it names, a style property brings the token folder its values
|
|
402
|
+
* range over, a code component brings the files its module reaches — and every one of those refs
|
|
403
|
+
* lands bare, on the local copy. A path this config already holds as its own is reused rather
|
|
404
|
+
* than copied, and refs bind to it. Only a ref to a kind this config can't hold stays qualified,
|
|
405
|
+
* and the pin survives to answer it. Someone who wants theirs by reference writes `Button@slug`
|
|
406
|
+
* instead.
|
|
407
|
+
*
|
|
408
|
+
* A pointer at the path, minted through this pin, becomes the literal it stood for with its
|
|
409
|
+
* `extend` laid over the source's body. A folder's meta comes with its members, so a label the
|
|
410
|
+
* folder was inheriting becomes its own.
|
|
411
|
+
*
|
|
412
|
+
* `data` is keyed the way the pin's `link` is: by accessor, then `'*'` for every entry of the kind
|
|
413
|
+
* or the source's paths — an item, or a folder, which takes its members. `'*'` alone takes every
|
|
414
|
+
* kind the source offers and this config can hold. Kinds taken in one edit are copied together,
|
|
415
|
+
* which is what lets a copied component point at the copied tokens rather than back through the
|
|
416
|
+
* pin.
|
|
417
|
+
*
|
|
418
|
+
* Every row lands in one derive (`Config.applyAll`), and each is its own `create` or `replace`
|
|
419
|
+
* patch carrying the literal body, so a draft replays a copy onto a base with nothing attached.
|
|
420
|
+
*/
|
|
421
|
+
copy: {
|
|
422
|
+
input: copyInput,
|
|
423
|
+
readOnly: false,
|
|
424
|
+
label: "Copy from",
|
|
425
|
+
description: "Take entities from a linked system as your own, holding the values the source has now and no longer following it. `data` names them by accessor: `{ tokens: ['color/brand', 'color'] }` takes an entity and a folder's members, `{ tokens: '*' }` every token, and `'*'` the whole system. What a copied entity points at comes with it, so run `linked-system/copy-plan` with the same input first and see everything that lands. To use theirs without owning it, declare it in the pin's `link` instead. A whole-system copy is thousands of rows, and on a repo-backed system each one is written into `uds.config.ts`.",
|
|
426
|
+
scope: "item",
|
|
427
|
+
approval: "reference-change",
|
|
428
|
+
title: (entity) => `Copy something from ${entity}`,
|
|
429
|
+
example: copyExample,
|
|
430
|
+
handler: (input, config) => {
|
|
431
|
+
const { patches } = planCopy({
|
|
432
|
+
config,
|
|
433
|
+
input: copyInputOf(input)
|
|
434
|
+
});
|
|
435
|
+
return config.applyAll(patches);
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
/**
|
|
439
|
+
* What that same `copy` call would do, without doing it — the rows that land, what comes along
|
|
440
|
+
* because something named points at it, and the rows of yours a copied ref binds to instead.
|
|
441
|
+
*
|
|
442
|
+
* A read, taking `copy`'s own input verbatim, so a surface plans the invocation it is about to
|
|
443
|
+
* send. The op is this plan applied, so the two cannot disagree.
|
|
444
|
+
*/
|
|
445
|
+
"copy-plan": {
|
|
446
|
+
input: copyInput,
|
|
447
|
+
readOnly: true,
|
|
448
|
+
label: "Plan a copy",
|
|
449
|
+
description: "See what copying would bring before you do it — what lands as yours, what comes along because it is pointed at, and what you already hold that the copies will point at. Takes exactly the same input as `copy` and writes nothing. Run it before `copy`, and always before a `'*'` copy.",
|
|
450
|
+
scope: "item",
|
|
451
|
+
title: (entity) => `Plan a copy from ${entity}`,
|
|
452
|
+
example: copyExample,
|
|
453
|
+
handler: (input, config) => {
|
|
454
|
+
const { patches: _patches, ...plan } = planCopy({
|
|
455
|
+
config,
|
|
456
|
+
input: copyInputOf(input)
|
|
457
|
+
});
|
|
458
|
+
return plan;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
fields: z.strictObject({
|
|
463
|
+
/**
|
|
464
|
+
* The sibling config this link resolves from — the repo-local form, and the primary field, so it is
|
|
465
|
+
* declared as a bare string.
|
|
466
|
+
*
|
|
467
|
+
* A config path rather than a directory, because `-c, --config <path>` is the convention everywhere
|
|
468
|
+
* else and a package may hold more than one config (web and native), so a directory would be
|
|
469
|
+
* ambiguous about which system you meant. Naming the config also lets install check
|
|
470
|
+
* `.uds/build-context.json`, so pointing at one config and reading another's output is an error
|
|
471
|
+
* rather than a silent wrong answer.
|
|
472
|
+
*
|
|
473
|
+
* ```ts
|
|
474
|
+
* linkedSystems: {
|
|
475
|
+
* ds: '../design-system/uds.config.ts', // repo-local
|
|
476
|
+
* yahooOs: LinkedSystem.item({ systemId, name, version }), // published
|
|
477
|
+
* }
|
|
478
|
+
* ```
|
|
479
|
+
*
|
|
480
|
+
* Both forms are declared in one place, because "which system" and "where from" are one decision
|
|
481
|
+
* per link. npm spells it the same way, with the value saying both (`"workspace:*"` against
|
|
482
|
+
* `"^2.4.0"`).
|
|
483
|
+
*
|
|
484
|
+
* It is `configPath` and not `path`, because an entity's identity is its `path`: `Entity` assigns
|
|
485
|
+
* fields onto itself, so a field called `path` would overwrite the slug and key the link by its own
|
|
486
|
+
* directory. Nor `workspace`, which is a product noun here (org → workspace → team → system). It is
|
|
487
|
+
* literally a config path, and it mirrors `githubConfigPath` on `systems`, the field push derives
|
|
488
|
+
* this link's identity against.
|
|
489
|
+
*
|
|
490
|
+
* **`configPath` wins for resolution.** When it is set, the build reads the sibling's artifact and
|
|
491
|
+
* ignores `version`: a repo-local link tracks whatever the sibling currently is, which is the point
|
|
492
|
+
* in a monorepo, and a built artifact carries no `systemId` or `version` to pin against.
|
|
493
|
+
*/
|
|
494
|
+
configPath: z.string().min(1).optional(),
|
|
495
|
+
/**
|
|
496
|
+
* What this system draws from that source — a standing declaration, and the thing the build turns
|
|
497
|
+
* into rows.
|
|
498
|
+
* ```ts
|
|
499
|
+
* link: '*' // everything, every kind
|
|
500
|
+
* link: { tokens: '*', icons: '*' } // those kinds whole
|
|
501
|
+
* link: { tokens: ['color/brand', 'color/paper'] } // those entities
|
|
502
|
+
* link: { components: ['actions'] } // a folder: its members
|
|
503
|
+
* ```
|
|
504
|
+
* `Config.withDerivedRows` reads it and makes a pointer row per entity it covers — on every build,
|
|
505
|
+
* and inside the apply of any patch to this pin — so a link's cost in authored source is this one
|
|
506
|
+
* declaration whatever the source's size, and the rows come and go with it.
|
|
507
|
+
*
|
|
508
|
+
* Keyed by accessor (`tokens`, `components`) rather than by kind (`token`, `component`), because
|
|
509
|
+
* this is read in a config file, and a config file is keyed by accessor everywhere else —
|
|
510
|
+
* `register({ tokens, components })`, `sources: { linkedSystems: … }`. Ops and refs use the kind.
|
|
511
|
+
* `Config.accessorOf` maps between them, off the config type's own manifest, so no kind is named
|
|
512
|
+
* here. Paths are the source's own; a path that names a folder there covers its members.
|
|
513
|
+
*
|
|
514
|
+
* **Standing, so a source that grows is covered without re-running anything.** The declaration is
|
|
515
|
+
* read on every build, and what it produces is an ordinary stored row — indistinguishable from a
|
|
516
|
+
* borrow written by hand, which is what keeps the rename gate, `inlineAllSources` when it seals an
|
|
517
|
+
* artifact and `emitBorrowedComponents` when it emits a module all working unchanged.
|
|
518
|
+
*
|
|
519
|
+
* A path this config already holds is left alone, silently, whether it authored that row or
|
|
520
|
+
* borrowed it. Local shadows a source, and that is what makes deriving over a grown source
|
|
521
|
+
* additive rather than a set of conflicts to resolve. It is also how one of their entities is kept
|
|
522
|
+
* out: author your own at that path, or narrow what this declares.
|
|
523
|
+
*
|
|
524
|
+
* Deleting a row this covers is refused ({@link EntityKind.bulkAdopts} is what reads it): the
|
|
525
|
+
* build would make it again on the next pass, and a delete that comes back is worse than one that
|
|
526
|
+
* was refused. To own one of their entities, `copy` it; to stop drawing it, `unlink` it.
|
|
527
|
+
*
|
|
528
|
+
* The gestures that write this are the kind's own ops — `link`, `unlink`, `break` — each with a
|
|
529
|
+
* `-plan` read beside it, so a surface never assembles the declaration by hand.
|
|
530
|
+
*
|
|
531
|
+
* `'*'` means every kind that may be drawn on, which is every kind that may be borrowed — each
|
|
532
|
+
* kind says which it is (`EntityKind.linkable`). Nothing is subtracted: a pin and a `file` aren't
|
|
533
|
+
* borrowable, so no borrow of one exists for this to cover.
|
|
534
|
+
*/
|
|
535
|
+
link: z.union([z.literal("*"), z.record(z.string(), z.union([z.literal("*"), z.array(z.string().min(1)).min(1)]))]).optional(),
|
|
536
|
+
/**
|
|
537
|
+
* What this system does to individual entities of the source, keyed by accessor, then by the path
|
|
538
|
+
* the source spells, then by verb.
|
|
539
|
+
*
|
|
540
|
+
* ```ts
|
|
541
|
+
* options: {
|
|
542
|
+
* tokens: {
|
|
543
|
+
* 'color/brand': {
|
|
544
|
+
* override: { modifiers: { dark: Token.ref('spectrum/violet/70') } },
|
|
545
|
+
* extend: { modifiers: { highContrast: Token.ref('spectrum/violet/90') } },
|
|
546
|
+
* },
|
|
547
|
+
* 'palette/legacy': { hidden: true },
|
|
548
|
+
* },
|
|
549
|
+
* }
|
|
550
|
+
* ```
|
|
551
|
+
*
|
|
552
|
+
* `override` changes a value the source defines and `extend` covers a case it does not. Both land in
|
|
553
|
+
* the row's `extend`, so a bulk adjustment and a hand-written `.extend(...)` store the same thing and
|
|
554
|
+
* nothing downstream can tell which spelling made it. Both may sit on one path: a token overridden
|
|
555
|
+
* under one modifier and extended under another is one entity pulled two ways.
|
|
556
|
+
*
|
|
557
|
+
* OPEN: `overlayBody` combines the two with a spread, so two verbs naming one top-level field keep
|
|
558
|
+
* only the `extend` half — the example above loses its dark override. They have to merge by member,
|
|
559
|
+
* the way the stored `extend` merges into the source's body.
|
|
560
|
+
*
|
|
561
|
+
* Which verb applies is derivable — comparing the body against the source says whether a leaf is
|
|
562
|
+
* new — and the build does compare, to check the declaration. It is declared anyway because that is
|
|
563
|
+
* what notices the source moving: a field they add later turns an extension into an override
|
|
564
|
+
* silently, and a declared verb is what has something to disagree with.
|
|
565
|
+
*
|
|
566
|
+
* `hidden` keeps the entity and stops offering it; references already resolving through it have to
|
|
567
|
+
* keep working, so nothing about the output changes. Carried on {@link SourceOverlay}, and read by no
|
|
568
|
+
* authoring surface yet.
|
|
569
|
+
*
|
|
570
|
+
* A field rather than accessor keys at the pin's top level, so the pin stays a closed object and a
|
|
571
|
+
* misspelled `systemId` is still refused. Only these keys are open, and they are accessor names
|
|
572
|
+
* for the reason `link` takes accessors: a config file is accessor-keyed everywhere else.
|
|
573
|
+
*
|
|
574
|
+
* OPEN: nothing checks them against the config's own accessors, so a misspelled `toknes` covers no
|
|
575
|
+
* path and says nothing.
|
|
576
|
+
*/
|
|
577
|
+
options: pinOptions,
|
|
578
|
+
/**
|
|
579
|
+
* An npm package that ships a built system — `@acme/design-system`.
|
|
580
|
+
*
|
|
581
|
+
* The third origin, and the one a consumer outside the publisher's monorepo and off their Create
|
|
582
|
+
* instance actually has. Resolution walks the `node_modules` ancestry for the name and reads that
|
|
583
|
+
* package's own build output, so the package has to ship its `.uds/` directory.
|
|
584
|
+
*
|
|
585
|
+
* **A name, not a path into `node_modules`.** A path would hardcode a hoisting layout, which is the
|
|
586
|
+
* one thing about `node_modules` a consumer can't predict, and is why the resolver walks upward. A
|
|
587
|
+
* name also keeps the origin legible: a linking surface can say "from npm" instead of
|
|
588
|
+
* pattern-matching a directory.
|
|
589
|
+
*
|
|
590
|
+
* **npm does the pinning, so there is no `version` here.** A `package` link tracks whatever is
|
|
591
|
+
* installed, the way a `configPath` link tracks whatever the sibling currently is. `package.json`
|
|
592
|
+
* and the lockfile already decide it, so a second copy here would be a version nobody resolves
|
|
593
|
+
* against. `version` stays observed for this origin, as it is for a sibling.
|
|
594
|
+
*/
|
|
595
|
+
package: z.string().min(1).optional(),
|
|
596
|
+
/**
|
|
597
|
+
* The source's database id — durable identity, which survives a rename on their side.
|
|
598
|
+
*
|
|
599
|
+
* Optional, because a repo-local link has none to give. Identity is a Create concept resolved from
|
|
600
|
+
* the repo binding, and the only thing that reads it is Studio, which is only reached by `uds push`.
|
|
601
|
+
* So push derives it for a `configPath` link and writes it into the artifact, and nothing local asks
|
|
602
|
+
* an author to hand-type an unverifiable id.
|
|
603
|
+
*/
|
|
604
|
+
systemId: z.string().min(1).optional(),
|
|
605
|
+
/** Display only, never identity: what to call this source in the UI. Defaults to the slug. */
|
|
606
|
+
name: z.string().optional(),
|
|
607
|
+
/**
|
|
608
|
+
* The exact published version every ref through this link resolves against.
|
|
609
|
+
*
|
|
610
|
+
* For a `configPath` link this is **observed rather than pinned**: push records what the sibling was
|
|
611
|
+
* at push time so Studio can say which version it saw, and the local build still tracks current
|
|
612
|
+
* state. Read as a pin, a repo-local link would go stale the moment the sibling moved.
|
|
613
|
+
*/
|
|
614
|
+
version: z.string().regex(EXACT_VERSION, "A link is pinned to an exact version (2.4.0), not a range").optional()
|
|
615
|
+
}),
|
|
616
|
+
computed: {
|
|
617
|
+
/**
|
|
618
|
+
* What to call this source — its `name`, or the slug when it declares none.
|
|
619
|
+
*
|
|
620
|
+
* The fallback belongs here because the default is a fact about the field: `name` is optional, and
|
|
621
|
+
* a source with none is the slug it's filed under. Left to consumers, every surface that renders a
|
|
622
|
+
* source implements the same `??`, and one of them eventually renders `undefined`.
|
|
623
|
+
*/
|
|
624
|
+
displayName: ({ member, path }) => typeof member.name === "string" && member.name.length > 0 ? member.name : path },
|
|
625
|
+
/**
|
|
626
|
+
* The fields that decide which system a slug resolves against — the same ones `uds install` records
|
|
627
|
+
* in `pin.json`. `name` is display only, so a value borrowed through this link survives a rename of
|
|
628
|
+
* the label.
|
|
629
|
+
*/
|
|
630
|
+
sourceIdentity: [
|
|
631
|
+
"configPath",
|
|
632
|
+
"package",
|
|
633
|
+
"systemId",
|
|
634
|
+
"version"
|
|
635
|
+
],
|
|
636
|
+
/** Which system, at whatever version: two pins agreeing here draw on one origin. */
|
|
637
|
+
sourceOrigin: [
|
|
638
|
+
"configPath",
|
|
639
|
+
"package",
|
|
640
|
+
"systemId"
|
|
641
|
+
],
|
|
642
|
+
/**
|
|
643
|
+
* A pin is never borrowed, only declared. Taking one from a source would make their dependencies
|
|
644
|
+
* yours, at their versions, including systems you have no access to and no way to see — and the slug
|
|
645
|
+
* would join `sourceSlugs()` as a source this config must resolve on its own. What motivates it needs
|
|
646
|
+
* none of that: a borrow whose value points at a system two hops away already resolves, because
|
|
647
|
+
* `ownerOf` follows the chain through the source that declared it.
|
|
648
|
+
*
|
|
649
|
+
* Wanting a direct dependency on what a source depends on is reasonable, and it is a pin of your own,
|
|
650
|
+
* with a version you chose.
|
|
651
|
+
*/
|
|
652
|
+
linkable: false,
|
|
653
|
+
/**
|
|
654
|
+
* What a resolver for this kind may answer with. The framework supplies the `attached`/`unavailable`
|
|
655
|
+
* tag; these two say what each branch of this kind's answer carries.
|
|
656
|
+
*/
|
|
657
|
+
sourceResolution: {
|
|
658
|
+
resolved: resolvedSource,
|
|
659
|
+
unavailable: sourceUnavailable
|
|
660
|
+
},
|
|
661
|
+
/**
|
|
662
|
+
* Whether this pin's `link` declaration covers one of the source's entities — the derive pass's
|
|
663
|
+
* question and the delete gate's, answered here because `link` is this kind's field and its shape
|
|
664
|
+
* is this kind's to interpret.
|
|
665
|
+
*
|
|
666
|
+
* `'*'` at either level covers everything under it; a list covers the paths it names, as the source
|
|
667
|
+
* spells them, and a named folder covers its members. Nothing excludes a kind here — a kind a
|
|
668
|
+
* whole-source take does not reach is one no borrow of it exists for (`EntityKind.linkable`), so
|
|
669
|
+
* the claim is unreachable rather than filtered.
|
|
670
|
+
*/
|
|
671
|
+
bulkAdopts: ({ body, kind, accessor, path }) => {
|
|
672
|
+
const declared = body.link;
|
|
673
|
+
if (declared === void 0) return false;
|
|
674
|
+
if (declared === "*") return true;
|
|
675
|
+
const paths = declared[accessor ?? kind];
|
|
676
|
+
if (paths === void 0) return false;
|
|
677
|
+
if (paths === "*") return true;
|
|
678
|
+
return paths.some((declaredPath) => declaredPath === path || path.startsWith(`${declaredPath}/`));
|
|
679
|
+
},
|
|
680
|
+
bulkOverlay: ({ body, kind, accessor, path }) => {
|
|
681
|
+
const directive = directiveAt(body, accessor ?? kind, path);
|
|
682
|
+
if (!directive) return void 0;
|
|
683
|
+
return {
|
|
684
|
+
override: directive.override,
|
|
685
|
+
extend: directive.extend,
|
|
686
|
+
hidden: directive.hidden === true
|
|
687
|
+
};
|
|
688
|
+
},
|
|
689
|
+
/**
|
|
690
|
+
* The field-path address `options` stores one entry at — accessor, then the path the source
|
|
691
|
+
* spells, so an edit to a minted row lands on the same key {@link directiveAt} reads back.
|
|
692
|
+
*/
|
|
693
|
+
bulkDirectives: (body) => {
|
|
694
|
+
const options = body.options;
|
|
695
|
+
if (!options) return [];
|
|
696
|
+
return Object.entries(options).flatMap(([accessor, entries]) => isPlainObject(entries) ? Object.keys(entries).map((path) => ({
|
|
697
|
+
accessor,
|
|
698
|
+
path
|
|
699
|
+
})) : []);
|
|
700
|
+
},
|
|
701
|
+
bulkOverlayWrite: ({ accessor, path, overlay }) => ({ [`options.${accessor}.${path}`]: overlay })
|
|
702
|
+
});
|
|
703
|
+
/** The `options` entry for one path, or `undefined` where the pin says nothing about it. */
|
|
704
|
+
function directiveAt(body, accessor, path) {
|
|
705
|
+
return body.options?.[accessor]?.[path];
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* The source an entity comes from — `undefined` for one wholly this config's own, and for one that
|
|
709
|
+
* involves two systems, since naming either would label the other wrongly. See `Entity.source`.
|
|
710
|
+
*
|
|
711
|
+
* This is the provenance read. It folds both ways an entity can involve another system — borrowed and
|
|
712
|
+
* locally extended, or authored here with a value pointing through a link — because a badge, an
|
|
713
|
+
* adornment and a "Source" row want the same answer either way. {@link borrowedItem} is the other read,
|
|
714
|
+
* the one a write gate wants, and picking the wrong one is quiet in both directions.
|
|
715
|
+
*
|
|
716
|
+
* Two hops: the entity's `source` answers which slug, and this kind answers for the slug. It lives
|
|
717
|
+
* beside the kind rather than on `Config` because the answer is one of these items, typed off the schema
|
|
718
|
+
* above. A `Config` method could only hand back a bare `Entity` or a bag naming the fields this kind
|
|
719
|
+
* happens to declare, and every consumer of that bag has to be found again when the kind gains one.
|
|
720
|
+
*
|
|
721
|
+
* Named for the item it answers about, because {@link Config.sourceFor} already answers the
|
|
722
|
+
* slug-to-config question and `sourceOf` already reads the slug off a ref.
|
|
723
|
+
*/
|
|
724
|
+
function sourceOfItem({ config, kind, path }) {
|
|
725
|
+
const slug = config.resolve(kind, path)?.source;
|
|
726
|
+
return slug === void 0 ? void 0 : LinkedSystem.get(config, slug);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* The source a group draws through — set only when every member agrees on one.
|
|
730
|
+
*
|
|
731
|
+
* The agreement rule is kind-agnostic and lives on {@link Config.groupSource}. This makes the same
|
|
732
|
+
* second hop {@link sourceOfItem} makes, so a surface gets the typed pin rather than a slug it would
|
|
733
|
+
* have to look up itself.
|
|
734
|
+
*/
|
|
735
|
+
function sourceOfGroup({ config, kind, groupPath }) {
|
|
736
|
+
const slug = config.groupSource({
|
|
737
|
+
kind,
|
|
738
|
+
groupPath
|
|
739
|
+
});
|
|
740
|
+
return slug === void 0 ? void 0 : LinkedSystem.get(config, slug);
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* The source an item is — `undefined` for one this config authored, including one whose own fields
|
|
744
|
+
* point through a link.
|
|
745
|
+
*
|
|
746
|
+
* The write gate's reader, where {@link sourceOfItem} is the badge's. They are easy to swap, and the
|
|
747
|
+
* failure is quiet in both directions: gating on `sourceOfItem` locks a local alias its own author
|
|
748
|
+
* wrote, and badging on this one leaves a borrowed value looking native.
|
|
749
|
+
*
|
|
750
|
+
* To choose between them, ask whether the other side is affected. Retargeting a ref we declared changes
|
|
751
|
+
* nothing but our artifact, so it is ours; the fields of an item we borrowed are the source's, and a
|
|
752
|
+
* local edit to them can only be an overlay. See `Entity.borrowedFrom`.
|
|
753
|
+
*/
|
|
754
|
+
function borrowedItem({ config, kind, path }) {
|
|
755
|
+
const slug = config.resolve(kind, path)?.borrowedFrom;
|
|
756
|
+
return slug === void 0 ? void 0 : LinkedSystem.get(config, slug);
|
|
757
|
+
}
|
|
758
|
+
/** The source a whole group is borrowed from — {@link borrowedItem}'s group form, and the same second
|
|
759
|
+
* hop {@link sourceOfGroup} makes. Set only when every member agrees, which is `Config`'s rule to
|
|
760
|
+
* state and not this one's. Gates what can be done to the folder: adding, renaming, reordering. */
|
|
761
|
+
function borrowedGroup({ config, kind, groupPath }) {
|
|
762
|
+
const slug = config.groupBorrowedFrom({
|
|
763
|
+
kind,
|
|
764
|
+
groupPath
|
|
765
|
+
});
|
|
766
|
+
return slug === void 0 ? void 0 : LinkedSystem.get(config, slug);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* A config path as a source is indexed by — separators normalized and a leading `./` dropped, so
|
|
770
|
+
* `./a/b` and `a/b` name one source rather than two.
|
|
771
|
+
*
|
|
772
|
+
* Not exported. It is the grammar of the `configPath` field, and the question a caller has is
|
|
773
|
+
* {@link sourceSlugFor}, which applies it. A caller normalizing paths itself is a second copy of this
|
|
774
|
+
* rule, and the copies disagree the first time one forgets a case.
|
|
775
|
+
*/
|
|
776
|
+
const configPathKey = (value) => value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
777
|
+
/**
|
|
778
|
+
* The slug this config already draws a source under, or `undefined` when it doesn't draw it at all.
|
|
779
|
+
*
|
|
780
|
+
* Takes either way a source can be named, because the same system reached two ways is the same system:
|
|
781
|
+
* a form offering to draw on one that is already a source should say so and name the slug it lives
|
|
782
|
+
* under, rather than offering a second declaration under another name. Which fields those are is this
|
|
783
|
+
* kind's to know ({@link LinkedSystem.sourceIdentity}); a caller that indexed them itself would be
|
|
784
|
+
* restating the schema.
|
|
785
|
+
*/
|
|
786
|
+
function sourceSlugFor({ config, configPath, systemId }) {
|
|
787
|
+
if (!config) return void 0;
|
|
788
|
+
const wanted = configPath ? configPathKey(configPath) : void 0;
|
|
789
|
+
for (const entry of LinkedSystem.list(config)) {
|
|
790
|
+
if (wanted !== void 0 && entry.configPath !== void 0 && configPathKey(entry.configPath) === wanted) return entry.path;
|
|
791
|
+
if (systemId && entry.systemId === systemId) return entry.path;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* A system's name as a starting slug — lowercase, a single path segment as {@link LINK_SLUG} wants,
|
|
796
|
+
* and not one this config has already taken.
|
|
797
|
+
*
|
|
798
|
+
* The suffix matters because the suggestion is applied on open and on every pick, before anyone has
|
|
799
|
+
* typed: suggesting a slug that already exists greets you with a validation error you didn't cause and
|
|
800
|
+
* a dead submit button. It is for two different systems whose names normalize to the same slug — the
|
|
801
|
+
* same system declared twice is refused outright, by {@link sourceSlugFor}.
|
|
802
|
+
*/
|
|
803
|
+
function suggestLinkSlug({ name, taken }) {
|
|
804
|
+
const base = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
805
|
+
if (!base) return "";
|
|
806
|
+
if (!isLinkSlug(base)) return "";
|
|
807
|
+
if (!taken.has(base)) return base;
|
|
808
|
+
for (let suffix = 2;; suffix++) {
|
|
809
|
+
const candidate = `${base}-${suffix}`;
|
|
810
|
+
if (!taken.has(candidate)) return candidate;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
//#endregion
|
|
814
|
+
export { LINK_SLUG, LINK_SLUG_MESSAGE, LinkedSystem, borrowedGroup, borrowedItem, isLinkSlug, resolvedSource, sourceOfGroup, sourceOfItem, sourceSlugFor, sourceUnavailable, suggestLinkSlug };
|