@alloy-js/core 0.19.0-dev.3 → 0.20.0-dev.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 +22 -0
- package/dist/src/components/AppendFile.d.ts +90 -0
- package/dist/src/components/AppendFile.d.ts.map +1 -0
- package/dist/src/components/AppendFile.js +226 -0
- package/dist/src/components/CopyFile.d.ts +12 -0
- package/dist/src/components/CopyFile.d.ts.map +1 -0
- package/dist/src/components/CopyFile.js +15 -0
- package/dist/src/components/TemplateFile.d.ts +84 -0
- package/dist/src/components/TemplateFile.d.ts.map +1 -0
- package/dist/src/components/TemplateFile.js +133 -0
- package/dist/src/components/UpdateFile.d.ts +34 -0
- package/dist/src/components/UpdateFile.d.ts.map +1 -0
- package/dist/src/components/UpdateFile.js +66 -0
- package/dist/src/components/index.d.ts +4 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/components/index.js +4 -0
- package/dist/src/components/stc/index.d.ts +4 -0
- package/dist/src/components/stc/index.d.ts.map +1 -1
- package/dist/src/components/stc/index.js +4 -0
- package/dist/src/context/source-directory.d.ts +3 -3
- package/dist/src/context/source-directory.d.ts.map +1 -1
- package/dist/src/context/source-file.d.ts +4 -0
- package/dist/src/context/source-file.d.ts.map +1 -1
- package/dist/src/debug.d.ts.map +1 -1
- package/dist/src/debug.js +4 -1
- package/dist/src/host/alloy-host.browser.d.ts +11 -0
- package/dist/src/host/alloy-host.browser.d.ts.map +1 -0
- package/dist/src/host/alloy-host.browser.js +31 -0
- package/dist/src/host/alloy-host.d.ts +11 -0
- package/dist/src/host/alloy-host.d.ts.map +1 -0
- package/dist/src/host/alloy-host.js +143 -0
- package/dist/src/host/interface.d.ts +144 -0
- package/dist/src/host/interface.d.ts.map +1 -0
- package/dist/src/host/interface.js +1 -0
- package/dist/src/index.browser.d.ts +1 -1
- package/dist/src/index.browser.d.ts.map +1 -1
- package/dist/src/index.browser.js +2 -2
- package/dist/src/render.d.ts +10 -2
- package/dist/src/render.d.ts.map +1 -1
- package/dist/src/render.js +20 -1
- package/dist/src/resource.d.ts +80 -0
- package/dist/src/resource.d.ts.map +1 -0
- package/dist/src/resource.js +118 -0
- package/dist/src/scheduler.d.ts +6 -0
- package/dist/src/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler.js +36 -0
- package/dist/src/write-output.d.ts +1 -1
- package/dist/src/write-output.d.ts.map +1 -1
- package/dist/src/write-output.js +40 -21
- package/dist/test/components/append-file.test.d.ts +2 -0
- package/dist/test/components/append-file.test.d.ts.map +1 -0
- package/dist/test/components/append-file.test.js +281 -0
- package/dist/test/components/copy-file.test.d.ts +2 -0
- package/dist/test/components/copy-file.test.d.ts.map +1 -0
- package/dist/test/components/copy-file.test.js +94 -0
- package/dist/test/components/source-file.test.d.ts.map +1 -1
- package/dist/test/components/template-file.test.d.ts +2 -0
- package/dist/test/components/template-file.test.d.ts.map +1 -0
- package/dist/test/components/template-file.test.js +133 -0
- package/dist/test/components/update-file.test.d.ts +2 -0
- package/dist/test/components/update-file.test.d.ts.map +1 -0
- package/dist/test/components/update-file.test.js +169 -0
- package/dist/test/rendering/formatting.test.d.ts.map +1 -1
- package/dist/testing/extend-expect.js +60 -54
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/components/AppendFile.tsx +294 -0
- package/src/components/CopyFile.tsx +29 -0
- package/src/components/TemplateFile.tsx +193 -0
- package/src/components/UpdateFile.tsx +86 -0
- package/src/components/index.tsx +4 -0
- package/src/components/stc/index.ts +4 -0
- package/src/context/source-directory.ts +5 -3
- package/src/context/source-file.ts +5 -0
- package/src/debug.ts +4 -1
- package/src/host/alloy-host.browser.ts +56 -0
- package/src/host/alloy-host.ts +160 -0
- package/src/host/interface.ts +153 -0
- package/src/index.browser.ts +1 -1
- package/src/render.ts +44 -5
- package/src/resource.ts +152 -0
- package/src/scheduler.ts +39 -0
- package/src/write-output.ts +49 -19
- package/temp/api.json +2009 -546
- package/test/components/append-file.test.tsx +275 -0
- package/test/components/copy-file.test.tsx +98 -0
- package/test/components/source-file.test.tsx +5 -2
- package/test/components/template-file.test.tsx +127 -0
- package/test/components/update-file.test.tsx +214 -0
- package/test/rendering/formatting.test.tsx +9 -3
- package/testing/extend-expect.ts +74 -58
- package/testing/vitest.d.ts +4 -0
- package/dist/src/write-output.browser.d.ts +0 -2
- package/dist/src/write-output.browser.d.ts.map +0 -1
- package/dist/src/write-output.browser.js +0 -4
- package/src/write-output.browser.ts +0 -4
package/src/debug.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
1
|
import { isReactive } from "@vue/reactivity";
|
|
3
2
|
import Table from "cli-table3";
|
|
4
3
|
import pc from "picocolors";
|
|
@@ -19,12 +18,15 @@ const debug: DebugInterface = {
|
|
|
19
18
|
component: {
|
|
20
19
|
stack: debugStack,
|
|
21
20
|
tree() {
|
|
21
|
+
//eslint-disable-next-line no-console
|
|
22
22
|
console.log("tree");
|
|
23
23
|
},
|
|
24
24
|
watch() {
|
|
25
|
+
//eslint-disable-next-line no-console
|
|
25
26
|
console.log("watch");
|
|
26
27
|
},
|
|
27
28
|
render() {
|
|
29
|
+
//eslint-disable-next-line no-console
|
|
28
30
|
console.log("render");
|
|
29
31
|
},
|
|
30
32
|
context: debugContext,
|
|
@@ -78,6 +80,7 @@ function debugStack() {
|
|
|
78
80
|
function debugContext() {
|
|
79
81
|
let currentContext = getContext();
|
|
80
82
|
while (currentContext !== null) {
|
|
83
|
+
//eslint-disable-next-line no-console
|
|
81
84
|
console.log(printContext(currentContext));
|
|
82
85
|
currentContext = currentContext.owner;
|
|
83
86
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AlloyFileInterface, AlloyHostInterface } from "./interface.js";
|
|
2
|
+
|
|
3
|
+
export const AlloyHost: AlloyHostInterface = {
|
|
4
|
+
read(source: string): AlloyFileInterface {
|
|
5
|
+
return new AlloyFile(source);
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
async write(
|
|
9
|
+
destination: string,
|
|
10
|
+
content: string | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"File system write operations are not supported in the browser",
|
|
14
|
+
);
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async exists(source: string): Promise<boolean> {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"File system exists operations are not supported in the browser",
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async mkdir(path: string): Promise<void> {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"File system mkdir operations are not supported in the browser",
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export class AlloyFile implements AlloyFileInterface {
|
|
31
|
+
constructor(private source: string) {}
|
|
32
|
+
|
|
33
|
+
async text(): Promise<string> {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"File system read operations are not supported in the browser",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async arrayBuffer(): Promise<ArrayBuffer> {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"File system read operations are not supported in the browser",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async bytes(): Promise<Uint8Array> {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"File system read operations are not supported in the browser",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
stream(): ReadableStream<Uint8Array> {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"File system read operations are not supported in the browser",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from "fs";
|
|
2
|
+
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { AlloyFileInterface, AlloyHostInterface } from "./interface.js";
|
|
4
|
+
|
|
5
|
+
export const AlloyHost: AlloyHostInterface = {
|
|
6
|
+
read(source: string): AlloyFileInterface {
|
|
7
|
+
return new AlloyFile(source);
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
async write(
|
|
11
|
+
destination: string,
|
|
12
|
+
content: string | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
if (typeof content === "string") {
|
|
15
|
+
//eslint-disable-next-line no-useless-catch
|
|
16
|
+
try {
|
|
17
|
+
await writeFile(destination, content, "utf8");
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// get good callstacks
|
|
20
|
+
throw e;
|
|
21
|
+
}
|
|
22
|
+
} else if (content instanceof ArrayBuffer) {
|
|
23
|
+
//eslint-disable-next-line no-useless-catch
|
|
24
|
+
try {
|
|
25
|
+
await writeFile(destination, new Uint8Array(content));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// get good callstacks
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
} else if (content instanceof Uint8Array) {
|
|
31
|
+
//eslint-disable-next-line no-useless-catch
|
|
32
|
+
try {
|
|
33
|
+
await writeFile(destination, content);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// get good callstacks
|
|
36
|
+
throw e;
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
// content is ReadableStream<Uint8Array>
|
|
40
|
+
//eslint-disable-next-line no-useless-catch
|
|
41
|
+
try {
|
|
42
|
+
const writeStream = createWriteStream(destination);
|
|
43
|
+
const reader = content.getReader();
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
while (true) {
|
|
47
|
+
const { done, value } = await reader.read();
|
|
48
|
+
if (done) break;
|
|
49
|
+
|
|
50
|
+
await new Promise<void>((resolve, reject) => {
|
|
51
|
+
writeStream.write(value, (err) => {
|
|
52
|
+
if (err) reject(err);
|
|
53
|
+
else resolve();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
reader.releaseLock();
|
|
59
|
+
await new Promise<void>((resolve, reject) => {
|
|
60
|
+
writeStream.end((err?: Error) => {
|
|
61
|
+
if (err) reject(err);
|
|
62
|
+
else resolve();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
// get good callstacks
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async exists(source: string): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
await access(source);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async mkdir(path: string): Promise<void> {
|
|
83
|
+
//eslint-disable-next-line no-useless-catch
|
|
84
|
+
try {
|
|
85
|
+
await mkdir(path, { recursive: true });
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// get good callstacks
|
|
88
|
+
throw e;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export class AlloyFile implements AlloyFileInterface {
|
|
94
|
+
constructor(private source: string) {}
|
|
95
|
+
|
|
96
|
+
async text(): Promise<string> {
|
|
97
|
+
//eslint-disable-next-line no-useless-catch
|
|
98
|
+
try {
|
|
99
|
+
return await readFile(this.source, "utf8");
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// get good callstacks
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async arrayBuffer(): Promise<ArrayBuffer> {
|
|
107
|
+
//eslint-disable-next-line no-useless-catch
|
|
108
|
+
try {
|
|
109
|
+
const buffer = await readFile(this.source);
|
|
110
|
+
return buffer.buffer.slice(
|
|
111
|
+
buffer.byteOffset,
|
|
112
|
+
buffer.byteOffset + buffer.byteLength,
|
|
113
|
+
);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// get good callstacks
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async bytes(): Promise<Uint8Array> {
|
|
121
|
+
//eslint-disable-next-line no-useless-catch
|
|
122
|
+
try {
|
|
123
|
+
const buffer = await readFile(this.source);
|
|
124
|
+
return new Uint8Array(buffer);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// get good callstacks
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
stream(): ReadableStream<Uint8Array> {
|
|
132
|
+
//eslint-disable-next-line no-useless-catch
|
|
133
|
+
try {
|
|
134
|
+
const nodeStream = createReadStream(this.source);
|
|
135
|
+
|
|
136
|
+
return new ReadableStream<Uint8Array>({
|
|
137
|
+
start(controller) {
|
|
138
|
+
nodeStream.on("data", (chunk) => {
|
|
139
|
+
controller.enqueue(new Uint8Array(chunk as Buffer));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
nodeStream.on("end", () => {
|
|
143
|
+
controller.close();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
nodeStream.on("error", (err) => {
|
|
147
|
+
controller.error(err);
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
cancel() {
|
|
152
|
+
nodeStream.destroy();
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// get good callstacks
|
|
157
|
+
throw e;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for the Alloy host file system operations.
|
|
3
|
+
*
|
|
4
|
+
* This interface abstracts file system operations to allow different
|
|
5
|
+
* implementations across different environments (Node.js, browser, etc.).
|
|
6
|
+
*/
|
|
7
|
+
export interface AlloyHostInterface {
|
|
8
|
+
/**
|
|
9
|
+
* Read a file from the file system.
|
|
10
|
+
*
|
|
11
|
+
* @param source - The path to the file to read
|
|
12
|
+
* @returns An AlloyFileInterface for accessing the file content in different formats
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const file = AlloyHost.read('./config.json');
|
|
17
|
+
* const content = await file.text();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
read(source: string): AlloyFileInterface;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Write content to a file in the file system.
|
|
24
|
+
*
|
|
25
|
+
* Supports writing different types of content including strings, binary data,
|
|
26
|
+
* and streams. For strings, content is written with UTF-8 encoding.
|
|
27
|
+
*
|
|
28
|
+
* @param destination - The path where the file should be written
|
|
29
|
+
* @param content - The content to write (string, ArrayBuffer, Uint8Array, or ReadableStream)
|
|
30
|
+
* @returns A Promise that resolves when the write operation is complete
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* // Write a string
|
|
35
|
+
* await AlloyHost.write('./output.txt', 'Hello, world!');
|
|
36
|
+
*
|
|
37
|
+
* // Write binary data
|
|
38
|
+
* const data = new Uint8Array([72, 101, 108, 108, 111]);
|
|
39
|
+
* await AlloyHost.write('./binary.dat', data);
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
write(
|
|
43
|
+
destination: string,
|
|
44
|
+
content: string | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>,
|
|
45
|
+
): Promise<void>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a file or directory exists at the given path.
|
|
49
|
+
*
|
|
50
|
+
* @param source - The path to check for existence
|
|
51
|
+
* @returns A Promise that resolves to true if the path exists, false otherwise
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const fileExists = await AlloyHost.exists('./config.json');
|
|
56
|
+
* if (fileExists) {
|
|
57
|
+
* // File exists, safe to read
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
exists(source: string): Promise<boolean>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a directory at the specified path.
|
|
65
|
+
*
|
|
66
|
+
* Creates the directory and any necessary parent directories recursively.
|
|
67
|
+
* If the directory already exists, this operation succeeds without error.
|
|
68
|
+
*
|
|
69
|
+
* @param path - The path of the directory to create
|
|
70
|
+
* @returns A Promise that resolves when the directory creation is complete
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* await AlloyHost.mkdir('./output/nested/directory');
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
mkdir(path: string): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Interface for reading file content in different formats.
|
|
82
|
+
*
|
|
83
|
+
* This interface provides multiple ways to access the same file content,
|
|
84
|
+
* allowing consumers to choose the most appropriate format for their use case.
|
|
85
|
+
*/
|
|
86
|
+
export interface AlloyFileInterface {
|
|
87
|
+
/**
|
|
88
|
+
* Read the file content as a UTF-8 encoded string.
|
|
89
|
+
*
|
|
90
|
+
* @returns A Promise that resolves to the file content as a string
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const file = AlloyHost.read('./config.json');
|
|
95
|
+
* const jsonString = await file.text();
|
|
96
|
+
* const config = JSON.parse(jsonString);
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
text(): Promise<string>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Read the file content as an ArrayBuffer.
|
|
103
|
+
*
|
|
104
|
+
* Useful for working with binary data or when you need a raw buffer
|
|
105
|
+
* that can be used with various JavaScript APIs.
|
|
106
|
+
*
|
|
107
|
+
* @returns A Promise that resolves to the file content as an ArrayBuffer
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* const file = AlloyHost.read('./image.png');
|
|
112
|
+
* const buffer = await file.arrayBuffer();
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read the file content as a Uint8Array.
|
|
119
|
+
*
|
|
120
|
+
* Convenient for working with binary data when you need direct access
|
|
121
|
+
* to individual bytes.
|
|
122
|
+
*
|
|
123
|
+
* @returns A Promise that resolves to the file content as a Uint8Array
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const file = AlloyHost.read('./binary.dat');
|
|
128
|
+
* const bytes = await file.bytes();
|
|
129
|
+
* console.log('First byte:', bytes[0]);
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
bytes(): Promise<Uint8Array>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get a readable stream of the file content.
|
|
136
|
+
*
|
|
137
|
+
* Useful for processing large files without loading the entire content
|
|
138
|
+
* into memory at once, or for piping data to other streams.
|
|
139
|
+
*
|
|
140
|
+
* @returns A ReadableStream that yields Uint8Array chunks of the file content
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const file = AlloyHost.read('./large-file.dat');
|
|
145
|
+
* const stream = file.stream();
|
|
146
|
+
*
|
|
147
|
+
* for await (const chunk of stream) {
|
|
148
|
+
* console.log('Received chunk:', chunk);
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
stream(): ReadableStream<Uint8Array>;
|
|
153
|
+
}
|
package/src/index.browser.ts
CHANGED
package/src/render.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
Props,
|
|
24
24
|
} from "./runtime/component.js";
|
|
25
25
|
import { IntrinsicElement, isIntrinsicElement } from "./runtime/intrinsic.js";
|
|
26
|
-
import { flushJobs } from "./scheduler.js";
|
|
26
|
+
import { flushJobs, flushJobsAsync } from "./scheduler.js";
|
|
27
27
|
import { trace, TracePhase } from "./tracer.js";
|
|
28
28
|
|
|
29
29
|
const {
|
|
@@ -133,13 +133,22 @@ export interface OutputDirectory {
|
|
|
133
133
|
contents: (OutputDirectory | OutputFile)[];
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export interface
|
|
136
|
+
export interface OutputFileBase {
|
|
137
137
|
kind: "file";
|
|
138
|
-
contents: string;
|
|
139
138
|
path: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface CopyOutputFile extends OutputFileBase {
|
|
142
|
+
sourcePath: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface ContentOutputFile extends OutputFileBase {
|
|
146
|
+
contents: string;
|
|
140
147
|
filetype: string;
|
|
141
148
|
}
|
|
142
149
|
|
|
150
|
+
export type OutputFile = ContentOutputFile | CopyOutputFile;
|
|
151
|
+
|
|
143
152
|
const nodesToContext = new WeakMap<RenderedTextTree, Context>();
|
|
144
153
|
|
|
145
154
|
export function getContextForRenderNode(node: RenderedTextTree) {
|
|
@@ -181,6 +190,22 @@ export function render(
|
|
|
181
190
|
): OutputDirectory {
|
|
182
191
|
const tree = renderTree(children);
|
|
183
192
|
flushJobs();
|
|
193
|
+
return sourceFilesForTree(tree, options);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function renderAsync(
|
|
197
|
+
children: Children,
|
|
198
|
+
options?: PrintTreeOptions,
|
|
199
|
+
): Promise<OutputDirectory> {
|
|
200
|
+
const tree = renderTree(children);
|
|
201
|
+
await flushJobsAsync();
|
|
202
|
+
return sourceFilesForTree(tree, options);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function sourceFilesForTree(
|
|
206
|
+
tree: RenderedTextTree,
|
|
207
|
+
options?: PrintTreeOptions,
|
|
208
|
+
): OutputDirectory {
|
|
184
209
|
let rootDirectory: OutputDirectory | undefined = undefined;
|
|
185
210
|
|
|
186
211
|
// when passing Output, the first render tree child is the Output component.
|
|
@@ -234,7 +259,7 @@ export function render(
|
|
|
234
259
|
);
|
|
235
260
|
}
|
|
236
261
|
|
|
237
|
-
const sourceFile:
|
|
262
|
+
const sourceFile: ContentOutputFile = {
|
|
238
263
|
kind: "file",
|
|
239
264
|
path: context.meta?.sourceFile.path,
|
|
240
265
|
filetype: context.meta?.sourceFile.filetype,
|
|
@@ -254,6 +279,21 @@ export function render(
|
|
|
254
279
|
}),
|
|
255
280
|
};
|
|
256
281
|
|
|
282
|
+
currentDirectory.contents.push(sourceFile);
|
|
283
|
+
} else if (context.meta?.copyFile) {
|
|
284
|
+
if (!currentDirectory) {
|
|
285
|
+
// This shouldn't happen if you're using the Output component.
|
|
286
|
+
throw new Error(
|
|
287
|
+
"Copy file doesn't have parent directory. Make sure you have used the Output component.",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const sourceFile: CopyOutputFile = {
|
|
292
|
+
kind: "file",
|
|
293
|
+
path: context.meta?.copyFile.path,
|
|
294
|
+
sourcePath: context.meta?.copyFile.sourcePath,
|
|
295
|
+
};
|
|
296
|
+
|
|
257
297
|
currentDirectory.contents.push(sourceFile);
|
|
258
298
|
} else {
|
|
259
299
|
recurse(currentDirectory);
|
|
@@ -266,7 +306,6 @@ export function render(
|
|
|
266
306
|
}
|
|
267
307
|
}
|
|
268
308
|
}
|
|
269
|
-
|
|
270
309
|
export function renderTree(children: Children) {
|
|
271
310
|
const rootElem: RenderedTextTree = [];
|
|
272
311
|
try {
|
package/src/resource.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { isRef, reactive, Ref } from "@vue/reactivity";
|
|
2
|
+
import { AlloyHost } from "./host/alloy-host.js";
|
|
3
|
+
import { effect } from "./reactivity.js";
|
|
4
|
+
import { trackPromise } from "./scheduler.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents an external resource fetched asynchronously.
|
|
8
|
+
*/
|
|
9
|
+
export interface Resource<T> {
|
|
10
|
+
/**
|
|
11
|
+
* The data if it's been loaded successfully. Null when not yet loaded or
|
|
12
|
+
* there was an error.
|
|
13
|
+
*/
|
|
14
|
+
data: T | null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether the resource is still being fetched.
|
|
18
|
+
*/
|
|
19
|
+
loading: boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The error loading the resource, if any.
|
|
23
|
+
*/
|
|
24
|
+
error: null | Error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a resource that fetches data asynchronously.
|
|
29
|
+
*
|
|
30
|
+
* This function has two overloads:
|
|
31
|
+
* 1. Simple fetcher - fetches data once when the resource is created
|
|
32
|
+
* 2. Reactive fetcher - fetches data when a reactive source changes
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // Simple usage - fetches data once when created
|
|
37
|
+
* const userResource = createResource(async () => {
|
|
38
|
+
* const response = await fetch('/api/user');
|
|
39
|
+
* return response.json();
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* // Access the resource state
|
|
43
|
+
* console.log(userResource.loading); // true initially
|
|
44
|
+
* console.log(userResource.data); // null initially
|
|
45
|
+
* console.log(userResource.error); // null initially
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* // Reactive usage - fetches data when the ref changes
|
|
51
|
+
* const userId = ref(1);
|
|
52
|
+
*
|
|
53
|
+
* const userResource = createResource(userId, async (id) => {
|
|
54
|
+
* const response = await fetch(`/api/user/${id}`);
|
|
55
|
+
* return response.json();
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* // The fetcher will be called automatically when userId changes
|
|
59
|
+
* userId.value = 2; // This triggers a new fetch with id=2
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function createResource<U>(fetcher: () => Promise<U>): Resource<U>;
|
|
63
|
+
/**
|
|
64
|
+
* Create a resource that fetches data asynchronously based on a reactive source.
|
|
65
|
+
*/
|
|
66
|
+
export function createResource<T, U>(
|
|
67
|
+
refSource: Ref<T> | (() => T),
|
|
68
|
+
fetcher: (input: T) => Promise<U>,
|
|
69
|
+
): Resource<U>;
|
|
70
|
+
export function createResource<T, U>(
|
|
71
|
+
fetcherOrSource: (() => Promise<U>) | Ref<T> | (() => T),
|
|
72
|
+
maybeFetcher?: (input: T) => Promise<U>,
|
|
73
|
+
): Resource<U> {
|
|
74
|
+
let getter: Ref<T> | (() => T) | null = null;
|
|
75
|
+
let fetcher: (() => Promise<U>) | ((input: T) => Promise<U>);
|
|
76
|
+
|
|
77
|
+
if (arguments.length === 1) {
|
|
78
|
+
fetcher = fetcherOrSource as () => Promise<U>;
|
|
79
|
+
} else {
|
|
80
|
+
getter = fetcherOrSource as Ref<T> | (() => T);
|
|
81
|
+
fetcher = maybeFetcher!;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resource: Resource<U> = reactive({
|
|
85
|
+
data: null,
|
|
86
|
+
loading: true,
|
|
87
|
+
error: null,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!getter) {
|
|
91
|
+
const promise = (fetcher as () => Promise<U>)();
|
|
92
|
+
trackPromise(
|
|
93
|
+
promise
|
|
94
|
+
.then((result) => {
|
|
95
|
+
resource.data = result;
|
|
96
|
+
resource.loading = false;
|
|
97
|
+
})
|
|
98
|
+
.catch((error) => {
|
|
99
|
+
resource.error = error;
|
|
100
|
+
resource.loading = false;
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
effect(() => {
|
|
105
|
+
let input: T;
|
|
106
|
+
if (isRef(getter)) {
|
|
107
|
+
input = getter.value;
|
|
108
|
+
} else {
|
|
109
|
+
input = getter();
|
|
110
|
+
}
|
|
111
|
+
const promise = (fetcher as (input: T) => Promise<U>)(input);
|
|
112
|
+
trackPromise(promise);
|
|
113
|
+
promise.then(
|
|
114
|
+
(result) => {
|
|
115
|
+
resource.data = result;
|
|
116
|
+
resource.loading = false;
|
|
117
|
+
},
|
|
118
|
+
(error) => {
|
|
119
|
+
resource.error = error;
|
|
120
|
+
resource.loading = false;
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return resource;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create a resource that reads a file from the file system.
|
|
131
|
+
*
|
|
132
|
+
* This is a convenience function that creates a resource for reading file content
|
|
133
|
+
* using the AlloyHost file system API. The file is read as text when the resource
|
|
134
|
+
* is created.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* // Read a configuration file
|
|
139
|
+
* const configResource = createFileResource('./config.json');
|
|
140
|
+
*
|
|
141
|
+
* // Access the file content
|
|
142
|
+
* if (!configResource.loading && !configResource.error) {
|
|
143
|
+
* const configText = configResource.data; // string content of the file
|
|
144
|
+
* const config = JSON.parse(configText);
|
|
145
|
+
* }
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export function createFileResource(path: string) {
|
|
149
|
+
return createResource(() => {
|
|
150
|
+
return AlloyHost.read(path).text();
|
|
151
|
+
});
|
|
152
|
+
}
|
package/src/scheduler.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface QueueJob {
|
|
|
5
5
|
}
|
|
6
6
|
const immediateQueue = new Set<QueueJob>();
|
|
7
7
|
const queue = new Set<QueueJob>();
|
|
8
|
+
const pendingPromises = new Set<Promise<any>>();
|
|
8
9
|
|
|
9
10
|
export function scheduler(
|
|
10
11
|
jobGetter: () => ReactiveEffectRunner,
|
|
@@ -25,11 +26,49 @@ export function queueJob(job: QueueJob, immediate = false) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Register a promise that the scheduler should wait for during flushJobs.
|
|
31
|
+
* This is used by async resources to ensure the scheduler waits for their completion.
|
|
32
|
+
*/
|
|
33
|
+
export function trackPromise(promise: Promise<any>) {
|
|
34
|
+
pendingPromises.add(promise);
|
|
35
|
+
void promise.finally(() => {
|
|
36
|
+
pendingPromises.delete(promise);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
export function flushJobs() {
|
|
41
|
+
// First, run all synchronous jobs
|
|
29
42
|
let job;
|
|
30
43
|
while ((job = takeJob()) !== null) {
|
|
31
44
|
job();
|
|
32
45
|
}
|
|
46
|
+
|
|
47
|
+
// If there are no pending promises, we're done
|
|
48
|
+
if (pendingPromises.size > 0) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"Asynchronous jobs were found but render was called synchronously. Use `asyncRender` instead.",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function flushJobsAsync() {
|
|
56
|
+
// Keep running jobs until both the queues are empty and all promises are resolved
|
|
57
|
+
while (true) {
|
|
58
|
+
// First, run all synchronous jobs
|
|
59
|
+
let job;
|
|
60
|
+
while ((job = takeJob()) !== null) {
|
|
61
|
+
job();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If there are no pending promises, we're done
|
|
65
|
+
if (pendingPromises.size === 0) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Wait for all current promises to complete
|
|
70
|
+
await Promise.allSettled(Array.from(pendingPromises));
|
|
71
|
+
}
|
|
33
72
|
}
|
|
34
73
|
|
|
35
74
|
function takeJob() {
|