@alloy-js/core 0.19.0-dev.1 → 0.19.0-dev.12

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 (100) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/src/components/AppendFile.d.ts +90 -0
  3. package/dist/src/components/AppendFile.d.ts.map +1 -0
  4. package/dist/src/components/AppendFile.js +226 -0
  5. package/dist/src/components/CopyFile.d.ts +12 -0
  6. package/dist/src/components/CopyFile.d.ts.map +1 -0
  7. package/dist/src/components/CopyFile.js +15 -0
  8. package/dist/src/components/TemplateFile.d.ts +84 -0
  9. package/dist/src/components/TemplateFile.d.ts.map +1 -0
  10. package/dist/src/components/TemplateFile.js +133 -0
  11. package/dist/src/components/UpdateFile.d.ts +34 -0
  12. package/dist/src/components/UpdateFile.d.ts.map +1 -0
  13. package/dist/src/components/UpdateFile.js +66 -0
  14. package/dist/src/components/index.d.ts +4 -0
  15. package/dist/src/components/index.d.ts.map +1 -1
  16. package/dist/src/components/index.js +4 -0
  17. package/dist/src/components/stc/index.d.ts +4 -0
  18. package/dist/src/components/stc/index.d.ts.map +1 -1
  19. package/dist/src/components/stc/index.js +4 -0
  20. package/dist/src/context/source-directory.d.ts +3 -3
  21. package/dist/src/context/source-directory.d.ts.map +1 -1
  22. package/dist/src/context/source-file.d.ts +4 -0
  23. package/dist/src/context/source-file.d.ts.map +1 -1
  24. package/dist/src/debug.d.ts.map +1 -1
  25. package/dist/src/debug.js +4 -1
  26. package/dist/src/host/alloy-host.browser.d.ts +11 -0
  27. package/dist/src/host/alloy-host.browser.d.ts.map +1 -0
  28. package/dist/src/host/alloy-host.browser.js +31 -0
  29. package/dist/src/host/alloy-host.d.ts +11 -0
  30. package/dist/src/host/alloy-host.d.ts.map +1 -0
  31. package/dist/src/host/alloy-host.js +143 -0
  32. package/dist/src/host/interface.d.ts +144 -0
  33. package/dist/src/host/interface.d.ts.map +1 -0
  34. package/dist/src/host/interface.js +1 -0
  35. package/dist/src/index.browser.d.ts +1 -1
  36. package/dist/src/index.browser.d.ts.map +1 -1
  37. package/dist/src/index.browser.js +2 -2
  38. package/dist/src/reactivity.js +2 -2
  39. package/dist/src/render.d.ts +10 -2
  40. package/dist/src/render.d.ts.map +1 -1
  41. package/dist/src/render.js +20 -1
  42. package/dist/src/resource.d.ts +80 -0
  43. package/dist/src/resource.d.ts.map +1 -0
  44. package/dist/src/resource.js +118 -0
  45. package/dist/src/scheduler.d.ts +6 -0
  46. package/dist/src/scheduler.d.ts.map +1 -1
  47. package/dist/src/scheduler.js +36 -0
  48. package/dist/src/write-output.d.ts +1 -1
  49. package/dist/src/write-output.d.ts.map +1 -1
  50. package/dist/src/write-output.js +40 -21
  51. package/dist/test/components/append-file.test.d.ts +2 -0
  52. package/dist/test/components/append-file.test.d.ts.map +1 -0
  53. package/dist/test/components/append-file.test.js +281 -0
  54. package/dist/test/components/copy-file.test.d.ts +2 -0
  55. package/dist/test/components/copy-file.test.d.ts.map +1 -0
  56. package/dist/test/components/copy-file.test.js +94 -0
  57. package/dist/test/components/source-file.test.d.ts.map +1 -1
  58. package/dist/test/components/template-file.test.d.ts +2 -0
  59. package/dist/test/components/template-file.test.d.ts.map +1 -0
  60. package/dist/test/components/template-file.test.js +133 -0
  61. package/dist/test/components/update-file.test.d.ts +2 -0
  62. package/dist/test/components/update-file.test.d.ts.map +1 -0
  63. package/dist/test/components/update-file.test.js +169 -0
  64. package/dist/test/rendering/formatting.test.d.ts.map +1 -1
  65. package/dist/testing/extend-expect.d.ts +0 -1
  66. package/dist/testing/extend-expect.d.ts.map +1 -1
  67. package/dist/testing/extend-expect.js +60 -55
  68. package/dist/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +3 -3
  70. package/src/components/AppendFile.tsx +294 -0
  71. package/src/components/CopyFile.tsx +29 -0
  72. package/src/components/TemplateFile.tsx +193 -0
  73. package/src/components/UpdateFile.tsx +86 -0
  74. package/src/components/index.tsx +4 -0
  75. package/src/components/stc/index.ts +4 -0
  76. package/src/context/source-directory.ts +5 -3
  77. package/src/context/source-file.ts +5 -0
  78. package/src/debug.ts +4 -1
  79. package/src/host/alloy-host.browser.ts +56 -0
  80. package/src/host/alloy-host.ts +160 -0
  81. package/src/host/interface.ts +153 -0
  82. package/src/index.browser.ts +1 -1
  83. package/src/reactivity.ts +2 -2
  84. package/src/render.ts +44 -5
  85. package/src/resource.ts +152 -0
  86. package/src/scheduler.ts +39 -0
  87. package/src/write-output.ts +49 -19
  88. package/temp/api.json +2009 -546
  89. package/test/components/append-file.test.tsx +275 -0
  90. package/test/components/copy-file.test.tsx +98 -0
  91. package/test/components/source-file.test.tsx +5 -2
  92. package/test/components/template-file.test.tsx +127 -0
  93. package/test/components/update-file.test.tsx +214 -0
  94. package/test/rendering/formatting.test.tsx +9 -3
  95. package/testing/extend-expect.ts +74 -59
  96. package/testing/vitest.d.ts +4 -0
  97. package/dist/src/write-output.browser.d.ts +0 -2
  98. package/dist/src/write-output.browser.d.ts.map +0 -1
  99. package/dist/src/write-output.browser.js +0 -4
  100. package/src/write-output.browser.ts +0 -4
