@yahoo/uds-create-config 5.41.7 → 5.42.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.
@@ -60,36 +60,35 @@ const buildFields = z.strictObject({
60
60
  description: "The directory every sealed component file lives under, relative to the config. Emitted modules keep the layout beneath it. Unset, the `uds/components/` folder convention applies."
61
61
  }).optional(),
62
62
  /**
63
- * Files to ship that no component imports — a toast queue's `createToast`, a motion wrapper like
64
- * yos-v3's `SpringMotionConfig` — as paths relative to the config.
63
+ * Files under {@link inputDir} to keep out of what the system ships, as paths or globs relative to
64
+ * the config: `src/components/legacy`, `src/components/**\/*.mock.ts`.
65
65
  *
66
- * A build only emits component files and what they import, so a module apps import on its own
67
- * never reached the output. That bites hardest for a system that links this one: its build emits
68
- * only what this system's components reach, so it had nothing to import the module from. This
69
- * system's own apps couldn't import `#uds/components/<file>` either. Listing a file here emits it,
70
- * and what it imports, at its own path under `components/` — `#uds/components/toast-portal` here,
71
- * `@/uds/registry/<slug>/toast-portal` for a consumer that links it.
72
- *
73
- * Each has to live under {@link inputDir}, like every other file the output mirrors. Shipping
74
- * every file by default is planned.
66
+ * Every source file under `inputDir` ships by default, with what it imports. A module no component
67
+ * imports, a toast queue's `createToast` or a motion wrapper like yos-v3's `SpringMotionConfig`,
68
+ * is emitted at its own path under `components/`, so this system's apps import it as
69
+ * `#uds/components/<file>` and a system that links this one as `@/uds/registry/<slug>/<file>`.
70
+ * Tests, stories and type declarations never ship, and neither does a file only the config or a
71
+ * script imports. List a file here to keep it out as well, such as a helper whose npm
72
+ * dependencies a linking team shouldn't have to install. An excluded file nothing else imports is
73
+ * not sealed either.
75
74
  */
76
- include: z.array(z.string().min(1).refine((value) => !value.startsWith("/") && !/^[A-Za-z]:/.test(value) && !value.replace(/\\/g, "/").split("/").includes(".."), { message: "A path in `include` is relative to the config, inside the project." })).meta({
77
- title: "Include",
78
- description: "A build only emits component files and what they import, so a module apps import but no component imports, such as a toast queue's `createToast` or a motion wrapper, never ships: a system that links yours has nothing to import it from, and your own apps can't import `#uds/components/<file>`. List those files here, as paths relative to the config: `src/components/toast-portal.tsx`. Each is emitted with what it imports under `components/`, and a linking system's build emits it too. Shipping every file by default is planned."
75
+ exclude: z.array(z.string().min(1).refine((value) => !value.startsWith("/") && !/^[A-Za-z]:/.test(value) && !value.replace(/\\/g, "/").split("/").includes(".."), { message: "A path in `exclude` is relative to the config, inside the project." })).meta({
76
+ title: "Exclude",
77
+ description: "Every source file under `inputDir` ships by default, with what it imports, so an app can import a module no component imports, such as a toast queue's `createToast`, from `#uds/components/<file>`, and a system that links yours can import it too. Tests, stories, type declarations and files only the config or a script imports never ship. List paths or globs here, relative to the config, to keep other files out: `src/components/legacy`, `src/components/**/*.mock.ts`."
79
78
  }).optional(),
80
79
  /**
81
80
  * Files a team that links this system runs from its installed sources — a migration script that
82
81
  * reads a team's old config and writes their new one — as paths relative to the config.
83
82
  *
84
- * Each is a root of the source graph, as an {@link include}d file is, so it is sealed, pushed and
85
- * installed with the system's sources even though nothing imports it. Unlike an included file it
86
- * is never emitted: a script runs as written, against the authoring package, so it may read the
83
+ * Each is a root of the source graph, so it is sealed, pushed and installed with the system's
84
+ * sources even though nothing imports it. Unlike a shipped module it is never emitted, even under
85
+ * {@link inputDir}: a script runs as written, against the authoring package, so it may read the
87
86
  * config (`Token.get`, `Component.list`) where a shipped module may only read token values. A
88
87
  * linking team runs it from the installed tree, `src/uds/registry/<slug>/<path>`.
89
88
  */
90
89
  scripts: z.array(z.string().min(1).refine((value) => !value.startsWith("/") && !/^[A-Za-z]:/.test(value) && !value.replace(/\\/g, "/").split("/").includes(".."), { message: "A path in `scripts` is relative to the config, inside the project." })).meta({
91
90
  title: "Scripts",
92
- description: "Files a team that links this system runs from its installed sources, such as a migration script that reads their old config and writes their new one, as paths relative to the config: `src/migrate/convert.ts`. Each is sealed, pushed and installed with the system's sources, at `src/uds/registry/<slug>/<path>` in a linking project, but never emitted, so it runs as written against the authoring package and may read the config. A file apps import at runtime belongs in `include` instead."
91
+ description: "Files a team that links this system runs from its installed sources, such as a migration script that reads their old config and writes their new one, as paths relative to the config: `src/migrate/convert.ts`. Each is sealed, pushed and installed with the system's sources, at `src/uds/registry/<slug>/<path>` in a linking project, but never emitted, so it runs as written against the authoring package and may read the config. A file apps import at runtime ships without being listed."
93
92
  }).optional(),
94
93
  /** Where `uds build` writes. A CLI `--output-dir` still wins, because a one-off build into a
95
94
  * scratch directory is not a property of the system. */
@@ -131,6 +130,6 @@ const buildFields = z.strictObject({
131
130
  description: `The React major the app this repo builds for runs. \`18\` wraps each emitted web component in \`forwardRef\`, so a call site's \`ref\` reaches its element. Unset, \`${BUILD_DEFAULTS.react}\`.`,
132
131
  default: BUILD_DEFAULTS.react
133
132
  }).optional()
134
- });
133
+ }, { error: (issue) => issue.code === "unrecognized_keys" && issue.keys.includes("include") ? "`settings.build.include` was removed: every source file under `inputDir` now ships by default. Delete it, and list any file to keep out in `settings.build.exclude`." : void 0 });
135
134
  //#endregion
