@pixel-point/toolcraft 0.0.8 → 0.0.11

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.
Files changed (88) hide show
  1. package/README.md +42 -9
  2. package/package.json +1 -1
  3. package/src/generate.mjs +42 -5
  4. package/src/generate.test.mjs +40 -0
  5. package/src/package-json.mjs +15 -0
  6. package/src/package-json.test.mjs +14 -1
  7. package/templates/runtime/contracts/component-contracts.test.ts +251 -47
  8. package/templates/runtime/contracts/component-contracts.ts +130 -57
  9. package/templates/runtime/contracts/decision-contracts.test.ts +38 -1
  10. package/templates/runtime/contracts/decision-contracts.ts +22 -9
  11. package/templates/runtime/export/export.test.ts +63 -0
  12. package/templates/runtime/export/export.ts +55 -0
  13. package/templates/runtime/index.ts +1 -0
  14. package/templates/runtime/react/canvas-shell.test.tsx +58 -4
  15. package/templates/runtime/react/canvas-shell.tsx +31 -9
  16. package/templates/runtime/react/controls-panel.test.tsx +916 -108
  17. package/templates/runtime/react/controls-panel.tsx +136 -30
  18. package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
  19. package/templates/runtime/react/settings-transfer.test.ts +4 -0
  20. package/templates/runtime/react/settings-transfer.ts +6 -1
  21. package/templates/runtime/react/timeline-panel.test.tsx +14 -0
  22. package/templates/runtime/react/timeline-panel.tsx +44 -7
  23. package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
  24. package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
  25. package/templates/runtime/react/toolcraft-app.tsx +56 -37
  26. package/templates/runtime/schema/define-toolcraft.test.ts +266 -170
  27. package/templates/runtime/schema/define-toolcraft.ts +140 -246
  28. package/templates/runtime/schema/runtime-targets.ts +21 -0
  29. package/templates/runtime/schema/types.ts +44 -0
  30. package/templates/runtime/state/create-template-state.test.ts +156 -0
  31. package/templates/runtime/state/create-template-state.ts +38 -8
  32. package/templates/runtime/state/media-defaults.ts +105 -0
  33. package/templates/runtime/state/persistence.test.ts +58 -0
  34. package/templates/runtime/state/persistence.ts +105 -1
  35. package/templates/runtime/state/reducer.test.ts +280 -4
  36. package/templates/runtime/state/reducer.ts +195 -9
  37. package/templates/runtime/state/timeline-loop.test.ts +71 -0
  38. package/templates/runtime/state/timeline-loop.ts +35 -0
  39. package/templates/runtime/state/types.ts +27 -0
  40. package/templates/runtime/testing/performance.test.ts +810 -21
  41. package/templates/runtime/testing/performance.ts +823 -53
  42. package/templates/starter/AGENTS.md +24 -18
  43. package/templates/starter/docs/toolcraft/README.md +8 -4
  44. package/templates/starter/docs/toolcraft/acceptance-testing.md +43 -10
  45. package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
  46. package/templates/starter/docs/toolcraft/assembly-workflow.md +55 -20
  47. package/templates/starter/docs/toolcraft/component-rules.md +75 -42
  48. package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
  49. package/templates/starter/docs/toolcraft/performance.md +37 -11
  50. package/templates/starter/docs/toolcraft/schema-reference.md +171 -41
  51. package/templates/starter/docs/toolcraft/workflow.md +5 -2
  52. package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
  53. package/templates/starter/e2e/app-performance.spec.ts +55 -2
  54. package/templates/starter/e2e/performance-helpers.ts +45 -0
  55. package/templates/starter/gitignore +1 -0
  56. package/templates/starter/index.html +1 -0
  57. package/templates/starter/package.json +3 -0
  58. package/templates/starter/playwright.config.ts +1 -1
  59. package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
  60. package/templates/starter/scripts/run-vite-on-free-port.mjs +114 -13
  61. package/templates/starter/scripts/toolcraft-port.mjs +280 -0
  62. package/templates/starter/scripts/toolcraft-port.test.mjs +207 -1
  63. package/templates/starter/src/app/starter-acceptance.test.ts +3412 -479
  64. package/templates/starter/src/app/starter-acceptance.ts +1453 -97
  65. package/templates/starter/src/app/starter-performance.test.ts +111 -7
  66. package/templates/starter/src/app/starter-performance.ts +5 -0
  67. package/templates/starter/src/app/starter-schema.test.ts +32 -7
  68. package/templates/starter/src/app/starter-schema.ts +6 -2
  69. package/templates/starter/vite.config.ts +58 -2
  70. package/templates/ui/components/control-layout/index.tsx +8 -3
  71. package/templates/ui/components/controls/actions/actions-control.tsx +56 -5
  72. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  73. package/templates/ui/components/controls/color/index.ts +4 -1
  74. package/templates/ui/components/controls/color/palette-control.tsx +34 -4
  75. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  76. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  77. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
  78. package/templates/ui/components/controls/file-drop/index.ts +6 -1
  79. package/templates/ui/components/controls/index.ts +4 -0
  80. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  81. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
  82. package/templates/ui/components/controls/select/select-control.tsx +8 -25
  83. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  84. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  85. package/templates/ui/components/controls/vector/index.ts +1 -0
  86. package/templates/ui/components/controls/vector/vector-control.tsx +109 -12
  87. package/templates/ui/components/panel/panel-actions.tsx +1 -1
  88. package/templates/ui/components/panel/panel-section.tsx +29 -5
