@valbuild/cli 0.120.3 → 0.121.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/CHANGELOG.md CHANGED
@@ -1,5 +1,99 @@
1
1
  # @valbuild/cli
2
2
 
3
+ ## 0.121.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#605](https://github.com/valbuild/val/pull/605) [`6794d29`](https://github.com/valbuild/val/commit/6794d2980bc81284ab7f2cc667f01cc21c9e3a79) Thanks [@freekh](https://github.com/freekh)! - `s.settings()`: the project's settings, as content.
8
+
9
+ A settings module is one per project, at the root of the content tree:
10
+
11
+ ```typescript
12
+ // settings.val.ts
13
+ export default c.define("/settings.val.ts", s.settings(), {});
14
+ ```
15
+
16
+ Register it in `val.modules.ts` like any other module, and it shows up in the
17
+ Studio under the cog at the foot of the left rail. Everything in it is content:
18
+ it is edited as a draft, it appears in the publish diff, and it is the same for
19
+ everyone working on the project.
20
+
21
+ Every key is optional, at every level, so `{}` is a complete settings module —
22
+ and stays one as sections are added. What it holds today is the assistant:
23
+
24
+ ```typescript
25
+ export default c.define("/settings.val.ts", s.settings(), {
26
+ assistant: {
27
+ enabled: true,
28
+ context: "A CMS for developers, run by a team of four in Oslo.",
29
+ tone: "Plain and direct. British English, sentence case in headings.",
30
+ },
31
+ });
32
+ ```
33
+
34
+ `context` is background the assistant would otherwise guess at; `tone` is how it
35
+ should write when it writes content. Both are sent with every message it makes.
36
+
37
+ `enabled` decides whether editors have an assistant, and it has **three** states
38
+ rather than two:
39
+
40
+ - `true` — they do.
41
+ - `false` — they do not, and every trace of it goes: no button in the top bar,
42
+ no row in the quick actions, no panel, nothing sent.
43
+ - unset — nobody has decided. The assistant is still **shown**, and asks to be
44
+ turned on before it is used. Hiding an assistant nobody has decided about
45
+ means nobody discovers it; quietly enabling one means a project starts sending
46
+ its content to a model because it did not know to say no.
47
+
48
+ A project with no settings module at all has an assistant, as before: there is
49
+ nowhere to record a decision, and nowhere for the prompt to write the answer.
50
+
51
+ **Breaking: `ai.chat` is gone from `val.config.ts`.** Whether the assistant is
52
+ available is a decision about the project's content, made by the people who edit
53
+ it, so it moved to settings — turning the chat on used to take a developer, a
54
+ deploy and a code review of a boolean. Remove the whole block:
55
+
56
+ ```diff
57
+ const { s, c, val, config } = initVal({
58
+ - ai: {
59
+ - chat: {
60
+ - experimental: { enable: true },
61
+ - suggestions: ["Summarize", "Fix typos at this page"],
62
+ - title: "Ask me anything",
63
+ - description: "Val can answer questions about the content.",
64
+ - },
65
+ - },
66
+ });
67
+ ```
68
+
69
+ `experimental.enable` becomes `assistant.enabled` in the settings module.
70
+ `suggestions`, `title` and `description` are removed with nothing replacing
71
+ them: the assistant now opens with its own copy. A project that had the chat
72
+ enabled and wants it to stay on for everyone should write
73
+ `assistant: { enabled: true }` — otherwise editors are offered it and asked.
74
+
75
+ `ai.commitMessages` stays in `val.config.ts`, and is unaffected.
76
+
77
+ Two settings modules, or one in a subdirectory, is a module error: the dev
78
+ server refuses to serve sources, `npx val validate` reports it against the file,
79
+ and the Studio says so rather than picking one.
80
+
81
+ ### Patch Changes
82
+
83
+ - Updated dependencies [[`2bcc6fd`](https://github.com/valbuild/val/commit/2bcc6fdff8d668123e07e3c5e81ac6fa1436e47b), [`2bcbee1`](https://github.com/valbuild/val/commit/2bcbee1be682c2bbd5b7bc7d152ddd4204162fd2), [`6794d29`](https://github.com/valbuild/val/commit/6794d2980bc81284ab7f2cc667f01cc21c9e3a79)]:
84
+ - @valbuild/shared@0.121.0
85
+ - @valbuild/server@0.121.0
86
+ - @valbuild/core@0.121.0
87
+ - @valbuild/language-server@0.121.0
88
+
89
+ ## 0.120.4
90
+
91
+ ### Patch Changes
92
+
93
+ - Updated dependencies []:
94
+ - @valbuild/server@0.120.4
95
+ - @valbuild/language-server@0.120.4
96
+
3
97
  ## 0.120.3
4
98
 
5
99
  ### Patch Changes
@@ -145,6 +145,28 @@ async function* runValidation({
145
145
  return;
146
146
  }
147
147
  const start = Date.now();
148
+ // How this module is DECLARED, before what is in it: a module error is not
149
+ // something content can fix (a misplaced settings module is otherwise
150
+ // perfectly valid content), and validating on top of it would report the
151
+ // consequences instead of the cause.
152
+ const moduleErrors = service.getModuleErrors().filter(moduleError => moduleError.path === moduleFilePath);
153
+ if (moduleErrors.length > 0) {
154
+ for (const moduleError of moduleErrors) {
155
+ errors += 1;
156
+ yield {
157
+ type: "fatal-error",
158
+ file: moduleFilePath,
159
+ message: moduleError.message
160
+ };
161
+ }
162
+ yield {
163
+ type: "file-error-count",
164
+ file: moduleFilePath,
165
+ errorCount: moduleErrors.length,
166
+ durationMs: Date.now() - start
167
+ };
168
+ return;
169
+ }
148
170
  const valModule = await service.get(moduleFilePath, "", {
149
171
  validate: true
150
172
  });
@@ -145,6 +145,28 @@ async function* runValidation({
145
145
  return;
146
146
  }
147
147
  const start = Date.now();
148
+ // How this module is DECLARED, before what is in it: a module error is not
149
+ // something content can fix (a misplaced settings module is otherwise
150
+ // perfectly valid content), and validating on top of it would report the
151
+ // consequences instead of the cause.
152
+ const moduleErrors = service.getModuleErrors().filter(moduleError => moduleError.path === moduleFilePath);
153
+ if (moduleErrors.length > 0) {
154
+ for (const moduleError of moduleErrors) {
155
+ errors += 1;
156
+ yield {
157
+ type: "fatal-error",
158
+ file: moduleFilePath,
159
+ message: moduleError.message
160
+ };
161
+ }
162
+ yield {
163
+ type: "file-error-count",
164
+ file: moduleFilePath,
165
+ errorCount: moduleErrors.length,
166
+ durationMs: Date.now() - start
167
+ };
168
+ return;
169
+ }
148
170
  const valModule = await service.get(moduleFilePath, "", {
149
171
  validate: true
150
172
  });
@@ -113,6 +113,28 @@ async function* runValidation({
113
113
  return;
114
114
  }
115
115
  const start = Date.now();
116
+ // How this module is DECLARED, before what is in it: a module error is not
117
+ // something content can fix (a misplaced settings module is otherwise
118
+ // perfectly valid content), and validating on top of it would report the
119
+ // consequences instead of the cause.
120
+ const moduleErrors = service.getModuleErrors().filter(moduleError => moduleError.path === moduleFilePath);
121
+ if (moduleErrors.length > 0) {
122
+ for (const moduleError of moduleErrors) {
123
+ errors += 1;
124
+ yield {
125
+ type: "fatal-error",
126
+ file: moduleFilePath,
127
+ message: moduleError.message
128
+ };
129
+ }
130
+ yield {
131
+ type: "file-error-count",
132
+ file: moduleFilePath,
133
+ errorCount: moduleErrors.length,
134
+ durationMs: Date.now() - start
135
+ };
136
+ return;
137
+ }
116
138
  const valModule = await service.get(moduleFilePath, "", {
117
139
  validate: true
118
140
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@valbuild/cli",
3
3
  "private": false,
4
- "version": "0.120.3",
4
+ "version": "0.121.0",
5
5
  "description": "Val CLI tools",
6
6
  "repository": {
7
7
  "type": "git",
@@ -24,11 +24,11 @@
24
24
  "jszip": "^3.10.1",
25
25
  "meow": "^9.0.0",
26
26
  "picocolors": "^1.1.1",
27
- "@valbuild/core": "0.120.0",
27
+ "@valbuild/core": "0.121.0",
28
28
  "@valbuild/eslint-plugin": "0.108.0",
29
- "@valbuild/language-server": "0.120.3",
30
- "@valbuild/server": "0.120.3",
31
- "@valbuild/shared": "0.120.0"
29
+ "@valbuild/language-server": "0.121.0",
30
+ "@valbuild/server": "0.121.0",
31
+ "@valbuild/shared": "0.121.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "prettier": "*",
@@ -878,4 +878,105 @@ describe("runValidation", () => {
878
878
  expect(events.at(-1)).not.toEqual({ type: "summary-success" });
879
879
  });
880
880
  });
881
+
882
+ describe("settings modules", () => {
883
+ /**
884
+ * Writes a settings module into the fixture and registers it.
885
+ *
886
+ * Written by the test rather than kept as a fixture file: "one settings
887
+ * module, at the root" is a rule about the whole project, so a permanent
888
+ * broken fixture would be a rule the fixture project is always breaking.
889
+ */
890
+ const addSettingsModule = (moduleFilePath: string) => {
891
+ const relative = moduleFilePath.slice(1);
892
+ const upToRoot = "../".repeat(relative.split("/").length - 1) || "./";
893
+ fs.mkdirSync(path.dirname(path.join(tmpDir, relative)), {
894
+ recursive: true,
895
+ });
896
+ fs.writeFileSync(
897
+ path.join(tmpDir, relative),
898
+ [
899
+ `import { s, c } from "${upToRoot}val.config";`,
900
+ `export default c.define("${moduleFilePath}", s.settings(), {});`,
901
+ "",
902
+ ].join("\n"),
903
+ );
904
+ const valModulesPath = path.join(tmpDir, "val.modules.ts");
905
+ const registration = ` { def: () => import("./${relative.replace(
906
+ /\.ts$/,
907
+ "",
908
+ )}") },\n]);`;
909
+ fs.writeFileSync(
910
+ valModulesPath,
911
+ fs
912
+ .readFileSync(valModulesPath, "utf8")
913
+ .replace(/\]\);\s*$/, registration),
914
+ );
915
+ };
916
+
917
+ const runOn = async (valFiles: string[]) => {
918
+ const events: ValidationEvent[] = [];
919
+ for await (const event of runValidation({
920
+ root: tmpDir,
921
+ fix: false,
922
+ valFiles,
923
+ project: undefined,
924
+ remote: mockRemote,
925
+ fs: createDefaultValFSHost(),
926
+ })) {
927
+ events.push(event);
928
+ }
929
+ return events;
930
+ };
931
+
932
+ test("a settings module at the root is valid", async () => {
933
+ addSettingsModule("/settings.val.ts");
934
+
935
+ expect((await runOn(["settings.val.ts"])).at(-1)).toEqual({
936
+ type: "summary-success",
937
+ });
938
+ });
939
+
940
+ test("rejects a settings module in a subdirectory", async () => {
941
+ addSettingsModule("/content/settings.val.ts");
942
+
943
+ const events = await runOn(["content/settings.val.ts"]);
944
+
945
+ expect(events).toEqual(
946
+ expect.arrayContaining([
947
+ expect.objectContaining({
948
+ type: "fatal-error",
949
+ message: expect.stringContaining(
950
+ "Settings must be defined at the root of the content tree",
951
+ ),
952
+ }),
953
+ ]),
954
+ );
955
+ expect(events.at(-1)).not.toEqual({ type: "summary-success" });
956
+ });
957
+
958
+ test("rejects a second settings module, naming both", async () => {
959
+ addSettingsModule("/settings.val.ts");
960
+ addSettingsModule("/config.val.ts");
961
+
962
+ const events = await runOn(["settings.val.ts", "config.val.ts"]);
963
+
964
+ expect(events).toEqual(
965
+ expect.arrayContaining([
966
+ expect.objectContaining({
967
+ type: "fatal-error",
968
+ file: "/settings.val.ts",
969
+ message: expect.stringContaining(
970
+ "A project can only define settings once",
971
+ ),
972
+ }),
973
+ expect.objectContaining({
974
+ type: "fatal-error",
975
+ file: "/config.val.ts",
976
+ }),
977
+ ]),
978
+ );
979
+ expect(events.at(-1)).not.toEqual({ type: "summary-success" });
980
+ });
981
+ });
881
982
  });
@@ -134,6 +134,30 @@ export async function* runValidation({
134
134
  return;
135
135
  }
136
136
  const start = Date.now();
137
+ // How this module is DECLARED, before what is in it: a module error is not
138
+ // something content can fix (a misplaced settings module is otherwise
139
+ // perfectly valid content), and validating on top of it would report the
140
+ // consequences instead of the cause.
141
+ const moduleErrors = service
142
+ .getModuleErrors()
143
+ .filter((moduleError) => moduleError.path === moduleFilePath);
144
+ if (moduleErrors.length > 0) {
145
+ for (const moduleError of moduleErrors) {
146
+ errors += 1;
147
+ yield {
148
+ type: "fatal-error",
149
+ file: moduleFilePath,
150
+ message: moduleError.message,
151
+ };
152
+ }
153
+ yield {
154
+ type: "file-error-count",
155
+ file: moduleFilePath,
156
+ errorCount: moduleErrors.length,
157
+ durationMs: Date.now() - start,
158
+ };
159
+ return;
160
+ }
137
161
  const valModule = await service.get(moduleFilePath, "" as ModulePath, {
138
162
  validate: true,
139
163
  });