@@ -0,0 +1,214 @@
1
+ import { existsSync, unlinkSync, writeFileSync } from "fs";
2
+ import { tmpdir } from "os";
3
+ import { join } from "pathe";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { UpdateFile } from "../../src/components/UpdateFile.jsx";
6
+ import { render } from "../../src/render.js";
7
+ import "../../testing/extend-expect.js";
8
+ import { d } from "../../testing/render.js";
9
+
10
+ describe("UpdateFile", () => {
11
+ let testFilePath: string;
12
+
13
+ beforeEach(() => {
14
+ testFilePath = join(tmpdir(), "test-file.txt");
15
+ });
16
+
17
+ afterEach(() => {
18
+ if (existsSync(testFilePath)) {
19
+ unlinkSync(testFilePath);
20
+ }
21
+ });
22
+
23
+ describe("when file exists", () => {
24
+ it("should read existing file and transform content", async () => {
25
+ const existingContent = "Hello World";
26
+ writeFileSync(testFilePath, existingContent, "utf-8");
27
+
28
+ const result = (
29
+ <UpdateFile path={testFilePath}>
30
+ {(currentContents) => {
31
+ return `${currentContents?.toUpperCase()}`;
32
+ }}
33
+ </UpdateFile>
34
+ );
35
+
36
+ await expect(result).toRenderToAsync("HELLO WORLD");
37
+ });
38
+
39
+ it("should handle JSON file updates", async () => {
40
+ const existingConfig = { version: "1.0.0", name: "test" };
41
+ writeFileSync(testFilePath, JSON.stringify(existingConfig), "utf-8");
42
+
43
+ const result = (
44
+ <UpdateFile path={testFilePath}>
45
+ {(currentContents) => {
46
+ const config = JSON.parse(currentContents!);
47
+ config.version = "2.0.0";
48
+ config.newProperty = "added";
49
+ return JSON.stringify(config, null, 2);
50
+ }}
51
+ </UpdateFile>
52
+ );
53
+
54
+ await expect(result).toRenderToAsync(d`
55
+ {
56
+ "version": "2.0.0",
57
+ "name": "test",
58
+ "newProperty": "added"
59
+ }
60
+ `);
61
+ });
62
+
63
+ it("should handle multiline content transformation", async () => {
64
+ const existingContent = d`
65
+ line 1
66
+ line 2
67
+ line 3
68
+ `;
69
+ writeFileSync(testFilePath, existingContent, "utf-8");
70
+
71
+ const result = (
72
+ <UpdateFile path={testFilePath}>
73
+ {(currentContents) => {
74
+ const lines = currentContents!.split("\n");
75
+ const modifiedLines = lines.map((line) => `// ${line}`);
76
+ return modifiedLines.join("\n");
77
+ }}
78
+ </UpdateFile>
79
+ );
80
+
81
+ await expect(result).toRenderToAsync(d`
82
+ // line 1
83
+ // line 2
84
+ // line 3
85
+ `);
86
+ });
87
+
88
+ it("should handle empty existing file", async () => {
89
+ writeFileSync(testFilePath, "", "utf-8");
90
+
91
+ const result = (
92
+ <UpdateFile path={testFilePath}>
93
+ {(currentContents) => {
94
+ return `Content was: "${currentContents || "empty"}"`;
95
+ }}
96
+ </UpdateFile>
97
+ );
98
+
99
+ await expect(result).toRenderToAsync('Content was: "empty"');
100
+ });
101
+ });
102
+
103
+ describe("when file does not exist", () => {
104
+ it("should use defaultContent when file does not exist", async () => {
105
+ const result = (
106
+ <UpdateFile path="non-existent.txt" defaultContent="default content">
107
+ {(currentContents) => {
108
+ if (currentContents === null) {
109
+ return `File did not exist`;
110
+ }
111
+ return `File existed with: ${currentContents}`;
112
+ }}
113
+ </UpdateFile>
114
+ );
115
+
116
+ await expect(result).toRenderToAsync("File did not exist");
117
+ });
118
+
119
+ it("should use defaultContentPath when file does not exist", async () => {
120
+ const defaultFilePath = join(
121
+ tmpdir(),
122
+ `default-content-${Date.now()}.txt`,
123
+ );
124
+ const defaultContent = "This is default content from file";
125
+ writeFileSync(defaultFilePath, defaultContent, "utf-8");
126
+
127
+ try {
128
+ const result = (
129
+ <UpdateFile
130
+ path="non-existent.txt"
131
+ defaultContentPath={defaultFilePath}
132
+ >
133
+ {(currentContents) => {
134
+ return `Default: ${currentContents}`;
135
+ }}
136
+ </UpdateFile>
137
+ );
138
+
139
+ await expect(result).toRenderToAsync(
140
+ "Default: This is default content from file",
141
+ );
142
+ } finally {
143
+ if (existsSync(defaultFilePath)) {
144
+ unlinkSync(defaultFilePath);
145
+ }
146
+ }
147
+ });
148
+
149
+ it("should pass null to children when no default content provided", async () => {
150
+ const result = (
151
+ <UpdateFile path="non-existent.txt">
152
+ {(currentContents) => {
153
+ if (currentContents === null) {
154
+ return `Creating new file`;
155
+ }
156
+ return `Updating existing file: ${currentContents}`;
157
+ }}
158
+ </UpdateFile>
159
+ );
160
+
161
+ await expect(result).toRenderToAsync("Creating new file");
162
+ });
163
+
164
+ it("should create JSON file from scratch when it doesn't exist", async () => {
165
+ const result = (
166
+ <UpdateFile path="new-config.json">
167
+ {(currentContents) => {
168
+ const config = currentContents ? JSON.parse(currentContents) : {};
169
+ config.name = "new-project";
170
+ config.version = "1.0.0";
171
+ config.dependencies = {};
172
+ return JSON.stringify(config, null, 2);
173
+ }}
174
+ </UpdateFile>
175
+ );
176
+
177
+ await expect(result).toRenderToAsync(d`
178
+ {
179
+ "name": "new-project",
180
+ "version": "1.0.0",
181
+ "dependencies": {}
182
+ }
183
+ `);
184
+ });
185
+ });
186
+
187
+ describe("error handling", () => {
188
+ it("should throw error when defaultContentPath file does not exist", async () => {
189
+ expect(() =>
190
+ render(
191
+ <UpdateFile
192
+ path="non-existent.txt"
193
+ defaultContentPath="/path/that/does/not/exist.txt"
194
+ >
195
+ {(currentContents) => currentContents}
196
+ </UpdateFile>,
197
+ ),
198
+ ).toThrow();
199
+ });
200
+
201
+ it("should handle file system errors gracefully", async () => {
202
+ // Try to read a directory as a file (should cause an error)
203
+ const dirPath = join(tmpdir(), `test-dir-${Date.now()}`);
204
+
205
+ expect(() =>
206
+ render(
207
+ <UpdateFile path={dirPath}>
208
+ {(currentContents) => currentContents}
209
+ </UpdateFile>,
210
+ ),
211
+ ).toThrow();
212
+ });
213
+ });
214
+ });
@@ -1,5 +1,11 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { For, Output, render, SourceFile } from "../../src/index.js";
2
+ import {
3
+ ContentOutputFile,
4
+ For,
5
+ Output,
6
+ render,
7
+ SourceFile,
8
+ } from "../../src/index.js";
3
9
  import "../../testing/extend-expect.js";