@@ -1,8 +1,19 @@
1
1
  import assert from "node:assert/strict";
2
+ import fs from "node:fs/promises";
2
3
  import net from "node:net";
4
+ import os from "node:os";
5
+ import path from "node:path";
3
6
  import test from "node:test";
4
7
 
5
- import { findAvailablePort } from "./toolcraft-port.mjs";
8
+ import {
9
+ findAvailablePort,
10
+ getListeningProcessIds,
11
+ killListeningProcessesOnPort,
12
+ readToolcraftAppIdentityFromHtml,
13
+ readSavedToolcraftPort,
14
+ waitForToolcraftAppOnPort,
15
+ writeSavedToolcraftPort,
16
+ } from "./toolcraft-port.mjs";
6
17
 
7
18
  function listen(host) {
8
19
  return new Promise((resolve, reject) => {
@@ -71,3 +82,198 @@ test("findAvailablePort skips ports occupied on IPv4 localhost", async (t) => {
71
82
  "A port occupied on 127.0.0.1 must be treated as unavailable for localhost URLs.",
72
83
  );
73
84
  });
85
+
86
+ test("saved Toolcraft port round-trips through local project state", async (t) => {
87
+ const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-port-"));
88
+ t.after(() => fs.rm(cwd, { force: true, recursive: true }));
89
+
90
+ assert.equal(await readSavedToolcraftPort(cwd), null);
91
+
92
+ await writeSavedToolcraftPort(4123, cwd);
93
+
94
+ assert.equal(await readSavedToolcraftPort(cwd), 4123);
95
+ });
96
+
97
+ test("getListeningProcessIds parses unique listening process ids", async () => {
98
+ const pids = await getListeningProcessIds(4123, async () => ({
99
+ stderr: "",
100
+ stdout: "123\n456\n123\nnot-a-pid\n",
101
+ }));
102
+
103
+ assert.deepEqual(pids, [123, 456]);
104
+ });
105
+
106
+ test("killListeningProcessesOnPort targets listeners found on the port", async () => {
107
+ const killed = [];
108
+ const pids = await killListeningProcessesOnPort(4123, {
109
+ execFileImpl: async () => ({ stderr: "", stdout: "123\n456\n" }),
110
+ killProcess: (pid, signal) => {
111
+ killed.push({ pid, signal });
112
+ },
113
+ });
114
+
115
+ assert.deepEqual(pids, [123, 456]);
116
+ assert.deepEqual(killed, [
117
+ { pid: 123, signal: "SIGTERM" },
118
+ { pid: 456, signal: "SIGTERM" },
119
+ ]);
120
+ });
121
+
122
+ test("killListeningProcessesOnPort supports forced restart cleanup", async () => {
123
+ const killed = [];
124
+ const pids = await killListeningProcessesOnPort(4123, {
125
+ execFileImpl: async () => ({ stderr: "", stdout: "789\n" }),
126
+ killProcess: (pid, signal) => {
127
+ killed.push({ pid, signal });
128
+ },
129
+ signal: "SIGKILL",
130
+ });
131
+
132
+ assert.deepEqual(pids, [789]);
133
+ assert.deepEqual(killed, [{ pid: 789, signal: "SIGKILL" }]);
134
+ });
135
+
136
+ test("readToolcraftAppIdentityFromHtml prefers the app title meta marker", () => {
137
+ assert.deepEqual(
138
+ readToolcraftAppIdentityFromHtml(`
139
+ <html>
140
+ <head>
141
+ <meta name="toolcraft-app-title" content="Generated App" />
142
+ <title>Other Title</title>
143
+ </head>
144
+ </html>
145
+ `),
146
+ {
147
+ appTitle: "Generated App",
148
+ title: "Other Title",
149
+ },
150
+ );
151
+ });
152
+
153
+ test("waitForToolcraftAppOnPort accepts the current app identity", async (t) => {
154
+ const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-port-identity-"));
155
+ t.after(() => fs.rm(cwd, { force: true, recursive: true }));
156
+ await fs.writeFile(
157
+ path.join(cwd, "index.html"),
158
+ '<meta name="toolcraft-app-title" content="Current App" /><title>Current App</title>',
159
+ );
160
+ const currentRoot = await fs.realpath(cwd);
161
+
162
+ const result = await waitForToolcraftAppOnPort(4321, {
163
+ cwd,
164
+ fetchImpl: async () => ({
165
+ ok: true,
166
+ text: async () => JSON.stringify({ appTitle: "Current App", root: currentRoot }),
167
+ }),
168
+ timeoutMs: 1,
169
+ });
170
+
171
+ assert.equal(result.url, "http://127.0.0.1:4321/");
172
+ });
173
+
174
+ test("waitForToolcraftAppOnPort verifies the current project root when the server identity endpoint exists", async (t) => {
175
+ const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-port-root-"));
176
+ t.after(() => fs.rm(cwd, { force: true, recursive: true }));
177
+ await fs.writeFile(
178
+ path.join(cwd, "index.html"),
179
+ '<meta name="toolcraft-app-title" content="Current App" /><title>Current App</title>',
180
+ );
181
+ const currentRoot = await fs.realpath(cwd);
182
+
183
+ const result = await waitForToolcraftAppOnPort(4321, {
184
+ cwd,
185
+ fetchImpl: async (url) => ({
186
+ ok: true,
187
+ text: async () =>
188
+ String(url).includes("/.toolcraft/server-identity.json")
189
+ ? JSON.stringify({ appTitle: "Current App", root: currentRoot })
190
+ : '<meta name="toolcraft-app-title" content="Different App" /><title>Different App</title>',
191
+ }),
192
+ timeoutMs: 1,
193
+ });
194
+
195
+ assert.equal(result.url, "http://127.0.0.1:4321/");
196
+ });
197
+
198
+ test("waitForToolcraftAppOnPort checks IPv6 localhost when IPv4 does not respond", async (t) => {
199
+ const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-port-ipv6-"));
200
+ t.after(() => fs.rm(cwd, { force: true, recursive: true }));
201
+ await fs.writeFile(
202
+ path.join(cwd, "index.html"),
203
+ '<meta name="toolcraft-app-title" content="Current App" /><title>Current App</title>',
204
+ );
205
+ const currentRoot = await fs.realpath(cwd);
206
+
207
+ const result = await waitForToolcraftAppOnPort(4321, {
208
+ cwd,
209
+ fetchImpl: async (url) => {
210
+ if (String(url).startsWith("http://127.0.0.1:4321")) {
211
+ throw new Error("fetch failed");
212
+ }
213
+
214
+ return {
215
+ ok: true,
216
+ text: async () =>
217
+ String(url).includes("/.toolcraft/server-identity.json")
218
+ ? JSON.stringify({ appTitle: "Current App", root: currentRoot })
219
+ : '<meta name="toolcraft-app-title" content="Current App" /><title>Current App</title>',
220
+ };
221
+ },
222
+ timeoutMs: 1,
223
+ });
224
+
225
+ assert.equal(result.url, "http://[::1]:4321/");
226
+ });
227
+
228
+ test("waitForToolcraftAppOnPort rejects a port serving another app", async (t) => {
229
+ const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-port-foreign-"));
230
+ t.after(() => fs.rm(cwd, { force: true, recursive: true }));
231
+ await fs.writeFile(
232
+ path.join(cwd, "index.html"),
233
+ '<meta name="toolcraft-app-title" content="Current App" /><title>Current App</title>',
234
+ );
235
+ const otherRoot = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-port-foreign-root-"));
236
+ t.after(() => fs.rm(otherRoot, { force: true, recursive: true }));
237
+ const otherRealRoot = await fs.realpath(otherRoot);
238
+
239
+ await assert.rejects(
240
+ () =>
241
+ waitForToolcraftAppOnPort(4321, {
242
+ cwd,
243
+ fetchImpl: async () => ({
244
+ ok: true,
245
+ text: async () => JSON.stringify({ appTitle: "Different App", root: otherRealRoot }),
246
+ }),
247
+ timeoutMs: 1,
248
+ }),
249
+ /Port 4321 is serving Different App at .* instead of this app Current App at /,
250
+ );
251
+ });
252
+
253
+ test("waitForToolcraftAppOnPort rejects a matching title from a different project root", async (t) => {
254
+ const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-port-root-current-"));
255
+ const otherRoot = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-port-root-other-"));
256
+ t.after(() => fs.rm(cwd, { force: true, recursive: true }));
257
+ t.after(() => fs.rm(otherRoot, { force: true, recursive: true }));
258
+ await fs.writeFile(
259
+ path.join(cwd, "index.html"),
260
+ '<meta name="toolcraft-app-title" content="Current App" /><title>Current App</title>',
261
+ );
262
+ const otherRealRoot = await fs.realpath(otherRoot);
263
+
264
+ await assert.rejects(
265
+ () =>
266
+ waitForToolcraftAppOnPort(4321, {
267
+ cwd,
268
+ fetchImpl: async (url) => ({
269
+ ok: true,
270
+ text: async () =>
271
+ String(url).includes("/.toolcraft/server-identity.json")
272
+ ? JSON.stringify({ appTitle: "Current App", root: otherRealRoot })
273
+ : '<meta name="toolcraft-app-title" content="Current App" /><title>Current App</title>',
274
+ }),
275
+ timeoutMs: 1,
276
+ }),
277
+ /Port 4321 is serving Current App at .* instead of this app Current App at /,
278
+ );
279
+ });