136
135
  export { BUILD_DEFAULTS, buildFields };
@@ -33,7 +33,7 @@ declare const ReactNativeSystemDefinition: ConfigClass<{
33
33
  }, _$zod_v4_core0.$strict>>;
34
34
  build: _$zod.ZodOptional<_$zod.ZodObject<{
35
35
  inputDir: _$zod.ZodOptional<_$zod.ZodString>;
36
- include: _$zod.ZodOptional<_$zod.ZodArray<_$zod.ZodString>>;
36
+ exclude: _$zod.ZodOptional<_$zod.ZodArray<_$zod.ZodString>>;
37
37
  scripts: _$zod.ZodOptional<_$zod.ZodArray<_$zod.ZodString>>;
38
38
  outDir: _$zod.ZodOptional<_$zod.ZodString>;
39
39
  sourceAlias: _$zod.ZodOptional<_$zod.ZodString>;
@@ -22,6 +22,7 @@ import { motionJsonSchemaMigration } from "../migrations/20261015221052_motion_j
22
22
  import { motionTypeOnlyMigration } from "../migrations/20261016135620_motion_type_only.js";
23
23
  import { motionCompatibilityMigration } from "../migrations/20261016164316_motion_compatibility.js";
24
24
  import { nativeStyleArraysAndAutoMigration } from "../migrations/20261016201727_native_style_arrays_and_auto.js";
25
+ import { buildExcludeMigration } from "../migrations/20261019165323_build_exclude.js";
25
26
  import { CanvasRole } from "../entities/system/CanvasRole.js";
26
27
  import { GlobalStyle } from "../entities/system/GlobalStyle.js";
27
28
  import { Guidance } from "../entities/system/Guidance.js";
@@ -175,5 +176,6 @@ if (!registeredSchemaMigrations().has(20261016135620)) registerSchemaMigrations(
175
176
  if (!registeredSchemaMigrations().has(20261015202345)) registerSchemaMigrations(componentElementSwapMigration);
176
177
  if (!registeredSchemaMigrations().has(20261016164316)) registerSchemaMigrations(motionCompatibilityMigration);
177
178
  if (!registeredSchemaMigrations().has(20261016201727)) registerSchemaMigrations(nativeStyleArraysAndAutoMigration);
179
+ if (!registeredSchemaMigrations().has(20261019165323)) registerSchemaMigrations(buildExcludeMigration);
178
180
  //#endregion
179
181
  export { System };
@@ -24,7 +24,7 @@ declare const NativeSettings: EntityClass<z.ZodObject<Omit<{
24
24
  }, z.core.$strict>>;
25
25
  build: z.ZodOptional<z.ZodObject<{
26
26
  inputDir: z.ZodOptional<z.ZodString>;
27
- include: z.ZodOptional<z.ZodArray<z.ZodString>>;
27
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
28
28
  scripts: z.ZodOptional<z.ZodArray<z.ZodString>>;
29
29
  outDir: z.ZodOptional<z.ZodString>;
30
30
  sourceAlias: z.ZodOptional<z.ZodString>;
@@ -17,7 +17,7 @@ declare const SystemSection: SystemSectionClass;
17
17
  * where linked systems are materialised. */
18
18
  declare const BuildSectionDefinition: SubEntityClass<z.ZodObject<{
19
19
  inputDir: z.ZodOptional<z.ZodString>;
20
- include: z.ZodOptional<z.ZodArray<z.ZodString>>;
20
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
21
  scripts: z.ZodOptional<z.ZodArray<z.ZodString>>;
22
22
  outDir: z.ZodOptional<z.ZodString>;
23
23
  sourceAlias: z.ZodOptional<z.ZodString>;
@@ -96,13 +96,20 @@ declare const PlaygroundSectionDefinition: SubEntityClass<z.ZodObject<{
96
96
  }, z.core.$strict>>;
97
97
  interface PlaygroundSectionClass extends Named<typeof PlaygroundSectionDefinition> {}
98
98
  declare const PlaygroundSection: PlaygroundSectionClass;
99
+ /**
100
+ * {@link derivedInputDir} over paths alone, for a build that has walked its source graph but not yet
101
+ * written the `file` rows the computed `inputDir` reads. `components` are the config-relative paths
102
+ * of the component files, and `reached` answers them with every local file they import, asked only
103
+ * when the convention's folder doesn't settle it.
104
+ */
105
+ declare function deriveInputDir(components: readonly string[], reached: () => readonly string[]): string;
99
106
  declare const SettingsDefinition: EntityClass<z.ZodObject<{
100
107
  system: z.ZodOptional<z.ZodObject<{
101
108
  id: z.ZodOptional<z.ZodString>;
102
109
  }, z.core.$strict>>;
103
110
  build: z.ZodOptional<z.ZodObject<{
104
111
  inputDir: z.ZodOptional<z.ZodString>;
105
- include: z.ZodOptional<z.ZodArray<z.ZodString>>;
112
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
106
113
  scripts: z.ZodOptional<z.ZodArray<z.ZodString>>;
107
114
  outDir: z.ZodOptional<z.ZodString>;
108
115
  sourceAlias: z.ZodOptional<z.ZodString>;
@@ -140,4 +147,4 @@ declare const SettingsDefinition: EntityClass<z.ZodObject<{
140
147
  interface SettingsClass extends Named<typeof SettingsDefinition> {}
141
148
  declare const Settings: SettingsClass;
142
149
  //#endregion
143
- export { BuildSection, BuildSectionClass, CSS_DEFAULTS, CssPrefix, CssSection, CssSectionClass, PLAYGROUND_DEFAULTS, PlaygroundSection, PlaygroundSectionClass, Settings, SettingsClass, SystemSection, SystemSectionClass, classPrefixOf, cssPrefixes, prefixed, varPrefixOf };
150
+ export { BuildSection, BuildSectionClass, CSS_DEFAULTS, CssPrefix, CssSection, CssSectionClass, PLAYGROUND_DEFAULTS, PlaygroundSection, PlaygroundSectionClass, Settings, SettingsClass, SystemSection, SystemSectionClass, classPrefixOf, cssPrefixes, deriveInputDir, prefixed, varPrefixOf };
@@ -223,12 +223,21 @@ const derivedInputDir = createSliceMemo({
223
223
  kinds: ["component", "file"],
224
224
  compute: (config) => {
225
225
  const components = localComponentFiles(config);
226
- if (components.length === 0) return "";
227
- const shared = sharedFolders(components.map(foldersOf));
228
- for (let end = shared.length; end >= 2; end -= 1) if (shared[end - 2] === "uds" && shared[end - 1] === "components") return shared.slice(0, end).join("/");
229
- return sharedFolders(reachedFiles(config, components).map(foldersOf)).join("/");
226
+ return deriveInputDir(components, () => reachedFiles(config, components));
230
227
  }
231
228
  });
229
+ /**
230
+ * {@link derivedInputDir} over paths alone, for a build that has walked its source graph but not yet
231
+ * written the `file` rows the computed `inputDir` reads. `components` are the config-relative paths
232
+ * of the component files, and `reached` answers them with every local file they import, asked only
233
+ * when the convention's folder doesn't settle it.
234
+ */
235
+ function deriveInputDir(components, reached) {
236
+ if (components.length === 0) return "";
237
+ const shared = sharedFolders(components.map(foldersOf));
238
+ for (let end = shared.length; end >= 2; end -= 1) if (shared[end - 2] === "uds" && shared[end - 1] === "components") return shared.slice(0, end).join("/");
239
+ return sharedFolders(reached().map(foldersOf)).join("/");
240
+ }
232
241
  const Settings = defineEntity({
233
242
  kind: "settings",
234
243
  label: "Settings",
@@ -257,4 +266,4 @@ const Settings = defineEntity({
257
266
  inputDir: ({ member, config }) => resolveInputDir(member.build) ?? derivedInputDir(config) }
258
267
  });
259
268
  //#endregion
260
- export { BuildSection, CSS_DEFAULTS, CssSection, PLAYGROUND_DEFAULTS, PlaygroundSection, Settings, SystemSection, classPrefixOf, cssPrefixes, prefixed, varPrefixOf };
269
+ export { BuildSection, CSS_DEFAULTS, CssSection, PLAYGROUND_DEFAULTS, PlaygroundSection, Settings, SystemSection, classPrefixOf, cssPrefixes, deriveInputDir, prefixed, varPrefixOf };
@@ -0,0 +1,66 @@
1
+ import { Config } from "../../framework/Config.js";
2
+
3
+ //#region src/entities/system/shipped-modules.d.ts
4
+ /**
5
+ * The modules a system ships beside its components: every source file under its `inputDir` that no
6
+ * component reaches, so an app imports it on its own (`#uds/components/toast-portal`) and a system
7
+ * that links this one emits it too (`@/uds/registry/<slug>/toast-portal`).
8
+ *
9
+ * One rule, read three ways. The owner's build emits these, the server serves them with a linked
10
+ * version, and a linking build emits them from the installed tree. Each reads it from the same
11
+ * sealed graph, so the three can't disagree about which files a system ships the way a hand-kept
12
+ * list did, where forgetting an entry failed only for a linked consumer.
13
+ */
14
+ /** A sealed file as the rule reads it: its config-relative path and the local files it imports. */
15
+ interface ShipGraphFile {
16
+ readonly path: string;
17
+ readonly imports: readonly string[];
18
+ }
19
+ /** The settings the rule reads, as config-relative paths. Structural, so a caller passes the
20
+ * computed `inputDir` and the `build` record of an artifact it never loaded. */
21
+ interface ShipRuleOptions {
22
+ /** The root the system's files are laid out beneath. Empty ships nothing: with no root there is
23
+ * nowhere to emit a module at. */
24
+ readonly inputDir: string;
25
+ /** `settings.build.scripts`: sealed and run as written, never emitted. */
26
+ readonly scripts?: readonly string[];
27
+ /** `settings.build.exclude`: paths or globs kept out of the default set. */
28
+ readonly exclude?: readonly string[];
29
+ /** `settings.playground.pagesDir`: the pages `uds dev --playground` serves, which are this repo's
30
+ * own and never ship, even when they sit under `inputDir`. */
31
+ readonly pagesDir?: string;
32
+ }
33
+ /**
34
+ * Whether `file` matches an `exclude` entry: a glob (`**`, `*`, `?`), or a plain path that names the
35
+ * file or a folder above it. Spelled out rather than taken from a glob package, because nothing in
36
+ * this package imports one and Studio runs the config in a browser.
37
+ */
38
+ declare function matchesExclude(file: string, patterns: readonly string[]): boolean;
39
+ /**
40
+ * Whether a file is in the default ship set by its path alone: a module under `inputDir` that isn't
41
+ * a test or story, a platform variant, a script, a playground page, or excluded. Whether it ships as a module of its
42
+ * own also depends on what imports it, which {@link shippedModuleRoots} decides.
43
+ */
44
+ declare function shipsByDefault(file: string, options: ShipRuleOptions): boolean;
45
+ /**
46
+ * The files that ship as modules of their own: in the default set, and imported by no other local
47
+ * file.
48
+ *
49
+ * What imports a file is what tells the three kinds apart. A component's helper is imported by the
50
+ * component, so it already ships as part of that module. A config-only file (`tokens.ts`,
51
+ * `composites.ts`) is imported by the config, and shipping it would fail the build, since those use
52
+ * `Token.ref(...)` as values. A script's helper is imported by the script. A file nothing imports
53
+ * is there for an app to import, so it ships, with everything it imports. `components` names the
54
+ * component files, which are emitted as their components already.
55
+ */
56
+ declare function shippedModuleRoots(files: readonly ShipGraphFile[], options: ShipRuleOptions & {
57
+ readonly components?: readonly string[];
58
+ }): string[];
59
+ /**
60
+ * {@link shippedModuleRoots} over a built config: its `file` rows, each component's `file` ref, its
61
+ * computed `inputDir` and its build settings. What the server serves with a linked version, and what
62
+ * a linking build emits from the installed tree.
63
+ */
64
+ declare function shippedModules(config: Config): string[];
65
+ //#endregion
66
+ export { ShipGraphFile, ShipRuleOptions, matchesExclude, shippedModuleRoots, shippedModules, shipsByDefault };
@@ -0,0 +1,127 @@
1
+ import { splitRef } from "../../framework/utils/refs.js";
2
+ import { Settings } from "./Settings.js";
3
+ import { SCRIPT_EXTENSIONS } from "./File.js";
4
+ //#region src/entities/system/shipped-modules.ts
5
+ /** A file-name part that marks a test or story (`button.test.tsx`), which no app imports. */
6
+ const NOT_SHIPPED_MARKERS = new Set([
7
+ "test",
8
+ "spec",
9
+ "e2e",
10
+ "stories"
11
+ ]);
12
+ /** Folders of tests and their fixtures. */
13
+ const NOT_SHIPPED_FOLDERS = new Set(["__tests__", "__fixtures__"]);
14
+ /** A Metro platform suffix (`toast.ios.tsx`): the variant travels with the file it varies rather
15
+ * than as a module of its own. */
16
+ const PLATFORM_SUFFIXES = new Set([
17
+ "ios",
18
+ "android",
19
+ "native",
20
+ "web"
21
+ ]);
22
+ /**
23
+ * Whether a path's own name rules it out: a test or story, a type declaration, a platform variant,
24
+ * or a file in a test or fixture folder. Read off the path's segments rather than matched with
25
+ * patterns, since every path comes from a repo's tree and an unanchored pattern backtracks across a
26
+ * name with many dots.
27
+ */
28
+ function notShippedByName(file) {
29
+ const segments = file.split("/");
30
+ if (segments.slice(0, -1).some((folder) => NOT_SHIPPED_FOLDERS.has(folder))) return true;
31
+ const parts = (segments[segments.length - 1] ?? "").split(".");
32
+ const extension = parts[parts.length - 1] ?? "";
33
+ const middle = parts.slice(1, -1);
34
+ return middle.some((part) => NOT_SHIPPED_MARKERS.has(part)) || middle[middle.length - 1] === "d" && /^[mc]?ts$/.test(extension) || middle.length > 0 && PLATFORM_SUFFIXES.has(middle[middle.length - 1] ?? "");
35
+ }
36
+ /** Forward slashes, no leading `./`, no trailing slash. Scanned rather than matched with `/\/+$/`,
37
+ * which backtracks quadratically on a run of slashes (see `registry-dir.ts`). */
38
+ const normalize = (value) => {
39
+ const posix = value.replaceAll("\\", "/");
40
+ const start = posix.startsWith("./") ? 2 : 0;
41
+ let end = posix.length;
42
+ while (end > start && posix[end - 1] === "/") end--;
43
+ return posix.slice(start, end);
44
+ };
45
+ const escapeRegExp = (value) => value.replace(/[.+^${}()|[\]\\]/g, "\\$&");
46
+ /**
47
+ * Whether `file` matches an `exclude` entry: a glob (`**`, `*`, `?`), or a plain path that names the
48
+ * file or a folder above it. Spelled out rather than taken from a glob package, because nothing in
49
+ * this package imports one and Studio runs the config in a browser.
50
+ */
51
+ function matchesExclude(file, patterns) {
52
+ const target = normalize(file);
53
+ return patterns.some((raw) => {
54
+ const pattern = normalize(raw);
55
+ if (!/[*?]/.test(pattern)) return target === pattern || target.startsWith(`${pattern}/`);
56
+ const source = pattern.split(/(\*\*\/|\*\*|\*|\?)/).map((part) => {
57
+ if (part === "**/") return "(?:.*/)?";
58
+ if (part === "**") return ".*";
59
+ if (part === "*") return "[^/]*";
60
+ if (part === "?") return "[^/]";
61
+ return escapeRegExp(part);
62
+ }).join("");
63
+ return new RegExp(`^${source}$`).test(target);
64
+ });
65
+ }
66
+ /**
67
+ * Whether a file is in the default ship set by its path alone: a module under `inputDir` that isn't
68
+ * a test or story, a platform variant, a script, a playground page, or excluded. Whether it ships as a module of its
69
+ * own also depends on what imports it, which {@link shippedModuleRoots} decides.
70
+ */
71
+ function shipsByDefault(file, options) {
72
+ const inputDir = normalize(options.inputDir);
73
+ const target = normalize(file);
74
+ if (!inputDir || !target.startsWith(`${inputDir}/`)) return false;
75
+ const dot = target.lastIndexOf(".");
76
+ if (dot < 0 || !SCRIPT_EXTENSIONS.has(target.slice(dot))) return false;
77
+ return !(notShippedByName(target) || (options.scripts ?? []).some((script) => normalize(script) === target) || matchesExclude(target, [...options.pagesDir ? [options.pagesDir] : [], ...options.exclude ?? []]));
78
+ }
79
+ /**
80
+ * The files that ship as modules of their own: in the default set, and imported by no other local
81
+ * file.
82
+ *
83
+ * What imports a file is what tells the three kinds apart. A component's helper is imported by the
84
+ * component, so it already ships as part of that module. A config-only file (`tokens.ts`,
85
+ * `composites.ts`) is imported by the config, and shipping it would fail the build, since those use
86
+ * `Token.ref(...)` as values. A script's helper is imported by the script. A file nothing imports
87
+ * is there for an app to import, so it ships, with everything it imports. `components` names the
88
+ * component files, which are emitted as their components already.
89
+ */
90
+ function shippedModuleRoots(files, options) {
91
+ const imported = new Set(files.flatMap((file) => file.imports.map(normalize)));
92
+ const components = new Set((options.components ?? []).map(normalize));
93
+ return files.map((file) => normalize(file.path)).filter((file) => !imported.has(file) && !components.has(file) && shipsByDefault(file, options)).sort();
94
+ }
95
+ /**
96
+ * {@link shippedModuleRoots} over a built config: its `file` rows, each component's `file` ref, its
97
+ * computed `inputDir` and its build settings. What the server serves with a linked version, and what
98
+ * a linking build emits from the installed tree.
99
+ */
100
+ function shippedModules(config) {
101
+ const settings = Settings.get(config);
102
+ const local = (ref) => {
103
+ if (!ref) return void 0;
104
+ const target = splitRef(ref);
105
+ return target.kind === "file" && !target.source ? target.path : void 0;
106
+ };
107
+ const files = config.list("file").map((item) => {
108
+ const { imports } = item.toJSON();
109
+ return {
110
+ path: item.path,
111
+ imports: Object.values(imports ?? {}).flatMap((edge) => local(edge.target?.__ref) ?? [])
112
+ };
113
+ });
114
+ const components = config.list("component").flatMap((component) => {
115
+ const { file } = component.toJSON();
116
+ return local(file?.__ref) ?? [];
117
+ });
118
+ return shippedModuleRoots(files, {
119
+ inputDir: settings?.inputDir ?? "",
120
+ scripts: settings?.build?.scripts,
121
+ exclude: settings?.build?.exclude,
122
+ pagesDir: settings?.playground?.pagesDir,
123
+ components
124
+ });
125
+ }
126
+ //#endregion
127
+ export { matchesExclude, shippedModuleRoots, shippedModules, shipsByDefault };
@@ -73,7 +73,7 @@ interface RecordedSource {
73
73
  * The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
74
74
  * that replaced it (see `detectedWireVersion`).
75
75
  */
76
- declare const SERIALIZED_CONFIG_VERSION = 20261016201727;
76
+ declare const SERIALIZED_CONFIG_VERSION = 20261019165323;
77
77
  interface SerializedConfig {
78
78
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
79
79
  readonly version: number;
@@ -6,7 +6,7 @@ import { applyPathDelta, isPlainObject, namedFields, pathSegments, sectionDelta,
6
6
  import { METADATA_KEYS, declaredField } from "./utils/field-schema.js";
7
7
  import { dataSchema, resolveInputSchema } from "./schemas.js";
8
8
  import { isComposedValue } from "../entities/system/composed-values.js";
9
- import { borrowedSlug, classifyOverlay } from "./source-integrity.js";
9
+ import { borrowedSlug, classifyOverlay, uniteEntries } from "./source-integrity.js";
10
10
  import { kindBorrowable, titleCase } from "./registry.js";
11
11
  import { isGroupBody } from "./utils/group.js";
12
12
  import { authoredBag, pinBodyStyleNumbers, stylePropNumberUnit } from "./projections.js";
@@ -5862,7 +5862,7 @@ var Config = class Config {
5862
5862
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5863
5863
  hydrateFrom(stored, options) {
5864
5864
  const claimed = schemaVersionOf(stored);
5865
- if (claimed > 20261016201727) throw new SchemaVersionTooNew(claimed);
5865
+ if (claimed > 20261019165323) throw new SchemaVersionTooNew(claimed);
5866
5866
  const wire = upgradeSerializedConfig(stored);
5867
5867
  if (!Array.isArray(wire.ownedKinds) || typeof wire.items !== "object" || wire.items === null) throw new ConfigFormatError(`Config "${stored.name ?? "system-config"}" cannot be hydrated: this is not a serialized config-v2 config (no "ownedKinds"/"items"). A pre-cutover build artifact converts through the registered cutover migration — import the config type's module (e.g. \`configs/system\`) before hydrating.`);
5868
5868
  const leftover = wire.options;
@@ -6095,19 +6095,6 @@ function layerOverlay({ onto, patch, theirs, union }) {
6095
6095
  }
6096
6096
  return merged;
6097
6097
  }
6098
- /** A union that keeps the existing order and drops a repeat. Entries are refs and small literals, so
6099
- * identity is what they serialize to rather than reference equality. */
6100
- function uniteEntries(existing, added) {
6101
- const out = Array.isArray(existing) ? [...existing] : [];
6102
- const seen = new Set(out.map((entry) => JSON.stringify(entry)));
6103
- for (const entry of added) {
6104
- const key = JSON.stringify(entry);
6105
- if (seen.has(key)) continue;
6106
- seen.add(key);
6107
- out.push(entry);
6108
- }
6109
- return out;
6110
- }
6111
6098
  function derivedItemChange(kind, path, base, extend = {}) {
6112
6099
  return {
6113
6100
  kind,
@@ -15,7 +15,7 @@ import { Patch, SerializedConfig } from "./Config.js";
15
15
  * sets this constant to its own older timestamp; the merge ref then carries a registered migration
16
16
  * newer than the constant, and the assertion fails until the author re-mints.
17
17
  */
18
- declare const CURRENT_SCHEMA_VERSION = 20261016201727;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261019165323;
19
19
  /** What the walkers need to see of an envelope — loose on purpose, so a typed `SerializedConfig`
20
20
  * and a raw old-shape record both flow in without casts. */
21
21
  interface VersionStamped {
@@ -13,7 +13,7 @@ import { ConfigFormatError } from "./rejection.js";
13
13
  * sets this constant to its own older timestamp; the merge ref then carries a registered migration
14
14
  * newer than the constant, and the assertion fails until the author re-mints.
15
15
  */
16
- const CURRENT_SCHEMA_VERSION = 20261016201727;
16
+ const CURRENT_SCHEMA_VERSION = 20261019165323;
17
17
  /** The mint that moved `options` into the `settings` kind — restated here, rather than imported
18
18
  * from the migration that owns it, because that migration imports this module. */
19
19
  const SETTINGS_AS_KIND_VERSION = 20260908171448;
@@ -45,7 +45,7 @@ function registerSchemaMigrations(...migrations) {
45
45
  for (const migration of migrations) {
46
46
  if (migration.version !== 1 && !isMintTimestamp(migration.version)) throw new Error(`Schema migration version ${migration.version} is not a mint timestamp — mint one with \`date +%Y%m%d%H%M%S\`, like a supabase migration.`);
47
47
  if (REGISTERED.has(migration.version)) throw new Error(`A schema migration at version ${migration.version} is already registered.`);
48
- if (migration.version > 20261016201727) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
48
+ if (migration.version > 20261019165323) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
49
49
  REGISTERED.set(migration.version, migration);
50
50
  }
51
51
  }
@@ -102,7 +102,7 @@ function pendingFor(version, target, options) {
102
102
  * Already-current input passes through untouched — same reference, so hot paths pay nothing.
103
103
  */
104
104
  function upgradeSerializedConfig(json, options) {
105
- const target = options?.target ?? 20261016201727;
105
+ const target = options?.target ?? 20261019165323;
106
106
  const version = detectedWireVersion(json);
107
107
  if (version > target) throw new SchemaVersionTooNew(version, target);
108
108
  const raw = json;
@@ -160,7 +160,7 @@ function upgradedSources(json, options) {
160
160
  * meaning under the current schema and replay skips it — reported by the caller, not swallowed here.
161
161
  */
162
162
  function upgradePatch(patch, writtenAt, options) {
163
- const target = options?.target ?? 20261016201727;
163
+ const target = options?.target ?? 20261019165323;
164
164
  if (writtenAt > target) throw new SchemaVersionTooNew(writtenAt, target);
165
165
  let current = patch;
166
166
  for (const migration of pendingFor(writtenAt, target, options)) {
@@ -179,7 +179,7 @@ function upgradePatch(patch, writtenAt, options) {
179
179
  * stamp would cause.
180
180
  */
181
181
  function upgradeDraftEntries(draft, options) {
182
- const target = options?.target ?? 20261016201727;
182
+ const target = options?.target ?? 20261019165323;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];
@@ -117,6 +117,9 @@ interface MisdeclaredOverlay {
117
117
  declared: 'override' | 'extend';
118
118
  /** The field it was wrong about, addressed from the entity body as segments. */
119
119
  field: readonly string[];
120
+ /** For an `extend` of a list, the entries the list it lays onto already holds. The entries are what
121
+ * the extension is judged on, so a list the source defines is not wrong to extend. */
122
+ entries?: readonly unknown[];
120
123
  }
121
124
  /**
122
125
  * A leaf address: dotted, or spelled as segments where a member key can itself contain a dot (a
@@ -136,6 +139,9 @@ type OverlayVerb = 'override' | 'extend';
136
139
  * nothing local sits there or the two agree. A leaf carried along unchanged is not overlaid: a sub-op
137
140
  * copies the whole record it touched into the delta, so a token given one local condition holds the
138
141
  * source's other conditions beside it.
142
+ *
143
+ * A list is the one leaf with members. One that keeps all of theirs and adds to it is an `extend`,
144
+ * since that is what an `extend` of the added entries reads back as.
139
145
  */
140
146
  declare function leafVerb({
141
147
  ours,
@@ -162,6 +168,10 @@ declare function overlayVerb(overlay: LocalOverlay, field: LeafAddress): Overlay
162
168
  * Judged leaf by leaf, because one entity legitimately carries both — a token overridden under one
163
169
  * modifier and extended under another is one body with two verbs, and a whole-entity comparison would
164
170
  * have to call it one or the other.
171
+ *
172
+ * A list is judged by its entries: an `extend` of one the source defines adds to it, and is wrong only
173
+ * in the entries the list beneath already holds. That is the same moving source one level down — they
174
+ * added a value this config was extending with.
165
175
  */
166
176
  declare function misdeclaredOverlays(config: Config): MisdeclaredOverlay[];
167
177
  /**
@@ -243,17 +243,50 @@ function valueAt(body, field) {
243
243
  function isRuleCondition(address) {
244
244
  return address.length >= 3 && address[0] === "styles" && address[2] === "when";
245
245
  }
246
+ /** A list entry's identity. Entries are refs and small literals, so it is what they serialize to
247
+ * rather than reference equality. */
248
+ const entryKey = (entry) => JSON.stringify(entry);
249
+ /**
250
+ * A union that keeps the existing order and drops a repeat — what an `extend` of a list lays onto the
251
+ * list beneath it.
252
+ */
253
+ function uniteEntries(existing, added) {
254
+ const out = Array.isArray(existing) ? [...existing] : [];
255
+ const seen = new Set(out.map(entryKey));
256
+ for (const entry of added) {
257
+ const key = entryKey(entry);
258
+ if (seen.has(key)) continue;
259
+ seen.add(key);
260
+ out.push(entry);
261
+ }
262
+ return out;
263
+ }
264
+ /**
265
+ * The entries `ours` adds to `theirs`, where uniting them onto `theirs` gives back `ours` exactly —
266
+ * their list whole and in order, with ours after it. `undefined` where it doesn't: a list that drops
267
+ * or reorders one of theirs replaces it, and so does anything that isn't a list on both sides.
268
+ */
269
+ function addedEntries(ours, theirs) {
270
+ if (!Array.isArray(ours) || !Array.isArray(theirs)) return void 0;
271
+ const listed = new Set(theirs.map(entryKey));
272
+ const added = ours.filter((entry) => !listed.has(entryKey(entry)));
273
+ return sameValue(uniteEntries(theirs, added), ours) ? added : void 0;
274
+ }
246
275
  /**
247
276
  * The verb one local leaf is against the source's value at the same address: `extend` where the
248
277
  * source has nothing there, `override` where it has something different, and `undefined` where
249
278
  * nothing local sits there or the two agree. A leaf carried along unchanged is not overlaid: a sub-op
250
279
  * copies the whole record it touched into the delta, so a token given one local condition holds the
251
280
  * source's other conditions beside it.
281
+ *
282
+ * A list is the one leaf with members. One that keeps all of theirs and adds to it is an `extend`,
283
+ * since that is what an `extend` of the added entries reads back as.
252
284
  */
253
285
  function leafVerb({ ours, theirs }) {
254
286
  if (ours === void 0) return void 0;
255
287
  if (theirs === void 0) return "extend";
256
- return sameValue(ours, theirs) ? void 0 : "override";
288
+ if (sameValue(ours, theirs)) return void 0;
289
+ return addedEntries(ours, theirs)?.length ? "extend" : "override";
257
290
  }
258
291
  /**
259
292
  * Which verb this config's value at `field` is, in the pin's vocabulary — `override` where the source
@@ -286,6 +319,10 @@ function sameValue(a, b) {
286
319
  * Judged leaf by leaf, because one entity legitimately carries both — a token overridden under one
287
320
  * modifier and extended under another is one body with two verbs, and a whole-entity comparison would
288
321
  * have to call it one or the other.
322
+ *
323
+ * A list is judged by its entries: an `extend` of one the source defines adds to it, and is wrong only
324
+ * in the entries the list beneath already holds. That is the same moving source one level down — they
325
+ * added a value this config was extending with.
289
326
  */
290
327
  function misdeclaredOverlays(config) {
291
328
  const found = [];
@@ -299,7 +336,29 @@ function misdeclaredOverlays(config) {
299
336
  const theirs = source.resolve(kind, path)?.toJSON();
300
337
  for (const field of leafAddresses(overlay.extend)) {
301
338
  if (isRuleCondition(field)) continue;
302
- if (valueAt(theirs, field) !== void 0) found.push({
339
+ const defined = valueAt(theirs, field);
340
+ if (defined === void 0) continue;
341
+ const added = valueAt(overlay.extend, field);
342
+ if (Array.isArray(added) && Array.isArray(defined)) {
343
+ const replaced = valueAt(overlay.override, field);
344
+ const beneath = Array.isArray(replaced) ? replaced : valueAt(config.copyOf({
345
+ slug,
346
+ kind,
347
+ path
348
+ }), field);
349
+ const listed = new Set((Array.isArray(beneath) ? beneath : defined).map(entryKey));
350
+ const entries = added.filter((entry) => listed.has(entryKey(entry)));
351
+ if (entries.length > 0) found.push({
352
+ slug,
353
+ kind,
354
+ path,
355
+ declared: "extend",
356
+ field,
357
+ entries
358
+ });
359
+ continue;
360
+ }
361
+ found.push({
303
362
  slug,
304
363
  kind,
305
364
  path,
@@ -336,12 +395,16 @@ function classifyOverlay(local, theirs) {
336
395
  for (const [address, value] of leafEntries(local)) {
337
396
  if (address.length === 0) continue;
338
397
  if (isRuleCondition(address)) continue;
398
+ const defined = valueAt(theirs, address);
339
399
  const verb = leafVerb({
340
400
  ours: value,
341
- theirs: valueAt(theirs, address)
401
+ theirs: defined
342
402
  });
343
403
  if (verb === "override") override = setAtPath(override, address, value);
344
- else if (verb === "extend") extend = setAtPath(extend, address, value);
404
+ else if (verb === "extend") {
405
+ const added = addedEntries(value, defined) ?? value;
406
+ extend = setAtPath(extend, address, added);
407
+ }
345
408
  }
346
409
  const rules = local.styles;
347
410
  if (isWalkable(rules)) for (const [key, rule] of Object.entries(rules)) {
@@ -594,4 +657,4 @@ function borrowedSlug(body) {
594
657
  return isRef(body) ? sourceOf(body) : void 0;
595
658
  }
596
659
  //#endregion
597
- export { borrowedSlug, classifyOverlay, danglingDirectives, danglingSourcedRefs, excludedCondition, leafAddresses, leafVerb, misdeclaredOverlays, overlayVerb, redundantQualifiers, sourceSlugs, unadoptedDirectives, unreadableBorrows, unstatableConditions, unstatableOverrides, valueAt };
660
+ export { borrowedSlug, classifyOverlay, danglingDirectives, danglingSourcedRefs, excludedCondition, leafAddresses, leafVerb, misdeclaredOverlays, overlayVerb, redundantQualifiers, sourceSlugs, unadoptedDirectives, uniteEntries, unreadableBorrows, unstatableConditions, unstatableOverrides, valueAt };