@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 +5 -2
- package/dist/check.js +6 -1
- package/dist/cli.js +89 -2
- package/dist/compile.d.ts +8 -0
- package/dist/compile.js +60 -12
- package/dist/config.d.ts +2 -0
- package/dist/config.js +5 -1
- package/dist/contract.js +7 -0
- package/dist/emit.js +17 -1
- package/dist/libraries.d.ts +41 -0
- package/dist/libraries.js +169 -0
- package/dist/library.d.ts +32 -0
- package/dist/library.js +333 -0
- package/dist/package.js +5 -2
- package/dist/theme.d.ts +7 -1
- package/dist/theme.js +46 -6
- package/dist/vite.js +16 -2
- package/package.json +4 -4
- package/schemas/valance.schema.json +5 -0
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
|
-
|
|
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
|
-
|
|
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
|
|
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) &&
|
|
73
|
-
|
|
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
|
-
//
|
|
79
|
-
const listed = declaredOf(layout, overrides,
|
|
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" &&
|
|
86
|
-
|
|
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
|
-
|
|
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;
|
package/dist/library.js
ADDED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
60
|
-
"@valancex/
|
|
61
|
-
"@valancex/
|
|
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.",
|