4
10
  import { d } from "../../testing/render.js";
5
11
 
@@ -454,7 +460,7 @@ it("formats based on the output component props", () => {
454
460
  );
455
461
 
456
462
  const tree = render(template);
457
- expect(tree.contents[0].contents).toEqual(d`
463
+ expect((tree.contents[0] as ContentOutputFile).contents).toEqual(d`
458
464
  1
459
465
  2
460
466
  3
@@ -476,7 +482,7 @@ it("formats based on the source file component props", () => {
476
482
  );
477
483
 
478
484
  const tree = render(template);
479
- expect(tree.contents[0].contents).toEqual(d`
485
+ expect((tree.contents[0] as ContentOutputFile).contents).toEqual(d`
480
486
  1
481
487
  2
482
488
  3
@@ -7,8 +7,8 @@ import {
7
7
  } from "@alloy-js/core";
8
8
  import "vitest";
9
9
  import { expect } from "vitest";
10
+ import { flushJobs, flushJobsAsync } from "../src/scheduler.js";
10
11
  import { dedent } from "./render.js";
11
- import "./vitest.d.ts";
12
12
 
13
13
  interface ToRenderToOptions {
14
14
  printWidth?: number;
@@ -22,67 +22,82 @@ expect.extend({
22
22
  expectedRaw: string | Record<string, string>,
23
23
  renderOptions?: ToRenderToOptions,
24
24
  ) {
25
- const message = () => `Render is${isNot ? " not" : ""} incorrect`;
25
+ const tree = renderTree(received);
26
+ flushJobs();
27
+ const actual = getFilesFromTree(tree, renderOptions);
26
28
 
27
- const { isNot } = this;
28
- const actual = renderAsFiles(received, renderOptions);
29
- if (typeof expectedRaw === "string") {
30
- const expected = dedent(expectedRaw);
31
- let actualStr;
32
- if (typeof actual === "string") {
33
- actualStr = actual;
34
- } else if (Object.keys(actual).length === 1) {
35
- // If we have a single file, we can use its content directly.
36
- actualStr = Object.values(actual)[0];
37
- } else {
38
- return {
39
- pass: false,
40
- message,
41
- actual,
42
- expected,
43
- };
44
- }
45
- return {
46
- pass: actualStr === expected,
47
- message,
48
- actual: actualStr,
49
- expected,
50
- };
51
- } else if (typeof actual === "object" && typeof expectedRaw === "object") {
52
- const expected = expectedRaw;
53
- const dedentExpected: Record<string, string> = {};
54
- for (const [key, value] of Object.entries(expected)) {
55
- dedentExpected[key] = dedent(value);
56
- }
57
- const pass =
58
- Object.keys(actual).length === Object.keys(expected).length &&
59
- Object.entries(actual).every(([key, value]) => {
60
- return dedentExpected[key] === value;
61
- });
62
- return {
63
- pass,
64
- message,
65
- actual,
66
- expected: dedentExpected,
67
- };
29
+ return validateRender(actual, expectedRaw, this.isNot);
30
+ },
31
+ async toRenderToAsync(
32
+ received: Children,
33
+ expectedRaw: string | Record<string, string>,
34
+ renderOptions?: ToRenderToOptions,
35
+ ) {
36
+ const tree = renderTree(received);
37
+ await flushJobsAsync();
38
+ const actual = getFilesFromTree(tree, renderOptions);
39
+ return validateRender(actual, expectedRaw, this.isNot);
40
+ },
41
+ });
42
+
43
+ function validateRender(
44
+ actual: string | Record<string, string>,
45
+ expectedRaw: string | Record<string, string>,
46
+ isNot: boolean,
47
+ ) {
48
+ const message = () => `Render is${isNot ? " not" : ""} incorrect`;
49
+
50
+ if (typeof expectedRaw === "string") {
51
+ const expected = dedent(expectedRaw);
52
+ let actualStr;
53
+ if (typeof actual === "string") {
54
+ actualStr = actual;
55
+ } else if (Object.keys(actual).length === 1) {
56
+ // If we have a single file, we can use its content directly.
57
+ actualStr = Object.values(actual)[0];
68
58
  } else {
69
59
  return {
70
60
  pass: false,
71
61
  message,
72
62
  actual,
73
- expected: expectedRaw,
63
+ expected,
74
64
  };
75
65
  }
76
- },
77
- });
66
+ return {
67
+ pass: actualStr === expected,
68
+ message,
69
+ actual: actualStr,
70
+ expected,
71
+ };
72
+ } else if (typeof actual === "object" && typeof expectedRaw === "object") {
73
+ const expected = expectedRaw;
74
+ const dedentExpected: Record<string, string> = {};
75
+ for (const [key, value] of Object.entries(expected)) {
76
+ dedentExpected[key] = dedent(value);
77
+ }
78
+ const pass =
79
+ Object.keys(actual).length === Object.keys(expected).length &&
80
+ Object.entries(actual).every(([key, value]) => {
81
+ return dedentExpected[key] === value;
82
+ });
83
+ return {
84
+ pass,
85
+ message,
86
+ actual,
87
+ expected: dedentExpected,
88
+ };
89
+ } else {
90
+ return {
91
+ pass: false,
92
+ message,
93
+ actual,
94
+ expected: expectedRaw,
95
+ };
96
+ }
97
+ }
78
98
 
79
- function renderAsFiles(
80
- received: Children,
81
- options?: ToRenderToOptions,
82
- ): Record<string, string> | string {
99
+ function getFilesFromTree(tree: RenderedTextTree, options?: ToRenderToOptions) {
83
100
  const files: Record<string, string> = {};
84
- const tree = renderTree(received);
85
-
86
101
  // when passing Output, the first render tree child is the Output component.
87
102
  const rootRenderOptions =
88
103
  Array.isArray(tree) ?
@@ -91,6 +106,13 @@ function renderAsFiles(
91
106
  : {};
92
107
 
93
108
  collectSourceFiles(tree);
109
+ // If we found no source files, we return the tree as a string.
110
+ if (Object.keys(files).length === 0) {
111
+ return printTree(tree, options);
112
+ } else {
113
+ return files;
114
+ }
115
+
94
116
  function collectSourceFiles(root: RenderedTextTree) {
95
117
  if (!Array.isArray(root)) {
96
118
  return;
@@ -121,11 +143,4 @@ function renderAsFiles(
121
143
  }
122
144
  }
123
145
  }
124
-
125
- // If we found no source files, we return the tree as a string.
126
- if (Object.keys(files).length === 0) {
127
- return printTree(tree, options);
128
- } else {
129
- return files;
130
- }
131
146
  }
@@ -11,6 +11,10 @@ interface CustomMatchers<R = unknown> {
11
11
  str: string | Record<string, string>,
12
12
  options?: ToRenderToRenderOptions,
13
13
  ) => R;
14
+ toRenderToAsync: (
15
+ str: string | Record<string, string>,
16
+ options?: ToRenderToRenderOptions,
17
+ ) => Promise<R>;
14
18
  }
15
19
 
16
20
  declare module "vitest" {
@@ -1,2 +0,0 @@
1
- export declare function writeOutput(output: any, basePath?: string): void;
2
- //# sourceMappingURL=write-output.browser.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"write-output.browser.d.ts","sourceRoot":"","sources":["../../src/write-output.browser.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAE,MAAW,QAG7D"}
@@ -1,4 +0,0 @@
1
- export function writeOutput(output, basePath = "") {
2
- // eslint-disable-next-line no-console
3
- console.warn("writeOutput is not supported in a browser environment.");
4
- }
@@ -1,4 +0,0 @@
1
- export function writeOutput(output: any, basePath: string = "") {
2
- // eslint-disable-next-line no-console
3
- console.warn("writeOutput is not supported in a browser environment.");
4
- }