@valancex/cli 0.14.0 → 0.15.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/build.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // `vlx build`: the project → a static site in `dist/web/`. The stages, in order: configuration, content, templates, the browser bundle, then one document per page (and a 404 page, a sitemap
2
2
  // when the site has a published address). Everything is generated under `.valance/` first; `dist/` holds only what is built.
3
- import { build as bundle } from "vite";
3
+ import { build as bundle, defaultServerConditions } from "vite";
4
+ import { CONDITION } from "./libraries.js";
4
5
  import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
6
  import { createRequire } from "node:module";
6
7
  import { dirname, join } from "node:path";
@@ -37,7 +38,9 @@ const buildDesktop = async (root) => {
37
38
  logLevel: "warn",
38
39
  plugins: [templates(root)],
39
40
  cacheDir: join(root, GENERATED_DIR, "cache"),
40
- ssr: { noExternal: true, target: "node" },
41
+ // the desktop engine is one file: everything is bundled, and a library resolves by its `valance` condition (K4)
42
+ resolve: { conditions: [CONDITION, ...defaultServerConditions] },
43
+ ssr: { noExternal: true, target: "node", resolve: { conditions: [CONDITION, ...defaultServerConditions] } },
41
44
  esbuild: { keepNames: true },
42
45
  build: {
43
46
  ssr: join(root, GENERATED_DIR, "desktop.ts"),
package/dist/check.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AppError } from "./diagnostics.js";
2
2
  import { loadAuthor } from "./author.js";
3
3
  import { loadConfig } from "./config.js";
4
+ import { librariesOf } from "./libraries.js";
4
5
  import { generateContent, generateEntries, generateProgram } from "./emit.js";
5
6
  import { createLoader } from "./vite.js";
6
7
  const collect = async (into, work) => {
@@ -22,6 +23,8 @@ export const check = async (root) => {
22
23
  if (config === undefined) {
23
24
  return { diagnostics: problems, pages: 0, components: [] };
24
25
  }
26
+ // a library whose marker is wrong is reported first: when the application cannot even be loaded, it is the likely reason (a package the build does not process)
27
+ problems.push(...librariesOf(root).problems);
25
28
  generateEntries(root, config);
26
29
  const content = await collect(problems, () => generateContent(root, config));
27
30
  const server = await createLoader(root, config);
@@ -29,7 +32,9 @@ export const check = async (root) => {
29
32
  const author = await collect(problems, () => loadAuthor(server, root));
30
33
  const failedAuthor = problems.some((each) => each.file === "src/main.ts");
31
34
  const components = failedAuthor ? undefined : await collect(problems, () => generateProgram(root, author, config));
32
- return { diagnostics: problems, pages: content?.pages.length ?? 0, components: components ?? [] };
35
+ // (the templates stage reports the same marker problems; once is enough)
36
+ const once = problems.filter((each, index) => problems.findIndex((other) => other.file === each.file && other.code === each.code && other.message === each.message) === index);
37
+ return { diagnostics: once, pages: content?.pages.length ?? 0, components: components ?? [] };
33
38
  }
34
39
  finally {
35
40
  await server.close();
package/dist/cli.js CHANGED
@@ -24,7 +24,8 @@ import { localCli } from "./delegate.js";
24
24
  import { dev } from "./dev.js";
25
25
  import { defaultRecording, replayFile } from "./replay.js";
26
26
  import { AppError, format } from "./diagnostics.js";
27
- import { detectPackageManager, directoryFor, directoryProblem, execute, nameProblem, runCommand, titleOf } from "./init.js";
27
+ import { detectPackageManager, directoryFor, directoryProblem, execute, install as installDependencies, nameProblem, PACKAGE_MANAGERS, runCommand, titleOf } from "./init.js";
28
+ import { buildLibrary, checkLibrary, scaffoldLibrary } from "./library.js";
28
29
  import { DEFAULT_TARGETS, TARGETS } from "./targets.js";
29
30
  import { canAsk, canDraw } from "./tty.js";
30
31
  const HELP = `vlx <command>
@@ -35,6 +36,8 @@ const HELP = `vlx <command>
35
36
  build write the site to dist/web/
36
37
  check [--json] find mistakes in your files without building
37
38
  theme check [--json] check the theme: tokens, recipes, contrast, and what each target applies
39
+ init library <name> make a component library: a workspace with the package and a playground
40
+ library build|check build a library's dist/ (JavaScript, declarations, templates, styles); check the package
38
41
  generate page|component <name> add a page, or a component (alias: g)
39
42
  generate target web|desktop add a target to the project
40
43
  package --desktop the desktop target as one executable beside its window (unsigned)
@@ -57,8 +60,32 @@ const GENERATE_HELP = `vlx generate <page|component|target> <name> (alias: vlx
57
60
 
58
61
  Nothing is overwritten: a name that is taken is refused.
59
62
  `;
63
+ const LIBRARY_HELP = `vlx library <build|check> [--json] [--app <dir>]
64
+
65
+ Run in a library's package directory (the one with the "valance" export condition; \`vlx init library\` makes one).
66
+
67
+ vlx library build dist/: the TypeScript compiled to JavaScript with declarations (the components' contracts are the
68
+ types component() gives them), and every .mprx and .css under src/ copied beside it. Templates ship
69
+ as source: the application's build compiles them. Needs typescript installed in the library.
70
+ vlx library check the package as a consumer's build needs it: the "valance" condition on exports["."] and the file it
71
+ names, no other entry a tool without the condition would load, a peer dependency on
72
+ @valancex/common, stylesheets that exist and "sideEffects" that keeps them; then the playground
73
+ (--app <dir>, default ../../playground, ./playground or ../playground), an application that depends
74
+ on this package: it must build every target the package declares and write every template the
75
+ package ships, and its own \`vlx check\` problems are reported with the library's.
76
+
77
+ Exits 0 when there is nothing to say, 1 when there is. --json prints the diagnostics.
78
+ `;
79
+ const INIT_LIBRARY_HELP = `vlx init library <name> [--pm <npm|pnpm|yarn|bun>] [--no-install] [--cli <spec>]
80
+
81
+ Make a component library: a workspace with packages/<name> (a sample component, written with the portable core, its contract,
82
+ a tsconfig and a package.json that is marked as a library) and playground/ (an application that depends on it and builds
83
+ the web and the desktop). <name> is the package name (acme-ui, @acme/ui); the directory is the name's last part.
84
+ Then: cd <dir>, install, \`vlx library build\` in packages/<name>, \`vlx library check\`, and the playground's \`vlx dev\`.
85
+ `;
60
86
  const COMMAND_HELP = {
61
- init: INIT_HELP,
87
+ init: `${INIT_HELP}\n${INIT_LIBRARY_HELP}`,
88
+ library: LIBRARY_HELP,
62
89
  generate: GENERATE_HELP,
63
90
  g: GENERATE_HELP,
64
91
  replay: "vlx replay <log> [--route changes,update,render] [--json] [--allow-user-data]\n\nPlay a session log (written by vlx dev --record, or one of MESH's conformance logs) against this project's templates by the three routes of the equivalence law, and find the first line where one differs from the log: a tree, an event's outcome, a program or manifest that is not the one recorded. It runs no command and reads no clock. Exits 0 when every route gave what the log recorded, 1 when one did not, 2 when the log cannot be read. A log that holds user data (every recording does: its header says containsUserData) is refused unless you pass --allow-user-data: a divergence prints what was typed and shown.\n",
@@ -142,6 +169,34 @@ const runInit = async (args, cwd) => {
142
169
  console.log(`vlx init: next: cd ${answers.dir}${outcome.installed ? "" : ` && ${answers.packageManager} install`} && ${runCommand(answers.packageManager)} dev`);
143
170
  return 0;
144
171
  };
172
+ const runInitLibrary = async (args, cwd) => {
173
+ const positional = args.filter((each, index) => !each.startsWith("-") && !["--pm", "--cli"].includes(args[index - 1] ?? ""));
174
+ const name = positional[0];
175
+ const manager = option(args, "--pm");
176
+ if (name === undefined || !/^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name)) {
177
+ console.error(`vlx init library: name the package: vlx init library acme-ui (lowercase letters, digits, - . _; or @scope/name)\n\n${INIT_LIBRARY_HELP}`);
178
+ return 2;
179
+ }
180
+ if (manager !== undefined && !PACKAGE_MANAGERS.includes(manager)) {
181
+ console.error(`vlx init library: --pm is one of ${PACKAGE_MANAGERS.join(", ")}`);
182
+ return 2;
183
+ }
184
+ const dir = directoryFor(name.replace(/^@[^/]+\//, ""), cwd);
185
+ const problem = directoryProblem(dir);
186
+ if (problem !== undefined) {
187
+ console.error(`vlx init library: ${problem}`);
188
+ return 1;
189
+ }
190
+ const answers = { name, dir, packageManager: manager ?? detectPackageManager(), ...(option(args, "--cli") === undefined ? {} : { cli: option(args, "--cli") }) };
191
+ const files = scaffoldLibrary(answers);
192
+ console.log(`vlx init library: created ${name} in ${dir} (${files.length} files)`);
193
+ if (!args.includes("--no-install")) {
194
+ const ran = await installDependencies(answers);
195
+ console.log(ran.ok ? "vlx init library: installed" : `vlx init library: the dependencies were not installed (${answers.packageManager} install failed); run it yourself in ${dir}`);
196
+ }
197
+ console.log(`vlx init library: next: cd ${dir}/packages/${name.replace(/^@[^/]+\//, "")} && vlx library build && vlx library check`);
198
+ return 0;
199
+ };
145
200
  export const main = async (args, cwd = process.cwd()) => {
146
201
  const [command, ...rest] = args;
147
202
  // `vlx dev --help` says what dev does and runs nothing (`init` and `generate` answer for themselves below).
@@ -161,6 +216,7 @@ export const main = async (args, cwd = process.cwd()) => {
161
216
  command === "package" ||
162
217
  command === "check" ||
163
218
  command === "theme" ||
219
+ command === "library" ||
164
220
  command === "sync" ||
165
221
  command === "generate" ||
166
222
  command === "g") {
@@ -178,6 +234,9 @@ export const main = async (args, cwd = process.cwd()) => {
178
234
  console.log(INIT_HELP);
179
235
  return 0;
180
236
  }
237
+ if (rest[0] === "library") {
238
+ return await runInitLibrary(rest.slice(1), cwd);
239
+ }
181
240
  const parsed = parseInit(rest);
182
241
  if (typeof parsed === "string") {
183
242
  console.error(`vlx init: ${parsed}\n\n${INIT_HELP}`);
@@ -287,6 +346,34 @@ export const main = async (args, cwd = process.cwd()) => {
287
346
  }
288
347
  return result.diagnostics.length === 0 ? 0 : 1;
289
348
  }
349
+ case "library": {
350
+ const sub = rest[0];
351
+ if (sub !== "build" && sub !== "check") {
352
+ console.error("vlx library: say what: vlx library build | vlx library check [--json] [--app <dir>]");
353
+ return 2;
354
+ }
355
+ const dir = cwd;
356
+ const json = rest.includes("--json");
357
+ const given = option(rest, "--app");
358
+ const report = (diagnostics) => {
359
+ if (json) {
360
+ console.log(JSON.stringify({ diagnostics }, null, 2));
361
+ }
362
+ else {
363
+ diagnostics.forEach((each) => console.error(`${each.file}${each.line > 0 ? `:${each.line}` : ""} ${each.code}: ${each.message}`));
364
+ console.log(diagnostics.length === 0 ? `vlx library ${sub}: no problems` : `vlx library ${sub}: ${diagnostics.length} problem${diagnostics.length === 1 ? "" : "s"}`);
365
+ }
366
+ return diagnostics.length === 0 ? 0 : 1;
367
+ };
368
+ if (sub === "build") {
369
+ const built = buildLibrary(dir);
370
+ if (!json && built.diagnostics.length === 0) {
371
+ console.log(`vlx library build: dist/ written; ${built.copied.length} template and stylesheet file${built.copied.length === 1 ? "" : "s"} copied as source`);
372
+ }
373
+ return report(built.diagnostics);
374
+ }
375
+ return report(await checkLibrary(dir, given === undefined ? {} : { app: resolve(cwd, given) }));
376
+ }
290
377
  case "theme": {
291
378
  if (rest[0] !== "check") {
292
379
  console.error("vlx theme: say what: vlx theme check [--json]");
package/dist/compile.d.ts CHANGED
@@ -17,8 +17,16 @@ interface Template {
17
17
  readonly source: string;
18
18
  readonly builtIn: boolean;
19
19
  }
20
+ /** A library component left out of a target's program because its library does not support the target (K5): the tag is reported at each use, once, where the application can act. */
21
+ export interface Excluded {
22
+ readonly id: string;
23
+ readonly library: string;
24
+ readonly supports: ReadonlyArray<string>;
25
+ }
20
26
  export interface Templates {
21
27
  readonly all: ReadonlyArray<Template>;
28
+ /** The library components this target leaves out, by tag. */
29
+ readonly excluded: ReadonlyMap<string, Excluded>;
22
30
  /** The declared tree, in `src/main.ts`'s own words. */
23
31
  readonly tree: ReadonlyArray<Node>;
24
32
  }
package/dist/compile.js CHANGED
@@ -11,6 +11,7 @@ import { AppError, diagnostic } from "./diagnostics.js";
11
11
  import { declarationsOf, declaredOf, linked, nameOf, treeOf } from "./tree.js";
12
12
  import { tagsOf } from "./outline.js";
13
13
  import { entryOf } from "./contract.js";
14
+ import { idOf, libraryOf, librariesOf } from "./libraries.js";
14
15
  import { locateKey } from "./wiring.js";
15
16
  import { namesOf, webVocabulary, WHOLE } from "@valancex/common/internal";
16
17
  import { checkReferences, checkTemplate } from "./rules.js";
@@ -48,7 +49,26 @@ export const templatesOf = (root, author, vocabulary = webVocabulary, views = BU
48
49
  }
49
50
  const layout = author?.layout;
50
51
  const overrides = author?.override ?? [];
51
- const declared = treeOf(layout, overrides, author?.components ?? []);
52
+ const { libraries } = librariesOf(root);
53
+ const resolved = author?.resolve ?? {};
54
+ const overridden = new Set(overrides.map((each) => nameOf(each.template.mprx)));
55
+ const excluded = new Map();
56
+ const idFor = (component) => idOf(libraries, root, component.template.mprx, nameOf(component.template.mprx));
57
+ // The components of this target's program. A library's component is left out when its library does not support this target (K5: one diagnostic at the use, not several inside the library), when the
58
+ // application's own template of that name replaces it (`override`), and when `resolve` chose another of that name (K14).
59
+ const components = (author?.components ?? []).filter((each) => {
60
+ const name = nameOf(each.template.mprx);
61
+ const library = libraryOf(libraries, root, each.template.mprx);
62
+ if (library === undefined) {
63
+ return true;
64
+ }
65
+ if (!library.targets.includes(vocabulary.target)) {
66
+ excluded.set(name, { id: idFor(each), library: library.name, supports: library.targets });
67
+ return false;
68
+ }
69
+ return !overridden.has(name) && (resolved[name] === undefined || resolved[name] === idFor(each));
70
+ });
71
+ const declared = treeOf(layout, overrides, components);
52
72
  const declarations = declarationsOf(root);
53
73
  const problems = [];
54
74
  /** Where a template's component is defined: its `component(…)` call, else where it is imported, else the top of the entry file. */
@@ -69,25 +89,41 @@ export const templatesOf = (root, author, vocabulary = webVocabulary, views = BU
69
89
  const builtInNames = new Set(found.keys());
70
90
  for (const each of overrides) {
71
91
  const name = nameOf(each.template.mprx);
72
- if (!builtInNames.has(name) && name !== ROOT) {
73
- problem(at(each.template.mprx), "override-unknown", `<${name}> is not a built-in component, so there is nothing to override; register it in \`components\` instead`);
92
+ if (!builtInNames.has(name) &&
93
+ name !== ROOT &&
94
+ !(author?.components ?? []).some((other) => nameOf(other.template.mprx) === name && libraryOf(libraries, root, other.template.mprx) !== undefined)) {
95
+ problem(at(each.template.mprx), "override-unknown", `<${name}> is not a built-in component and no library component in \`components\` has that name, so there is nothing to override; register it in \`components\` instead`);
96
+ }
97
+ }
98
+ for (const [tag, id] of Object.entries(resolved)) {
99
+ if (!(author?.components ?? []).some((each) => idFor(each) === id)) {
100
+ const named = (author?.components ?? []).filter((each) => nameOf(each.template.mprx) === tag).map(idFor);
101
+ problem({ file: ENTRY_FILE, line: 1, column: 1 }, "resolve-unknown", `\`resolve\` says <${tag}> is ${id}, but no component in \`components\` has that id${named.length === 0 ? ` (nothing named ${tag} is listed)` : ` (listed with that name: ${named.join(", ")})`}`);
74
102
  }
75
103
  }
76
104
  // A name is one component. Two files of one name, from anywhere in the definition (two libraries', or a library's and the application's), are an error: the first listed must not win silently
77
105
  // (decision K14). A component that is not listed in `override` and is named like a built-in is the same collision, with the built-in.
78
- // TODO(K14): qualified ids (`acme-ui/disclosure`) will let a library's component be named and overridden without renaming a file; until then the way out of a collision is to rename one.
79
- const listed = declaredOf(layout, overrides, author?.components ?? []);
106
+ // A library's component is named by its qualified id (`acme-ui/disclosure`) in a diagnostic and in `resolve`; the application's own, by the bare name.
107
+ const listed = declaredOf(layout, overrides, components);
80
108
  const firstOf = new Map();
109
+ const idOfFile = (file, name) => idOf(libraries, root, file, name);
81
110
  for (const each of listed) {
82
111
  const first = firstOf.get(each.name);
112
+ const id = idOfFile(each.file, each.name);
83
113
  if (first === undefined) {
84
- firstOf.set(each.name, each.file);
85
- if (each.role === "component" && builtInNames.has(each.name) && !listed.some((other) => other.role !== "component" && other.name === each.name)) {
86
- problem(at(each.file), "template-builtin", `${each.file} is <${each.name}>, which is also a built-in component: list it in \`override\` to replace the built-in, or rename the file so both can be used`);
114
+ firstOf.set(each.name, { file: each.file, id });
115
+ if (each.role === "component" &&
116
+ builtInNames.has(each.name) &&
117
+ resolved[each.name] !== id &&
118
+ !listed.some((other) => other.role !== "component" && other.name === each.name)) {
119
+ problem(at(each.file), "template-builtin", `${id} is <${each.name}>, which is also a built-in component: list your own template of that name in \`override\` to replace the built-in, or choose this one with \`resolve: { ${each.name}: "${id}" }\``);
87
120
  }
88
121
  }
89
- else if (first !== each.file) {
90
- problem(at(each.file), "template-duplicate", `${each.file} is also <${each.name}>, like ${first}: a component is named by its file, so the name must be unique; rename one of the files`);
122
+ else if (first.file !== each.file) {
123
+ const libraryMade = id !== each.name || first.id !== each.name;
124
+ problem(at(each.file), "template-duplicate", libraryMade
125
+ ? `${id === each.name ? each.file : id} and ${first.id === each.name ? first.file : first.id} are both <${each.name}>: a component is named by its file, so the name must be unique; choose one with \`resolve: { ${each.name}: "${id === each.name ? first.id : id}" }\` in defineApp, or replace both with your own template of that name in \`override\``
126
+ : `${each.file} is also <${each.name}>, like ${first.file}: a component is named by its file, so the name must be unique; rename one of the files`);
91
127
  }
92
128
  }
93
129
  const files = new Map();
@@ -127,7 +163,7 @@ export const templatesOf = (root, author, vocabulary = webVocabulary, views = BU
127
163
  if (problems.length > 0) {
128
164
  throw new AppError(problems);
129
165
  }
130
- return { all: [...found.values()], tree };
166
+ return { all: [...found.values()], tree, excluded };
131
167
  };
132
168
  /** The templates the root reaches, following the tags each uses. A template nothing uses is not part of the program (the compiler refuses one). */
133
169
  const reachable = (all) => {
@@ -253,7 +289,7 @@ const renamedHint = (vocabulary, used, at) => {
253
289
  };
254
290
  /** Compiles the application's templates against a target's vocabulary (the web's by default). Fails with every diagnostic, each pointing into the file it is about. */
255
291
  export const compileViews = async (root, author, vocabulary = webVocabulary, views = BUILT_IN) => {
256
- const { all, tree } = templatesOf(root, author, vocabulary, views);
292
+ const { all, tree, excluded } = templatesOf(root, author, vocabulary, views);
257
293
  const used = reachable(all);
258
294
  const model = modelOf(root, author, tree, vocabulary);
259
295
  if (!used.some((template) => template.component === ROOT)) {
@@ -268,6 +304,18 @@ export const compileViews = async (root, author, vocabulary = webVocabulary, vie
268
304
  const found = [
269
305
  ...(result.components ?? []).flatMap((entry) => entry.diagnostics.diagnostics
270
306
  .filter((each) => each.severity === "error")
307
+ .map((each) => {
308
+ // a library component this target leaves out (K5): one sentence at the use, in place of the compiler's "unknown component"
309
+ const left = each.code === "unknown-component" ? excluded.get(/component "([^"]+)"/.exec(each.message)?.[1] ?? "") : undefined;
310
+ return left === undefined
311
+ ? each
312
+ : {
313
+ ...each,
314
+ code: "unsupported-target",
315
+ message: `<${each.message.match(/component "([^"]+)"/)[1]}> is ${left.id}, and ${left.library} supports ${left.supports.join(" and ")} only; this project builds for ${vocabulary.target}. Use a component that ${vocabulary.target} has, or build ${left.library} for ${vocabulary.target}`,
316
+ suggestions: [],
317
+ };
318
+ })
271
319
  .map((each) => diagnostic(each.path, each.span.start.line, each.code, each.message +
272
320
  (each.suggestions.length > 0 ? ` (did you mean ${each.suggestions.map((suggestion) => `"${suggestion.replacement}"`).join(" or ")}?)` : "") +
273
321
  renamedHint(vocabulary, used, each), each.span.start.column))),
package/dist/config.d.ts CHANGED
@@ -27,6 +27,8 @@ export interface Config {
27
27
  /** The size budgets a production build is held to (Tier 2): the project's own, or none, in which case the framework's start-up budgets apply (W14). Never checked by `vlx dev`. */
28
28
  readonly budgets: ReadonlyArray<Budget> | undefined;
29
29
  /** What `vlx dev --record` leaves out of a session log (Tier 2; R9, R14): nothing, unless the project says. */
30
+ /** The theme package the project uses instead of the default theme (Tier 2; K12): an installed package whose `valance` field is `{ "kind": "theme" }`. Absent: the `theme/` directory, else the default. */
31
+ readonly theme: string | undefined;
30
32
  readonly record: {
31
33
  readonly redact: {
32
34
  readonly scope: ReadonlyArray<ReadonlyArray<string>>;
package/dist/config.js CHANGED
@@ -66,6 +66,9 @@ const url = (value) => {
66
66
  const title = (value) => typeof value === "string" && value.includes("{page}") && value.length <= 120 && !/[{}]/.test(value.replaceAll("{page}", "").replaceAll("{site}", ""))
67
67
  ? undefined
68
68
  : 'must contain "{page}" and may use "{site}", for example "{page} · {site}" (at most 120 characters)';
69
+ const themePackage = (value) => typeof value === "string" && /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(value)
70
+ ? undefined
71
+ : 'must be the name of an installed theme package, such as "acme-theme" or "@acme/theme"';
69
72
  const flag = (value) => (typeof value === "boolean" ? undefined : "must be true or false");
70
73
  const api = (value) => {
71
74
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -128,7 +131,7 @@ export const loadConfig = (root) => {
128
131
  const desktop = read(root, DESKTOP_FILE);
129
132
  const problems = [
130
133
  ...(desktop === undefined ? [] : check(DESKTOP_FILE, desktop.text, desktop.value, { $schema: schema, version }, [])),
131
- ...check(PROJECT_FILE, project.text, project.value, { $schema: schema, version, name: text, title, record, budgets: budgetProblems }, ["name"]),
134
+ ...check(PROJECT_FILE, project.text, project.value, { $schema: schema, version, name: text, title, record, budgets: budgetProblems, theme: themePackage }, ["name"]),
132
135
  ...(web === undefined ? [] : check(WEB_FILE, web.text, web.value, { $schema: schema, version, base, url, headers: flag, announce: flag, api }, [])),
133
136
  ];
134
137
  if (problems.length > 0) {
@@ -143,6 +146,7 @@ export const loadConfig = (root) => {
143
146
  web: { base: given.base ?? "/", url: given.url ?? "", headers: given.headers ?? true, announce: given.announce ?? true },
144
147
  api: { base: given.api?.base ?? "", proxy: given.api?.proxy ?? "" },
145
148
  budgets: named.budgets,
149
+ theme: named.theme,
146
150
  record: { redact: { scope: named.record?.redact.scope ?? [], commands: named.record?.redact.commands ?? [] } },
147
151
  };
148
152
  };
package/dist/contract.js CHANGED
@@ -71,6 +71,13 @@ export const entryOf = (declared) => {
71
71
  }
72
72
  }
73
73
  const unit = declared.unit === undefined ? undefined : unitEntry(declared.unit, props, problems);
74
+ // A controllable field is read in the template by the name of its prop, and the unit supplies that name (the prop's value when the caller writes it, else the unit's own); listing the prop in the scope
75
+ // as well is `manifest-unit-clash` in MESH, for a field the docs say to declare this way (found building a tabs component outside the repository).
76
+ for (const [name, each] of Object.entries(unit?.state ?? {})) {
77
+ if (each.controllable === true) {
78
+ delete scope[name];
79
+ }
80
+ }
74
81
  if (declared.name !== undefined) {
75
82
  const named = props[declared.name];
76
83
  if (named === undefined && !problems.some((each) => each.name === declared.name)) {
package/dist/emit.js CHANGED
@@ -21,6 +21,7 @@ import { targetOf } from "./targets.js";
21
21
  import { forTarget } from "@valancex/common/internal";
22
22
  import { AppError } from "./diagnostics.js";
23
23
  import { ENTRY_FILE, ensureGeneratedDir, GENERATED_DIR, generated, writeIfChanged } from "./project.js";
24
+ import { librariesOf } from "./libraries.js";
24
25
  import { themeOf } from "./theme.js";
25
26
  import { buildWiring } from "./wiring.js";
26
27
  /** The layout of the portable core on the web: `column` and `row` are custom elements, and this is what they are (`@valancex/common`'s web vocabulary). It is the library's layer, the first of the
@@ -50,6 +51,18 @@ const CORE_CSS = `/* Generated by \`valance\`. The web's realization of the port
50
51
  `;
51
52
  /** The theme's stylesheet. A theme with a problem is reported by \`check\` (and refused by \`build\`); the page gets no theme meanwhile rather than a half of one. */
52
53
  const themeStylesheet = (root) => themeOf(root).css ?? "/* The theme has a problem: run `vlx check`. */\n";
54
+ /**
55
+ * The stylesheets the libraries declare (K6), in the library layer: below the theme and the application, so a theme or the application's own rule wins without a specificity fight. The web's only (a
56
+ * window is drawn from the theme's resolved values). Absent when no library declares one.
57
+ */
58
+ const libraryStylesheet = (root) => {
59
+ const parts = librariesOf(root).libraries.flatMap((library) => library.styles.map((file) => ({ library, file })));
60
+ return parts.length === 0
61
+ ? undefined
62
+ : `/* Generated by \`valance\`: the stylesheets libraries declare, in the library layer. */\n@layer vlx-library, vlx-theme, vlx-app;\n${parts
63
+ .map(({ library, file }) => `/* ${library.name}: ${relative(library.dir, file).split(sep).join("/")} */\n@layer vlx-library {\n${readFileSync(file, "utf8")}\n}\n`)
64
+ .join("")}`;
65
+ };
53
66
  /** The stylesheets the web loads: every one under `src/` except those in another target's directory (`src/desktop/`). */
54
67
  const stylesheets = (root) => existsSync(join(root, SOURCE_DIR))
55
68
  ? readdirSync(join(root, SOURCE_DIR), { recursive: true, encoding: "utf8" })
@@ -64,7 +77,7 @@ const clientSource = (root, config, record = false) => `// Generated by \`valanc
64
77
  import { start } from "@valancex/common/client";
65
78
  import content from "./content.json";
66
79
  import program from "./${programFile(config, "web")}";
67
- import "./theme.css";
80
+ ${libraryStylesheet(root) === undefined ? "" : 'import "./library.css";\n'}import "./theme.css";
68
81
  import "./core.css";
69
82
  ${hasEntry(root) ? `import author from "../${ENTRY_FILE}";\n` : "const author = undefined;\n"}${stylesheets(root)
70
83
  .map((file) => `import "../${file}";\n`)
@@ -128,6 +141,8 @@ export const generateProgram = async (root, author, config, only) => {
128
141
  problems.push(...error.diagnostics);
129
142
  }
130
143
  }
144
+ // a library whose marker is wrong is reported at its package.json, where its author can act (K5)
145
+ problems.push(...librariesOf(root).problems);
131
146
  // a theme the project writes is checked with the templates: a mistake in it is as much the project's as one in a layout
132
147
  problems.push(...(themeOf(root).custom ? themeOf(root).diagnostics : []));
133
148
  if (problems.length > 0) {
@@ -194,6 +209,7 @@ export const generateEntries = (root, config, options) => {
194
209
  ? [
195
210
  writeIfChanged(generated(root, "core.css"), CORE_CSS),
196
211
  writeIfChanged(generated(root, "theme.css"), themeStylesheet(root)),
212
+ writeIfChanged(generated(root, "library.css"), libraryStylesheet(root) ?? "/* No library declares a stylesheet. */\n"),
197
213
  writeIfChanged(generated(root, "client.ts"), clientSource(root, config, options?.record === true)),
198
214
  writeIfChanged(generated(root, "server.ts"), serverSource(root, config)),
199
215
  ]
@@ -0,0 +1,41 @@
1
+ import type { Diagnostic } from "./diagnostics.js";
2
+ import type { TargetId } from "./targets.js";
3
+ /** The export condition that marks a package as a VALANCE library. */
4
+ export declare const CONDITION = "valance";
5
+ export declare const KINDS: readonly ["components", "theme"];
6
+ export type LibraryKind = (typeof KINDS)[number];
7
+ export interface Library {
8
+ readonly name: string;
9
+ readonly version: string;
10
+ /** The package's real directory (a symlink resolved). */
11
+ readonly dir: string;
12
+ readonly kind: LibraryKind;
13
+ /** The targets whose vocabularies the library's templates are written for. Absent in the field: every target. */
14
+ readonly targets: ReadonlyArray<TargetId>;
15
+ /** The stylesheets the library declares, as absolute paths of files that exist. */
16
+ readonly styles: ReadonlyArray<string>;
17
+ /** A theme's `theme.json`, as an absolute path (kind `theme`). */
18
+ readonly theme?: string;
19
+ }
20
+ /** Whether an `exports` value has the `valance` condition anywhere in it. */
21
+ export declare const hasCondition: (exports: unknown) => boolean;
22
+ /** The directory `name` is installed in for a project at `root`, looked for as Node looks for it: `node_modules/<name>` here and in each parent directory. */
23
+ export declare const installedAt: (root: string, name: string) => string | undefined;
24
+ /** What reading a project's dependencies found: the libraries, and what is wrong with their markers. */
25
+ export interface Found {
26
+ readonly libraries: ReadonlyArray<Library>;
27
+ readonly problems: ReadonlyArray<Diagnostic>;
28
+ }
29
+ /**
30
+ * The libraries a project at `root` depends on: each dependency (of any kind) installed under `node_modules` whose `exports` has the `valance` condition. A package with the field and no condition is
31
+ * said so (the build keys on the condition, K11); a field that is wrong is a diagnostic about the package's `package.json`, where its author can act.
32
+ */
33
+ export declare const librariesOf: (root: string) => Found;
34
+ /** The library a file belongs to (a template or a stylesheet, by absolute or project-relative path), if any. */
35
+ export declare const libraryOf: (libraries: ReadonlyArray<Library>, root: string, file: string) => Library | undefined;
36
+ /** The qualified id of a component: `package/name` for a library's, the bare name for the application's and the built-ins'. */
37
+ export declare const idOf: (libraries: ReadonlyArray<Library>, root: string, file: string, name: string) => string;
38
+ /** What the build tells Vite about marked packages: process them (`noExternal`), and resolve the `valance` condition. */
39
+ export declare const viteSettings: (libraries: ReadonlyArray<Library>) => {
40
+ readonly names: ReadonlyArray<string>;
41
+ };
@@ -0,0 +1,169 @@
1
+ // Libraries of components and themes (Bet 12; decisions K3 to K14). A package is a VALANCE library when its `exports` has the condition `valance`: that condition is the one thing the build keys on
2
+ // (K11). The package's top-level `valance` field is metadata only: what it holds (`components` or `theme`), the targets it supports, the stylesheets it declares. Templates ship as source and the
3
+ // consumer's build compiles them (K3), so the build must process a marked package instead of loading it in Node, which does not know `.mprx` or `.css`.
4
+ //
5
+ // This module reads the project's dependencies and says which are libraries. It does not load them: Vite does, with the settings `viteSettings` returns.
6
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
7
+ import { dirname, join, resolve, sep } from "node:path";
8
+ import { diagnostic } from "./diagnostics.js";
9
+ /** The targets a library can name. (Not imported from `targets.ts`, which imports the compiler, which imports this.) */
10
+ const TARGET_IDS = ["web", "desktop"];
11
+ const isTarget = (name) => TARGET_IDS.includes(name);
12
+ /** The export condition that marks a package as a VALANCE library. */
13
+ export const CONDITION = "valance";
14
+ export const KINDS = ["components", "theme"];
15
+ const read = (file) => {
16
+ try {
17
+ return JSON.parse(readFileSync(file, "utf8"));
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ };
23
+ /** Whether an `exports` value has the `valance` condition anywhere in it. */
24
+ export const hasCondition = (exports) => typeof exports === "object" && exports !== null && !Array.isArray(exports) && Object.entries(exports).some(([key, value]) => key === CONDITION || hasCondition(value));
25
+ /** The directory `name` is installed in for a project at `root`, looked for as Node looks for it: `node_modules/<name>` here and in each parent directory. */
26
+ export const installedAt = (root, name) => {
27
+ for (let at = resolve(root);; at = dirname(at)) {
28
+ const candidate = join(at, "node_modules", ...name.split("/"));
29
+ if (existsSync(join(candidate, "package.json"))) {
30
+ return realpathSync(candidate);
31
+ }
32
+ if (dirname(at) === at) {
33
+ return undefined;
34
+ }
35
+ }
36
+ };
37
+ const NONE = { libraries: [], problems: [] };
38
+ /** The metadata a library's `valance` field holds, checked. `file` names the package.json in a diagnostic. */
39
+ const fieldOf = (manifest, dir, file) => {
40
+ const problems = [];
41
+ const problem = (code, message) => void problems.push(diagnostic(file, 0, code, message));
42
+ const field = manifest.valance;
43
+ const given = typeof field === "object" && field !== null && !Array.isArray(field) ? field : undefined;
44
+ if (field !== undefined && given === undefined) {
45
+ problem("library-field", `"valance" in ${file} must be an object ({ "kind": "components", "targets": ["web", "desktop"] })`);
46
+ }
47
+ for (const key of Object.keys(given ?? {})) {
48
+ if (!["kind", "targets", "styles", "theme"].includes(key)) {
49
+ problem("library-field", `"valance.${key}" in ${file} is not a field of a library (kind, targets, styles, theme)`);
50
+ }
51
+ }
52
+ const kind = given?.["kind"] ?? "components";
53
+ if (!KINDS.includes(kind)) {
54
+ problem("library-field", `"valance.kind" in ${file} is ${JSON.stringify(kind)}; it is one of ${KINDS.map((each) => `"${each}"`).join(", ")}`);
55
+ }
56
+ const listed = given?.["targets"];
57
+ const targets = [];
58
+ if (listed !== undefined && (!Array.isArray(listed) || listed.length === 0)) {
59
+ problem("library-field", `"valance.targets" in ${file} must be a non-empty list of targets`);
60
+ }
61
+ for (const each of Array.isArray(listed) ? listed : []) {
62
+ if (typeof each === "string" && isTarget(each)) {
63
+ targets.push(each);
64
+ }
65
+ else {
66
+ problem("library-field", `"valance.targets" in ${file} names ${JSON.stringify(each)}, which is not a target`);
67
+ }
68
+ }
69
+ const styles = [];
70
+ const listedStyles = Array.isArray(given?.["styles"]) ? given["styles"] : [];
71
+ for (const each of listedStyles) {
72
+ const path = typeof each === "string" ? resolve(dir, each) : undefined;
73
+ if (path === undefined || !path.startsWith(dir + sep)) {
74
+ problem("library-field", `"valance.styles" in ${file} lists ${JSON.stringify(each)}: a stylesheet inside the package, as "./dist/x.css"`);
75
+ }
76
+ else if (!existsSync(path)) {
77
+ problem("library-styles", `"valance.styles" in ${file} lists ${String(each)}, which the package does not contain (did the library build run?)`);
78
+ }
79
+ else {
80
+ styles.push(path);
81
+ }
82
+ }
83
+ let theme;
84
+ if (kind === "theme") {
85
+ const entry = given?.["theme"];
86
+ const path = typeof entry === "string" ? resolve(dir, entry) : undefined;
87
+ if (path === undefined || !path.startsWith(dir + sep)) {
88
+ problem("library-field", `a theme package says where its theme is: "valance": { "kind": "theme", "theme": "./theme/theme.json" } in ${file}`);
89
+ }
90
+ else if (!existsSync(path)) {
91
+ problem("library-field", `"valance.theme" in ${file} is ${String(entry)}, which the package does not contain`);
92
+ }
93
+ else {
94
+ theme = path;
95
+ }
96
+ }
97
+ return {
98
+ problems,
99
+ ...(problems.length > 0
100
+ ? {}
101
+ : {
102
+ library: {
103
+ name: manifest.name ?? "",
104
+ version: manifest.version ?? "0.0.0",
105
+ dir,
106
+ kind: kind,
107
+ targets: targets.length === 0 ? ["web", "desktop"] : targets,
108
+ styles,
109
+ ...(theme === undefined ? {} : { theme }),
110
+ },
111
+ }),
112
+ };
113
+ };
114
+ /**
115
+ * The libraries a project at `root` depends on: each dependency (of any kind) installed under `node_modules` whose `exports` has the `valance` condition. A package with the field and no condition is
116
+ * said so (the build keys on the condition, K11); a field that is wrong is a diagnostic about the package's `package.json`, where its author can act.
117
+ */
118
+ export const librariesOf = (root) => {
119
+ const own = read(join(root, "package.json"));
120
+ if (own === undefined) {
121
+ return NONE;
122
+ }
123
+ const names = [...new Set([...Object.keys(own.dependencies ?? {}), ...Object.keys(own.devDependencies ?? {}), ...Object.keys(own.peerDependencies ?? {})])].sort();
124
+ const libraries = [];
125
+ const problems = [];
126
+ for (const name of names) {
127
+ const dir = installedAt(root, name);
128
+ const manifest = dir === undefined ? undefined : read(join(dir, "package.json"));
129
+ if (dir === undefined || manifest === undefined) {
130
+ continue;
131
+ }
132
+ const marked = hasCondition(manifest.exports);
133
+ if (!marked) {
134
+ if (manifest.valance !== undefined && name !== "@valancex/valance") {
135
+ problems.push(diagnostic(`node_modules/${name}/package.json`, 0, "library-unmarked", `${name} has a "valance" field but no "valance" export condition, so the build does not process it: add ${JSON.stringify({ exports: { ".": { valance: "./dist/index.js" } } })} (the condition is what marks a library)`));
136
+ }
137
+ continue;
138
+ }
139
+ const checked = fieldOf({ ...manifest, name }, dir, `node_modules/${name}/package.json`);
140
+ problems.push(...checked.problems);
141
+ if (checked.library !== undefined) {
142
+ libraries.push(checked.library);
143
+ // A library resolves its peers (`@valancex/common`, `effect`) from where it really is. One linked from outside the project (`npm install ../acme-ui`) finds none there (K7).
144
+ const missing = Object.keys(manifest.peerDependencies ?? {}).filter((peer) => manifest.peerDependenciesMeta?.[peer]?.optional !== true && installedAt(dir, peer) === undefined);
145
+ if (missing.length > 0) {
146
+ problems.push(diagnostic(`node_modules/${name}/package.json`, 0, "library-peers", `${name} cannot find its peer ${missing.map((each) => `${each}`).join(", ")} from ${dir}. A library linked from outside the project (\`npm install ../${name}\`) resolves its peers from its own directory: install it from a packed tarball (\`npm pack\`), or develop it in a workspace (\`vlx init library\`)`));
147
+ }
148
+ }
149
+ }
150
+ return { libraries, problems };
151
+ };
152
+ /** The library a file belongs to (a template or a stylesheet, by absolute or project-relative path), if any. */
153
+ export const libraryOf = (libraries, root, file) => {
154
+ let real = resolve(root, file);
155
+ try {
156
+ real = realpathSync(real);
157
+ }
158
+ catch {
159
+ // a file that does not exist belongs to whoever its path says
160
+ }
161
+ return libraries.find((each) => real.startsWith(each.dir + sep));
162
+ };
163
+ /** The qualified id of a component: `package/name` for a library's, the bare name for the application's and the built-ins'. */
164
+ export const idOf = (libraries, root, file, name) => {
165
+ const library = libraryOf(libraries, root, file);
166
+ return library === undefined ? name : `${library.name}/${name}`;
167
+ };
168
+ /** What the build tells Vite about marked packages: process them (`noExternal`), and resolve the `valance` condition. */
169
+ export const viteSettings = (libraries) => ({ names: libraries.map((each) => each.name) });
@@ -0,0 +1,32 @@
1
+ import type { Diagnostic } from "./diagnostics.js";
2
+ import type { PackageManager } from "./init.js";
3
+ export interface LibraryAnswers {
4
+ /** The library's package name (`acme-ui`, `@acme/ui`). */
5
+ readonly name: string;
6
+ /** Where the workspace is written: absolute. */
7
+ readonly dir: string;
8
+ readonly packageManager: PackageManager;
9
+ readonly cli?: string;
10
+ }
11
+ /** Every file \`vlx init library\` writes, without writing any: relative path (\`/\` separated) → text. */
12
+ export declare const planLibrary: (answers: LibraryAnswers) => ReadonlyMap<string, string>;
13
+ /** Writes the workspace. Throws if the directory has something in it. Returns the files written. */
14
+ export declare const scaffoldLibrary: (answers: LibraryAnswers) => ReadonlyArray<string>;
15
+ /**
16
+ * The rules a consumer's build relies on, held at the library where its author can act (K7, K8, K11, K13): the marker, the entry that exists, a peer on `@valancex/common`, no entry Node would load
17
+ * unmarked, stylesheets that exist and a `sideEffects` that keeps them, and the playground: an application that depends on this package, builds every target the package declares, and uses every
18
+ * template the package ships (so no template is shipped that no target has checked).
19
+ */
20
+ export declare const checkLibrary: (dir: string, options?: {
21
+ readonly app?: string;
22
+ }) => Promise<ReadonlyArray<Diagnostic>>;
23
+ export interface BuiltLibrary {
24
+ readonly diagnostics: ReadonlyArray<Diagnostic>;
25
+ /** The files copied beside the compiled JavaScript: templates and stylesheets, relative to `dist/`. */
26
+ readonly copied: ReadonlyArray<string>;
27
+ }
28
+ /**
29
+ * Writes `dist/`: the TypeScript compiled to JavaScript with declarations (the contracts of the components are the types `component()` gives them), and every `.mprx` and `.css` under `src/` copied beside
30
+ * it, because templates ship as source and the consumer's build compiles them (K3). It does not check the package; `vlx library check` does.
31
+ */
32
+ export declare const buildLibrary: (dir: string) => BuiltLibrary;
@@ -0,0 +1,333 @@
1
+ // The author's side of a library (Bet 12; K7, K8, K13): `vlx init library` makes a workspace (the library, and a playground application that uses it), `vlx library build` writes what is published
2
+ // (JavaScript, declarations, and the templates and stylesheets as source), `vlx library check` holds the package to the rules a consumer's build relies on. The consumer's side (finding a marked
3
+ // package, the build settings, the diagnostics at the use site) is `libraries.ts`.
4
+ import { spawnSync } from "node:child_process";
5
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
7
+ import { check } from "./check.js";
8
+ import { loadConfig } from "./config.js";
9
+ import { diagnostic } from "./diagnostics.js";
10
+ import { cliSpec, commonSpec, packageName, runCommand } from "./init.js";
11
+ import { CONDITION, hasCondition, installedAt } from "./libraries.js";
12
+ import { themeFileProblems } from "./theme.js";
13
+ const own = () => JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
14
+ const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
15
+ const MPRX_D_TS = `// What importing a template file gives TypeScript: the file's address. (The bundler makes the import; see @valancex/common/types/mprx.d.ts.)
16
+ declare module "*.mprx" {
17
+ const template: { readonly mprx: string };
18
+ export default template;
19
+ }
20
+ `;
21
+ /** Every file \`vlx init library\` writes, without writing any: relative path (\`/\` separated) → text. */
22
+ export const planLibrary = (answers) => {
23
+ const out = new Map();
24
+ const manager = answers.packageManager;
25
+ const slug = packageName(answers.name.replace(/^@[^/]+\//, ""));
26
+ const lib = `packages/${slug}`;
27
+ const common = own().dependencies["@valancex/common"];
28
+ const range = common === undefined || common.startsWith("workspace:") ? `^${own().version}` : common;
29
+ const link = manager === "pnpm" ? "workspace:*" : "*";
30
+ const effect = own().dependencies["effect"] ?? "^3.10.0";
31
+ const q = '"';
32
+ out.set("package.json", json({
33
+ name: `${slug}-workspace`,
34
+ version: "0.0.0",
35
+ private: true,
36
+ type: "module",
37
+ ...(manager === "pnpm" ? {} : { workspaces: ["packages/*", "playground"] }),
38
+ scripts: {
39
+ // each through the package's own script: `vlx` is installed in the package that uses it, not at the workspace root, where a script cannot find it
40
+ "build:library": `cd ${lib} && ${runCommand(manager)} build`,
41
+ "check:library": `cd ${lib} && ${runCommand(manager)} check`,
42
+ playground: `cd playground && ${runCommand(manager)} dev`,
43
+ },
44
+ }));
45
+ if (manager === "pnpm") {
46
+ out.set("pnpm-workspace.yaml", `packages:\n - "packages/*"\n - playground\nallowBuilds:\n esbuild: false\n`);
47
+ }
48
+ out.set(".gitignore", "node_modules\ndist\n.valance\n");
49
+ out.set(`${lib}/package.json`, json({
50
+ name: answers.name,
51
+ version: "0.0.0",
52
+ description: "A VALANCE component library.",
53
+ license: "MIT",
54
+ type: "module",
55
+ files: ["dist"],
56
+ // the condition is what marks a library: the consumer's build processes the package instead of loading it in Node, which does not know .mprx or .css (decision K11)
57
+ exports: { ".": { types: "./dist/index.d.ts", [CONDITION]: "./dist/index.js" } },
58
+ sideEffects: ["**/*.css"],
59
+ valance: { kind: "components", targets: ["web", "desktop"] },
60
+ scripts: { build: "vlx library build", check: "vlx library check" },
61
+ peerDependencies: { "@valancex/common": range, effect: ">=3.10.0 <4" },
62
+ devDependencies: { "@types/node": "^22.0.0", "@valancex/cli": cliSpec(manager, answers.cli), "@valancex/common": commonSpec(manager), effect, typescript: "^5.6.0" },
63
+ }));
64
+ out.set(`${lib}/tsconfig.json`, json({
65
+ compilerOptions: {
66
+ target: "ES2022",
67
+ module: "ESNext",
68
+ moduleResolution: "Bundler",
69
+ lib: ["ES2022", "DOM"],
70
+ types: ["node"],
71
+ strict: true,
72
+ noUncheckedIndexedAccess: true,
73
+ exactOptionalPropertyTypes: true,
74
+ skipLibCheck: true,
75
+ declaration: true,
76
+ rootDir: "src",
77
+ outDir: "dist",
78
+ },
79
+ include: ["src"],
80
+ }));
81
+ out.set(`${lib}/src/mprx.d.ts`, MPRX_D_TS);
82
+ out.set(`${lib}/src/index.ts`, `// What the library exports: its components, and the list of them an application can pass to \`components\`. Removing an export, a component, a prop or an event is a breaking change (decision K8).
83
+ export { greeting } from "./greeting.js";
84
+ import { greeting } from "./greeting.js";
85
+
86
+ export const components = [greeting];
87
+ `);
88
+ out.set(`${lib}/src/greeting.mprx`, `<column>
89
+ <heading>{title}</heading>
90
+ <text>{message}</text>
91
+ <button on.click={dismiss()}>Dismiss</button>
92
+ </column>
93
+ `);
94
+ out.set(`${lib}/src/greeting.ts`, `import { component } from "@valancex/common";
95
+ import { Schema } from "effect";
96
+ import template from "./greeting.mprx";
97
+
98
+ // The interface of \`<greeting>\`, stated once, where it is defined. It is built from the portable core (\`column\`, \`heading\`, \`text\`, \`button\`), so it runs on every target the package declares.
99
+ export const greeting = component(template, {
100
+ props: { title: Schema.String, message: Schema.String },
101
+ events: { dismiss: Schema.Void },
102
+ });
103
+ `);
104
+ out.set(`${lib}/README.md`, `# ${answers.name}
105
+
106
+ A VALANCE component library. Its templates ship as source and the application's build compiles them; the package says so with the \`valance\` export condition, and says what it supports in its \`valance\` field.
107
+
108
+ \`\`\`
109
+ ${runCommand(manager)} build:library # vlx library build: JavaScript, declarations, templates and stylesheets into dist/
110
+ ${runCommand(manager)} check:library # vlx library check: the package, and the playground that uses every component
111
+ ${runCommand(manager)} playground # the playground application, served
112
+ \`\`\`
113
+
114
+ The playground (\`../../playground\`) is an application that depends on this package the way a consumer does; it is how each target the package declares is checked.
115
+ `);
116
+ out.set("playground/package.json", json({
117
+ name: `${slug}-playground`,
118
+ version: "0.0.0",
119
+ private: true,
120
+ type: "module",
121
+ scripts: { dev: "vlx dev", build: "vlx build", check: "vlx check", prepare: "vlx sync" },
122
+ dependencies: { "@valancex/common": commonSpec(manager), effect, [answers.name]: link },
123
+ devDependencies: { "@types/node": "^22.0.0", "@valancex/cli": cliSpec(manager, answers.cli), typescript: "^5.6.0" },
124
+ }));
125
+ out.set("playground/valance.json", json({ $schema: "./node_modules/@valancex/cli/schemas/valance.schema.json", version: 1, name: `${slug} playground` }));
126
+ out.set("playground/valance.web.json", json({ $schema: "./node_modules/@valancex/cli/schemas/valance.web.schema.json", version: 1 }));
127
+ out.set("playground/valance.desktop.json", json({ $schema: "./node_modules/@valancex/cli/schemas/valance.desktop.schema.json", version: 1 }));
128
+ out.set("playground/tsconfig.json", json({
129
+ compilerOptions: {
130
+ types: ["node"],
131
+ target: "ES2022",
132
+ module: "NodeNext",
133
+ moduleResolution: "NodeNext",
134
+ lib: ["ES2022", "DOM"],
135
+ strict: true,
136
+ noUncheckedIndexedAccess: true,
137
+ exactOptionalPropertyTypes: true,
138
+ skipLibCheck: true,
139
+ isolatedModules: true,
140
+ noEmit: true,
141
+ allowArbitraryExtensions: true,
142
+ rootDirs: ["src", ".valance/types"],
143
+ },
144
+ include: ["src"],
145
+ }));
146
+ out.set("playground/.gitignore", "node_modules\ndist\n.valance\n");
147
+ out.set("playground/content/index.md", `---\ntitle: ${answers.name} playground\n---\n\n# ${answers.name}\n\nEvery component of the library, on every target it declares.\n`);
148
+ out.set("playground/src/main.ts", `import { component, defineApp, field } from "@valancex/common";
149
+ import { Schema } from "effect";
150
+ import { components } from ${q}${answers.name}${q};
151
+ import layout from "./layout.mprx";
152
+ import desktopLayout from "./desktop/layout.mprx";
153
+
154
+ // The playground uses the library the way an application does: it lists its components, and a template writes their tags. Each target the library declares has a layout here, so \`vlx library check\`
155
+ // holds every component to every target it says it supports.
156
+ export default defineApp({
157
+ targets: { desktop: { layout: component(desktopLayout) } },
158
+ layout: component(layout),
159
+ components,
160
+ state: { dismissed: field(Schema.Boolean, false) },
161
+ commands: (command) => ({ dismiss: command((state) => ({ ...state, dismissed: true })) }),
162
+ });
163
+ `);
164
+ const layout = `<column>
165
+ {#if dismissed}
166
+ <text>Dismissed</text>
167
+ {:else}
168
+ <greeting title="Hello" message="This is the playground." on.dismiss={dismiss()} />
169
+ {/if}
170
+ </column>
171
+ `;
172
+ out.set("playground/src/layout.mprx", layout);
173
+ out.set("playground/src/desktop/layout.mprx", layout);
174
+ return out;
175
+ };
176
+ /** Writes the workspace. Throws if the directory has something in it. Returns the files written. */
177
+ export const scaffoldLibrary = (answers) => {
178
+ if (existsSync(answers.dir) && readdirSync(answers.dir).filter((each) => each !== ".git").length > 0) {
179
+ throw new Error(`${answers.dir} is not empty`);
180
+ }
181
+ const made = planLibrary(answers);
182
+ for (const [name, text] of made) {
183
+ const path = join(answers.dir, ...name.split("/"));
184
+ mkdirSync(dirname(path), { recursive: true });
185
+ writeFileSync(path, text);
186
+ }
187
+ return [...made.keys()].sort();
188
+ };
189
+ const FILE = "package.json";
190
+ /** The files under `dir` with `extension`, relative and sorted. */
191
+ const under = (dir, extension) => existsSync(dir)
192
+ ? readdirSync(dir, { recursive: true, encoding: "utf8" })
193
+ .map((each) => each.split(sep).join("/"))
194
+ .filter((each) => each.endsWith(extension) && !each.split("/").includes("node_modules"))
195
+ .sort()
196
+ : [];
197
+ /**
198
+ * The rules a consumer's build relies on, held at the library where its author can act (K7, K8, K11, K13): the marker, the entry that exists, a peer on `@valancex/common`, no entry Node would load
199
+ * unmarked, stylesheets that exist and a `sideEffects` that keeps them, and the playground: an application that depends on this package, builds every target the package declares, and uses every
200
+ * template the package ships (so no template is shipped that no target has checked).
201
+ */
202
+ export const checkLibrary = async (dir, options = {}) => {
203
+ const problems = [];
204
+ const problem = (code, message) => void problems.push(diagnostic(FILE, 0, code, message));
205
+ let manifest;
206
+ try {
207
+ manifest = JSON.parse(readFileSync(join(dir, FILE), "utf8"));
208
+ }
209
+ catch {
210
+ return [diagnostic(FILE, 0, "library-package", `there is no ${FILE} in ${dir}: run \`vlx library check\` in the library's package directory`)];
211
+ }
212
+ if (!hasCondition(manifest.exports)) {
213
+ problem("library-unmarked", `${FILE} has no "valance" export condition: add ${JSON.stringify({ exports: { ".": { types: "./dist/index.d.ts", valance: "./dist/index.js" } } })}; the condition is what marks a library`);
214
+ return problems;
215
+ }
216
+ const entry = manifest.exports?.["."];
217
+ const marked = typeof entry?.[CONDITION] === "string" ? entry[CONDITION] : undefined;
218
+ if (marked === undefined) {
219
+ problem("library-entry", `exports["."] needs a "valance" condition that is the file: "valance": "./dist/index.js"`);
220
+ }
221
+ else if (!existsSync(resolve(dir, marked))) {
222
+ problem("library-entry", `exports["."].valance is ${marked}, which does not exist: run \`vlx library build\``);
223
+ }
224
+ // Node would load whatever else the entry offers; that is the .mprx failure the condition exists to prevent.
225
+ for (const key of ["default", "import", "require", "node"]) {
226
+ const other = entry?.[key];
227
+ if (other !== undefined && other !== marked) {
228
+ problem("library-default", `exports["."].${key} is ${JSON.stringify(other)}: a tool that does not know the "valance" condition would load it and meet a template. Remove it, or make it the same file as "valance"`);
229
+ }
230
+ }
231
+ const isTheme = manifest.valance?.kind === "theme";
232
+ if (!isTheme && manifest.peerDependencies?.["@valancex/common"] === undefined) {
233
+ problem("library-peer", `${FILE} should declare @valancex/common as a peer dependency (one copy of the framework in an application; removing it from the peers is breaking, K8)`);
234
+ }
235
+ const styles = manifest.valance?.styles ?? [];
236
+ if (styles.length > 0) {
237
+ const sideEffects = manifest.sideEffects;
238
+ const keepsCss = Array.isArray(sideEffects) && sideEffects.some((each) => typeof each === "string" && each.includes(".css"));
239
+ if (sideEffects === true || sideEffects === undefined || keepsCss) {
240
+ // (true and absent keep every file; a list must name the stylesheets)
241
+ }
242
+ else {
243
+ problem("library-side-effects", `"sideEffects" in ${FILE} drops stylesheets from a bundle: use ["**/*.css"] (K6)`);
244
+ }
245
+ }
246
+ // A theme is data: it is checked as a project's theme is (tokens, recipes, the contrast of the pairs it declares), and needs no playground.
247
+ if (isTheme) {
248
+ const file = manifest.valance?.theme === undefined ? undefined : resolve(dir, manifest.valance.theme);
249
+ if (file === undefined || !existsSync(file)) {
250
+ problem("library-field", `"valance.theme" in ${FILE} must name the package's theme.json`);
251
+ }
252
+ else {
253
+ problems.push(...themeFileProblems(file, dir));
254
+ }
255
+ return problems;
256
+ }
257
+ // the playground
258
+ const app = options.app ?? ["../../playground", "playground", "../playground"].map((each) => resolve(dir, each)).find((each) => existsSync(join(each, FILE)));
259
+ if (app === undefined) {
260
+ problem("library-playground", "no playground application was found (../../playground, ./playground or ../playground; or --app <dir>): an application that depends on this package and uses each of its components is how each target it declares is checked");
261
+ return problems;
262
+ }
263
+ const label = relative(dir, app).split(sep).join("/");
264
+ const config = await (async () => {
265
+ try {
266
+ return loadConfig(app);
267
+ }
268
+ catch (error) {
269
+ problem("library-playground", `${label}: ${error instanceof Error ? error.message : String(error)}`);
270
+ return undefined;
271
+ }
272
+ })();
273
+ if (config === undefined) {
274
+ return problems;
275
+ }
276
+ const declared = manifest.valance?.targets ?? ["web", "desktop"];
277
+ for (const target of declared) {
278
+ if (!config.targets.includes(target)) {
279
+ problem("library-playground-target", `the package declares ${target}, but the playground (${label}) does not build for it: add valance.${target}.json there, so that target is checked`);
280
+ }
281
+ }
282
+ if (installedAt(app, manifest.name ?? "") === undefined) {
283
+ problem("library-playground", `the playground (${label}) has no ${manifest.name} installed: install the workspace, and run \`vlx library build\` first`);
284
+ return problems;
285
+ }
286
+ const result = await check(app);
287
+ for (const each of result.diagnostics) {
288
+ problems.push({ ...each, file: `${label}/${each.file}` });
289
+ }
290
+ // every template the package ships is used by the playground: a target it declares has then checked it
291
+ const shipped = under(resolve(dir, dirname(marked ?? "dist/index.js")), ".mprx").map((each) => basename(each, ".mprx"));
292
+ const checked = new Set(result.components);
293
+ for (const name of shipped) {
294
+ if (!checked.has(name)) {
295
+ problems.push(diagnostic(`${label}/src/layout.mprx`, 0, "library-unused", `the package ships <${name}>, but the playground's templates never write it, so no target has checked it: use it there`));
296
+ }
297
+ }
298
+ return problems;
299
+ };
300
+ /** The TypeScript compiler of the library's own installation. */
301
+ const tscOf = (dir) => {
302
+ const installed = installedAt(dir, "typescript");
303
+ return installed === undefined ? undefined : join(installed, "bin", "tsc");
304
+ };
305
+ /**
306
+ * Writes `dist/`: the TypeScript compiled to JavaScript with declarations (the contracts of the components are the types `component()` gives them), and every `.mprx` and `.css` under `src/` copied beside
307
+ * it, because templates ship as source and the consumer's build compiles them (K3). It does not check the package; `vlx library check` does.
308
+ */
309
+ export const buildLibrary = (dir) => {
310
+ const src = join(dir, "src");
311
+ if (!existsSync(src)) {
312
+ return { diagnostics: [diagnostic("src", 0, "library-source", `there is no src/ in ${dir}: run \`vlx library build\` in the library's package directory`)], copied: [] };
313
+ }
314
+ const tsc = tscOf(dir);
315
+ if (tsc === undefined) {
316
+ return { diagnostics: [diagnostic(FILE, 0, "library-build", "typescript is not installed here: add it to the library's devDependencies and install")], copied: [] };
317
+ }
318
+ // dist/ is made by this build alone: a file left from a component that was renamed or removed would be shipped (and its template checked as if it were a component)
319
+ rmSync(join(dir, "dist"), { recursive: true, force: true });
320
+ const ran = spawnSync(process.execPath, [tsc, "-p", join(dir, "tsconfig.json")], { cwd: dir, encoding: "utf8" });
321
+ if (ran.status !== 0) {
322
+ const lines = `${ran.stdout}${ran.stderr}`.split("\n").filter((each) => each.trim() !== "");
323
+ return { diagnostics: lines.map((line) => diagnostic("tsconfig.json", 0, "library-build", line)), copied: [] };
324
+ }
325
+ const dist = join(dir, "dist");
326
+ const copied = [];
327
+ for (const file of [...under(src, ".mprx"), ...under(src, ".css")]) {
328
+ mkdirSync(dirname(join(dist, file)), { recursive: true });
329
+ copyFileSync(join(src, file), join(dist, file));
330
+ copied.push(file);
331
+ }
332
+ return { diagnostics: [], copied };
333
+ };
package/dist/package.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // `vlx package`: the desktop target as something to hand to a person. The engine becomes one executable (Node's single executable application: the engine bundled to one file, with the MESH
2
2
  // runtime's module inside as an asset, injected into a `node` binary), set beside the Desktop PORT's window program. Unsigned: signing and notarizing are not done here (the artifacts say so).
3
- import { build as bundle } from "vite";
3
+ import { build as bundle, defaultServerConditions } from "vite";
4
+ import { CONDITION } from "./libraries.js";
4
5
  import { execFileSync } from "node:child_process";
5
6
  import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
6
7
  import { createRequire } from "node:module";
@@ -95,7 +96,9 @@ export const packageDesktop = async (root, options = {}) => {
95
96
  logLevel: "warn",
96
97
  plugins: [templates(root)],
97
98
  cacheDir: join(root, GENERATED_DIR, "cache"),
98
- ssr: { noExternal: true, target: "node" },
99
+ // the desktop engine is one file: everything is bundled, and a library resolves by its `valance` condition (K4)
100
+ resolve: { conditions: [CONDITION, ...defaultServerConditions] },
101
+ ssr: { noExternal: true, target: "node", resolve: { conditions: [CONDITION, ...defaultServerConditions] } },
99
102
  esbuild: { keepNames: true },
100
103
  build: {
101
104
  ssr: join(root, GENERATED_DIR, "desktop.sea.ts"),
package/dist/theme.d.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import type { Diagnostic } from "./diagnostics.js";
2
2
  export declare const THEME_DIR = "theme";
3
- /** The theme file the project uses: its own `theme/theme.json`, else the default theme's. */
3
+ /**
4
+ * The theme file the project uses: a theme package it names (`"theme": "acme-theme"` in `valance.json`; K12), else its own `theme/theme.json`, else the default theme's. The package's problems (not
5
+ * installed, not a theme, and a `theme/` directory beside it) are `problems`.
6
+ */
4
7
  export declare const themeFileOf: (root: string) => {
5
8
  readonly file: string;
6
9
  readonly custom: boolean;
10
+ readonly problems: ReadonlyArray<Diagnostic>;
7
11
  };
12
+ /** The problems of a theme file, checked as a project's theme is (tokens, recipes, contrast of the pairs it declares); `base` is what a file's name is shown relative to. */
13
+ export declare const themeFileProblems: (file: string, base: string) => ReadonlyArray<Diagnostic>;
8
14
  export interface ThemeResult {
9
15
  readonly custom: boolean;
10
16
  readonly name: string;
package/dist/theme.js CHANGED
@@ -1,24 +1,64 @@
1
1
  // The project's theme. A project that writes none has the default theme (Tier 1: nothing to do, and every primitive of the portable core is styled); a project that has a `theme/` directory with a
2
2
  // `theme.json` (Tier 2) uses it instead, whole: its token documents by mode and its recipes. A recipe's `escape` block (Tier 3) is the marked way to a CSS property outside the contract, for the web.
3
3
  // The stylesheet is compiled here, at build time; nothing is computed in the browser.
4
- import { existsSync } from "node:fs";
4
+ import { existsSync, readFileSync } from "node:fs";
5
5
  import { join, relative } from "node:path";
6
6
  import { checkTheme, compileWeb, coverageOf, defaultInputs, loadTheme, resolveForDesktop, root as styleRoot } from "@valancex/style";
7
7
  import { diagnostic } from "./diagnostics.js";
8
+ import { librariesOf } from "./libraries.js";
8
9
  export const THEME_DIR = "theme";
9
- /** The theme file the project uses: its own `theme/theme.json`, else the default theme's. */
10
+ /** The theme package `valance.json` names, if it names one (read here without the rest of the configuration: `vlx check` reports a wrong `valance.json` itself). */
11
+ const themePackageOf = (root) => {
12
+ try {
13
+ const given = JSON.parse(readFileSync(join(root, "valance.json"), "utf8")).theme;
14
+ return typeof given === "string" ? given : undefined;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ };
20
+ /**
21
+ * The theme file the project uses: a theme package it names (`"theme": "acme-theme"` in `valance.json`; K12), else its own `theme/theme.json`, else the default theme's. The package's problems (not
22
+ * installed, not a theme, and a `theme/` directory beside it) are `problems`.
23
+ */
10
24
  export const themeFileOf = (root) => {
11
25
  const own = join(root, THEME_DIR, "theme.json");
12
- return existsSync(own) ? { file: own, custom: true } : { file: join(styleRoot, "themes", "default", "theme.json"), custom: false };
26
+ const named = themePackageOf(root);
27
+ const fallback = existsSync(own) ? { file: own, custom: true } : { file: join(styleRoot, "themes", "default", "theme.json"), custom: false };
28
+ if (named === undefined) {
29
+ return { ...fallback, problems: [] };
30
+ }
31
+ const library = librariesOf(root).libraries.find((each) => each.name === named);
32
+ if (library === undefined || library.kind !== "theme" || library.theme === undefined) {
33
+ return {
34
+ ...fallback,
35
+ custom: true,
36
+ problems: [
37
+ diagnostic("valance.json", 0, "theme-package", `"theme": "${named}" is not an installed theme package: install it, and it must be marked (a "valance" export condition and \`"valance": { "kind": "theme", "theme": "./theme/theme.json" }\`)`),
38
+ ],
39
+ };
40
+ }
41
+ return {
42
+ file: library.theme,
43
+ custom: true,
44
+ problems: existsSync(own)
45
+ ? [diagnostic("valance.json", 0, "theme-conflict", `"theme": "${named}" and a theme/ directory are both given: a project has one theme; remove one`)]
46
+ : [],
47
+ };
13
48
  };
14
49
  const asDiagnostic = (each) => diagnostic(each.file, 0, each.code, `${each.path === undefined || each.path === "" ? "" : `${each.path}: `}${each.message}`);
50
+ /** The problems of a theme file, checked as a project's theme is (tokens, recipes, contrast of the pairs it declares); `base` is what a file's name is shown relative to. */
51
+ export const themeFileProblems = (file, base) => {
52
+ const loaded = loadTheme(file, (path) => relative(base, path).split("\\").join("/"));
53
+ return checkTheme(loaded, defaultInputs()).problems.map(asDiagnostic);
54
+ };
15
55
  /** Loads and checks the project's theme. */
16
56
  export const themeOf = (root) => {
17
- const { file, custom } = themeFileOf(root);
57
+ const { file, custom, problems } = themeFileOf(root);
18
58
  const loaded = loadTheme(file, (path) => relative(root, path).split("\\").join("/"));
19
59
  const inputs = defaultInputs();
20
60
  const checked = checkTheme(loaded, inputs);
21
- const diagnostics = checked.problems.map(asDiagnostic);
61
+ const diagnostics = [...problems, ...checked.problems.map(asDiagnostic)];
22
62
  return {
23
63
  custom,
24
64
  name: loaded.theme.name,
@@ -37,7 +77,7 @@ export const themeReport = (root) => {
37
77
  custom: themeFileOf(root).custom,
38
78
  name: loaded.theme.name,
39
79
  modes: Object.keys(loaded.modes),
40
- diagnostics: checked.problems.map(asDiagnostic),
80
+ diagnostics: [...themeFileOf(root).problems, ...checked.problems.map(asDiagnostic)],
41
81
  contrast: checked.contrast,
42
82
  coverage: ["web", "desktop"].map((target) => coverageOf(loaded, inputs, target)),
43
83
  };
package/dist/vite.js CHANGED
@@ -1,5 +1,6 @@
1
- import { createLogger, createServer } from "vite";
1
+ import { createLogger, createServer, defaultClientConditions, defaultServerConditions } from "vite";
2
2
  import { join, relative, sep } from "node:path";
3
+ import { CONDITION, librariesOf } from "./libraries.js";
3
4
  import { GENERATED_DIR, OUTPUT_DIR } from "./project.js";
4
5
  export const CLIENT_ENTRY = `${GENERATED_DIR}/client.ts`;
5
6
  export const SERVER_ENTRY = `/${GENERATED_DIR}/server.ts`;
@@ -30,7 +31,19 @@ export const templates = (root) => ({
30
31
  // The module is only the file's address, so a change to the template changes nothing the browser holds; the rebuild (dev.ts) recompiles it and reloads once, with the overlay if it fails.
31
32
  handleHotUpdate: ({ file }) => (file.endsWith(".mprx") ? [] : undefined),
32
33
  });
34
+ /**
35
+ * What marked packages (libraries, K4) need from the bundler: the `valance` export condition resolved, for the browser and for the server, and the packages processed by Vite instead of loaded by
36
+ * Node, which does not know `.mprx` or `.css`. Added for every project, with nothing to write: a project with no library has no package that has the condition.
37
+ */
38
+ const librarySettings = (root) => {
39
+ const names = librariesOf(root).libraries.map((each) => each.name);
40
+ return {
41
+ resolve: { conditions: [CONDITION, ...defaultClientConditions] },
42
+ ssr: { noExternal: names, resolve: { conditions: [CONDITION, ...defaultServerConditions] } },
43
+ };
44
+ };
33
45
  export const baseConfig = (root, config, out = OUTPUT_DIR) => ({
46
+ ...librarySettings(root),
34
47
  root,
35
48
  configFile: false,
36
49
  base: config.web.base,
@@ -43,7 +56,8 @@ export const baseConfig = (root, config, out = OUTPUT_DIR) => ({
43
56
  clearScreen: false,
44
57
  build: { outDir: join(root, out), emptyOutDir: true, target: "esnext", manifest: true, rollupOptions: { input: join(root, CLIENT_ENTRY) } },
45
58
  // The MESH runtime refers to node:fs/promises on a path the browser never takes.
46
- optimizeDeps: { exclude: ["@valancex/mesh-runtime", "@valancex/cli", "@valancex/common"] },
59
+ // (a library's package is not prebundled either: its templates are the build's to compile)
60
+ optimizeDeps: { exclude: ["@valancex/mesh-runtime", "@valancex/cli", "@valancex/common", ...librariesOf(root).libraries.map((each) => each.name)] },
47
61
  });
48
62
  /** A Vite server that serves nothing: it loads the author's TypeScript and the generated server entry in Node. Used by build and check. */
49
63
  export const createLoader = async (root, config) => createServer({ ...baseConfig(root, config), optimizeDeps: { noDiscovery: true, include: [] }, server: { middlewareMode: true, hmr: false, watch: null, ws: false } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valancex/cli",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "vlx, the VALANCE command: make an application (vlx init), serve it as you edit (vlx dev), build it to a static site (vlx build), check it (vlx check). Content, layout, components and styles in, a website out.",
5
5
  "keywords": [
6
6
  "valance",
@@ -56,9 +56,9 @@
56
56
  "postject": "1.0.0-alpha.6",
57
57
  "react": "19.2.8",
58
58
  "vite": "7.3.6",
59
- "@valancex/style": "^0.14.0",
60
- "@valancex/valance": "^0.14.0",
61
- "@valancex/common": "^0.14.0"
59
+ "@valancex/common": "^0.15.0",
60
+ "@valancex/style": "^0.15.0",
61
+ "@valancex/valance": "^0.15.0"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/jsdom": "^21.1.7",
@@ -64,6 +64,11 @@
64
64
  }
65
65
  }
66
66
  },
67
+ "theme": {
68
+ "type": "string",
69
+ "pattern": "^(@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$",
70
+ "description": "The theme package this project uses in place of the default theme: an installed package whose `valance` field is { \"kind\": \"theme\", \"theme\": \"./theme/theme.json\" }. A project with both this and a `theme/` directory is refused."
71
+ },
67
72
  "budgets": {
68
73
  "type": "array",
69
74
  "description": "The size budgets `vlx build` holds the production build to (never `vlx dev`). A type, a warning and an error threshold in bytes: 500000, \"500kB\" or \"1MB\" (1 kB is 1,000 bytes). With none, the framework's start-up budgets apply.",