@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.
- package/dist/configs/build-options.js +18 -19
- package/dist/configs/react-native-system.d.ts +1 -1
- package/dist/configs/system.js +2 -0
- package/dist/entities/native/NativeSettings.d.ts +1 -1
- package/dist/entities/system/Settings.d.ts +10 -3
- package/dist/entities/system/Settings.js +14 -5
- package/dist/entities/system/shipped-modules.d.ts +66 -0
- package/dist/entities/system/shipped-modules.js +127 -0
- package/dist/framework/Config.d.ts +1 -1
- package/dist/framework/Config.js +2 -15
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/framework/source-integrity.d.ts +10 -0
- package/dist/framework/source-integrity.js +68 -5
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/migrations/20261019165323_build_exclude.d.ts +17 -0
- package/dist/migrations/20261019165323_build_exclude.js +81 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
64
|
-
*
|
|
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
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* and
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
-
|
|
77
|
-
title: "
|
|
78
|
-
description: "
|
|
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,
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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
|
|
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
|
-
|
|
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>;
|
package/dist/configs/system.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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;
|
package/dist/framework/Config.js
CHANGED
|
@@ -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 >
|
|
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 =
|
|
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 =
|
|
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 >
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
401
|
+
theirs: defined
|
|
342
402
|
});
|
|
343
403
|
if (verb === "override") override = setAtPath(override, address, value);
|
|
344
|
-
else if (verb === "extend")
|
|
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 };
|