@sprig-and-prose/sprig 0.1.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/README.md +41 -0
- package/biome.json +32 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +161 -0
- package/dist/cli.js.map +1 -0
- package/dist/compiler.d.ts +21 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +117 -0
- package/dist/compiler.js.map +1 -0
- package/dist/prose.d.ts +7 -0
- package/dist/prose.d.ts.map +1 -0
- package/dist/prose.js +18 -0
- package/dist/prose.js.map +1 -0
- package/dist/root.d.ts +7 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +25 -0
- package/dist/root.js.map +1 -0
- package/dist/ui.d.ts +5 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +293 -0
- package/dist/ui.js.map +1 -0
- package/package.json +33 -0
- package/src/cli.ts +186 -0
- package/src/compiler.ts +142 -0
- package/src/prose.ts +21 -0
- package/src/root.ts +29 -0
- package/src/ui.ts +336 -0
- package/tests/compile.test.d.ts +2 -0
- package/tests/compile.test.d.ts.map +1 -0
- package/tests/compile.test.js +64 -0
- package/tests/compile.test.js.map +1 -0
- package/tests/compile.test.ts +87 -0
- package/tests/root.test.d.ts +2 -0
- package/tests/root.test.d.ts.map +1 -0
- package/tests/root.test.js +43 -0
- package/tests/root.test.js.map +1 -0
- package/tests/root.test.ts +46 -0
- package/tsconfig.json +21 -0
package/src/root.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve, dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Walks upward from startPath to find universe.prose marker
|
|
6
|
+
* @param startPath - Starting directory path
|
|
7
|
+
* @returns Path to directory containing universe.prose, or null if not found
|
|
8
|
+
*/
|
|
9
|
+
export function discoverUniverseRoot(startPath: string): string | null {
|
|
10
|
+
let current = resolve(startPath);
|
|
11
|
+
const root = resolve("/");
|
|
12
|
+
|
|
13
|
+
while (current !== root) {
|
|
14
|
+
const markerPath = join(current, "universe.prose");
|
|
15
|
+
if (existsSync(markerPath) && statSync(markerPath).isFile()) {
|
|
16
|
+
return current;
|
|
17
|
+
}
|
|
18
|
+
current = dirname(current);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check root directory as well
|
|
22
|
+
const rootMarkerPath = join(root, "universe.prose");
|
|
23
|
+
if (existsSync(rootMarkerPath) && statSync(rootMarkerPath).isFile()) {
|
|
24
|
+
return root;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { createServer, type Server } from "node:http";
|
|
2
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
3
|
+
import { join, extname, resolve, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import chokidar from "chokidar";
|
|
6
|
+
import { discoverUniverseRoot } from "./root.js";
|
|
7
|
+
import { loadProseFiles } from "./prose.js";
|
|
8
|
+
import { compileUniverse } from "./compiler.js";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const mimeTypes: Record<string, string> = {
|
|
14
|
+
".html": "text/html",
|
|
15
|
+
".js": "application/javascript",
|
|
16
|
+
".json": "application/json",
|
|
17
|
+
".css": "text/css",
|
|
18
|
+
".png": "image/png",
|
|
19
|
+
".jpg": "image/jpeg",
|
|
20
|
+
".jpeg": "image/jpeg",
|
|
21
|
+
".gif": "image/gif",
|
|
22
|
+
".svg": "image/svg+xml",
|
|
23
|
+
".woff": "font/woff",
|
|
24
|
+
".woff2": "font/woff2",
|
|
25
|
+
".ttf": "font/ttf",
|
|
26
|
+
".eot": "application/vnd.ms-fontobject",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the dist directory path for sprig-ui-csr
|
|
31
|
+
*/
|
|
32
|
+
function resolveUIDist(): string {
|
|
33
|
+
// Try to resolve from node_modules
|
|
34
|
+
try {
|
|
35
|
+
// First, try to import the package and check for an export
|
|
36
|
+
// If that doesn't work, resolve from node_modules
|
|
37
|
+
const packagePath = resolve(__dirname, "../node_modules/@sprig-and-prose/sprig-ui-csr");
|
|
38
|
+
const distPath = join(packagePath, "dist");
|
|
39
|
+
if (existsSync(distPath)) {
|
|
40
|
+
return distPath;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Fall through to error
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fallback: try relative to current package
|
|
47
|
+
const fallbackPath = resolve(__dirname, "../../sprig-ui-csr/dist");
|
|
48
|
+
if (existsSync(fallbackPath)) {
|
|
49
|
+
return fallbackPath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error("Could not locate sprig-ui-csr dist directory");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let revisionCounter = 0;
|
|
56
|
+
const sseClients = new Set<import("node:http").ServerResponse>();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Broadcasts a manifest changed event to all connected clients
|
|
60
|
+
*/
|
|
61
|
+
function broadcastManifestChanged(): void {
|
|
62
|
+
revisionCounter += 1;
|
|
63
|
+
const message = `id: ${revisionCounter}\nevent: manifest\ndata: ${JSON.stringify({ revision: revisionCounter })}\n\n`;
|
|
64
|
+
console.log(`Broadcasting manifest change (revision ${revisionCounter}) to ${sseClients.size} client(s)`);
|
|
65
|
+
for (const res of sseClients) {
|
|
66
|
+
try {
|
|
67
|
+
res.write(message);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// Client disconnected, remove it
|
|
70
|
+
console.log(`Client disconnected, removing from set`);
|
|
71
|
+
sseClients.delete(res);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Creates the HTTP server
|
|
78
|
+
*/
|
|
79
|
+
function createServerHandler(
|
|
80
|
+
uiDistDir: string,
|
|
81
|
+
manifestPath: string,
|
|
82
|
+
): (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse) => void {
|
|
83
|
+
return (req, res) => {
|
|
84
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
85
|
+
const pathname = url.pathname;
|
|
86
|
+
|
|
87
|
+
// Handle /_ui/* routes - serve static files from dist/
|
|
88
|
+
if (pathname.startsWith("/_ui/")) {
|
|
89
|
+
const filePath = pathname.slice(5); // Remove '/_ui/'
|
|
90
|
+
const fullPath = join(uiDistDir, filePath);
|
|
91
|
+
|
|
92
|
+
// Security: ensure file is within dist directory
|
|
93
|
+
const resolvedPath = resolve(fullPath);
|
|
94
|
+
if (!resolvedPath.startsWith(resolve(uiDistDir))) {
|
|
95
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
96
|
+
res.end("Forbidden");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!existsSync(resolvedPath) || !statSync(resolvedPath).isFile()) {
|
|
101
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
102
|
+
res.end("Not Found");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const content = readFileSync(resolvedPath);
|
|
108
|
+
const ext = extname(resolvedPath);
|
|
109
|
+
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
110
|
+
|
|
111
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
112
|
+
res.end(content);
|
|
113
|
+
} catch {
|
|
114
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
115
|
+
res.end("Internal Server Error");
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle /api/manifest route
|
|
121
|
+
if (pathname === "/api/manifest") {
|
|
122
|
+
if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) {
|
|
123
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
124
|
+
res.end(JSON.stringify({ error: "Manifest not found" }));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const content = readFileSync(manifestPath, "utf-8");
|
|
130
|
+
const headers = {
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
};
|
|
133
|
+
res.writeHead(200, headers);
|
|
134
|
+
res.end(content);
|
|
135
|
+
} catch {
|
|
136
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
137
|
+
res.end(JSON.stringify({ error: "Error reading manifest" }));
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle /api/events route - Server-Sent Events
|
|
143
|
+
if (pathname === "/api/events") {
|
|
144
|
+
res.writeHead(200, {
|
|
145
|
+
"Content-Type": "text/event-stream",
|
|
146
|
+
"Cache-Control": "no-cache",
|
|
147
|
+
Connection: "keep-alive",
|
|
148
|
+
"X-Accel-Buffering": "no",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Send initial connection comment
|
|
152
|
+
res.write(": connected\n\n");
|
|
153
|
+
|
|
154
|
+
sseClients.add(res);
|
|
155
|
+
console.log(`SSE client connected (total: ${sseClients.size})`);
|
|
156
|
+
|
|
157
|
+
// Handle client disconnect
|
|
158
|
+
const cleanup = () => {
|
|
159
|
+
sseClients.delete(res);
|
|
160
|
+
console.log(`SSE client disconnected (remaining: ${sseClients.size})`);
|
|
161
|
+
};
|
|
162
|
+
req.on("close", cleanup);
|
|
163
|
+
res.on("close", cleanup);
|
|
164
|
+
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// All other routes - serve index.html for client-side routing
|
|
169
|
+
const indexPath = join(uiDistDir, "index.html");
|
|
170
|
+
if (!existsSync(indexPath)) {
|
|
171
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
172
|
+
res.end("Not Found - dist/index.html does not exist.");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const content = readFileSync(indexPath, "utf-8");
|
|
178
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
179
|
+
res.end(content);
|
|
180
|
+
} catch {
|
|
181
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
182
|
+
res.end("Internal Server Error");
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Rebuilds the manifest and broadcasts SSE event
|
|
189
|
+
* Only broadcasts after successful compile and atomic manifest write
|
|
190
|
+
*/
|
|
191
|
+
async function rebuildManifest(
|
|
192
|
+
universeRoot: string,
|
|
193
|
+
manifestPath: string,
|
|
194
|
+
): Promise<boolean> {
|
|
195
|
+
try {
|
|
196
|
+
const files = await loadProseFiles(universeRoot);
|
|
197
|
+
const result = await compileUniverse(universeRoot, files, true);
|
|
198
|
+
|
|
199
|
+
// Only broadcast if compilation succeeded (manifest was atomically written)
|
|
200
|
+
if (result.success) {
|
|
201
|
+
broadcastManifestChanged();
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
} catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Starts the UI server with watch mode
|
|
212
|
+
*/
|
|
213
|
+
export async function startUIServer(
|
|
214
|
+
universeRoot: string,
|
|
215
|
+
port: number = 6336,
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
const uiDistDir = resolveUIDist();
|
|
218
|
+
const manifestPath = join(universeRoot, ".sprig", "manifest.json");
|
|
219
|
+
|
|
220
|
+
// Compile once on startup
|
|
221
|
+
console.log("Compiling universe...");
|
|
222
|
+
const files = await loadProseFiles(universeRoot);
|
|
223
|
+
const result = await compileUniverse(universeRoot, files, true);
|
|
224
|
+
|
|
225
|
+
if (!result.success) {
|
|
226
|
+
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
227
|
+
if (errors.length > 0) {
|
|
228
|
+
for (const error of errors) {
|
|
229
|
+
console.error(`Error: ${error.message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log("✓ Universe compiled");
|
|
236
|
+
|
|
237
|
+
// Create HTTP server
|
|
238
|
+
const server = createServer(createServerHandler(uiDistDir, manifestPath));
|
|
239
|
+
|
|
240
|
+
server.on("error", (error: NodeJS.ErrnoException) => {
|
|
241
|
+
if (error.code === "EADDRINUSE") {
|
|
242
|
+
console.error("");
|
|
243
|
+
console.error("Couldn't start the UI server.");
|
|
244
|
+
console.error("");
|
|
245
|
+
console.error(`Port ${port} is already in use.`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
} else {
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
server.listen(port, () => {
|
|
253
|
+
console.log(`Server running at http://localhost:${port}`);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Set up heartbeat for SSE connections (every 25 seconds)
|
|
257
|
+
const heartbeatInterval = setInterval(() => {
|
|
258
|
+
for (const res of sseClients) {
|
|
259
|
+
try {
|
|
260
|
+
res.write(": ping\n\n");
|
|
261
|
+
} catch {
|
|
262
|
+
// Client disconnected, remove it
|
|
263
|
+
sseClients.delete(res);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}, 25000);
|
|
267
|
+
|
|
268
|
+
// Set up file watching with debounce
|
|
269
|
+
let rebuildTimeout: NodeJS.Timeout | null = null;
|
|
270
|
+
const DEBOUNCE_MS = 200;
|
|
271
|
+
|
|
272
|
+
const watcher = chokidar.watch(join(universeRoot, "**/*.prose"), {
|
|
273
|
+
ignored: [
|
|
274
|
+
join(universeRoot, ".sprig/**"),
|
|
275
|
+
join(universeRoot, "dist/**"),
|
|
276
|
+
join(universeRoot, "node_modules/**"),
|
|
277
|
+
join(universeRoot, ".git/**"),
|
|
278
|
+
],
|
|
279
|
+
persistent: true,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
watcher.on("change", () => {
|
|
283
|
+
if (rebuildTimeout) {
|
|
284
|
+
clearTimeout(rebuildTimeout);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
rebuildTimeout = setTimeout(async () => {
|
|
288
|
+
const success = await rebuildManifest(universeRoot, manifestPath);
|
|
289
|
+
if (success) {
|
|
290
|
+
// One-line status per rebuild (calm output)
|
|
291
|
+
process.stdout.write("\r✓ Rebuilt\n");
|
|
292
|
+
} else {
|
|
293
|
+
process.stdout.write("\r✗ Rebuild failed\n");
|
|
294
|
+
}
|
|
295
|
+
}, DEBOUNCE_MS);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Handle graceful shutdown
|
|
299
|
+
function shutdown() {
|
|
300
|
+
// Clear heartbeat interval
|
|
301
|
+
clearInterval(heartbeatInterval);
|
|
302
|
+
|
|
303
|
+
// Clear any pending rebuild timeout
|
|
304
|
+
if (rebuildTimeout) {
|
|
305
|
+
clearTimeout(rebuildTimeout);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Close all SSE connections
|
|
309
|
+
for (const res of sseClients) {
|
|
310
|
+
try {
|
|
311
|
+
res.end();
|
|
312
|
+
} catch {
|
|
313
|
+
// Ignore errors on already closed connections
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
sseClients.clear();
|
|
317
|
+
|
|
318
|
+
// Close file watcher
|
|
319
|
+
watcher.close();
|
|
320
|
+
|
|
321
|
+
// Close server
|
|
322
|
+
server.close(() => {
|
|
323
|
+
process.exit(0);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Force exit after 2 seconds if server doesn't close cleanly
|
|
327
|
+
setTimeout(() => {
|
|
328
|
+
console.error("Forcing exit...");
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}, 2000);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
process.on("SIGINT", shutdown);
|
|
334
|
+
process.on("SIGTERM", shutdown);
|
|
335
|
+
}
|
|
336
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.test.d.ts","sourceRoot":"","sources":["compile.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
|
|
4
|
+
import { join, tmpdir } from "node:path";
|
|
5
|
+
import { compileUniverse } from "../src/compiler.js";
|
|
6
|
+
test("compileUniverse writes .sprig/manifest.json", async () => {
|
|
7
|
+
const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
|
|
8
|
+
mkdirSync(testDir, { recursive: true });
|
|
9
|
+
// Create universe.prose marker
|
|
10
|
+
writeFileSync(join(testDir, "universe.prose"), `universe TestUniverse {
|
|
11
|
+
describe {
|
|
12
|
+
A test universe.
|
|
13
|
+
}
|
|
14
|
+
}`);
|
|
15
|
+
// Create a simple prose file
|
|
16
|
+
const proseFile = join(testDir, "test.prose");
|
|
17
|
+
writeFileSync(proseFile, `series TestSeries {
|
|
18
|
+
describe {
|
|
19
|
+
A test series.
|
|
20
|
+
}
|
|
21
|
+
}`);
|
|
22
|
+
try {
|
|
23
|
+
const result = await compileUniverse(testDir, [proseFile], true);
|
|
24
|
+
assert.strictEqual(result.success, true);
|
|
25
|
+
assert.ok(result.manifest);
|
|
26
|
+
// Check that manifest.json was written
|
|
27
|
+
const manifestPath = join(testDir, ".sprig", "manifest.json");
|
|
28
|
+
assert.ok(existsSync(manifestPath), "manifest.json should exist");
|
|
29
|
+
// Check manifest content
|
|
30
|
+
const manifestContent = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
31
|
+
assert.strictEqual(manifestContent.universes?.TestUniverse?.name, "TestUniverse");
|
|
32
|
+
assert.ok(manifestContent.generatedAt);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
test("compileUniverse does not write manifest when writeManifest is false", async () => {
|
|
39
|
+
const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
|
|
40
|
+
mkdirSync(testDir, { recursive: true });
|
|
41
|
+
writeFileSync(join(testDir, "universe.prose"), `universe TestUniverse {
|
|
42
|
+
describe {
|
|
43
|
+
A test universe.
|
|
44
|
+
}
|
|
45
|
+
}`);
|
|
46
|
+
const proseFile = join(testDir, "test.prose");
|
|
47
|
+
writeFileSync(proseFile, `series TestSeries {
|
|
48
|
+
describe {
|
|
49
|
+
A test series.
|
|
50
|
+
}
|
|
51
|
+
}`);
|
|
52
|
+
try {
|
|
53
|
+
const result = await compileUniverse(testDir, [proseFile], false);
|
|
54
|
+
assert.strictEqual(result.success, true);
|
|
55
|
+
assert.ok(result.manifest);
|
|
56
|
+
// Check that manifest.json was NOT written
|
|
57
|
+
const manifestPath = join(testDir, ".sprig", "manifest.json");
|
|
58
|
+
assert.ok(!existsSync(manifestPath), "manifest.json should not exist when writeManifest is false");
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=compile.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.test.js","sourceRoot":"","sources":["compile.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
|
|
4
|
+
import { join, tmpdir } from "node:path";
|
|
5
|
+
import { compileUniverse } from "../src/compiler.js";
|
|
6
|
+
|
|
7
|
+
test("compileUniverse writes .sprig/manifest.json", async () => {
|
|
8
|
+
const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
|
|
9
|
+
mkdirSync(testDir, { recursive: true });
|
|
10
|
+
|
|
11
|
+
// Create universe.prose marker
|
|
12
|
+
writeFileSync(
|
|
13
|
+
join(testDir, "universe.prose"),
|
|
14
|
+
`universe TestUniverse {
|
|
15
|
+
describe {
|
|
16
|
+
A test universe.
|
|
17
|
+
}
|
|
18
|
+
}`,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Create a simple prose file
|
|
22
|
+
const proseFile = join(testDir, "test.prose");
|
|
23
|
+
writeFileSync(
|
|
24
|
+
proseFile,
|
|
25
|
+
`series TestSeries {
|
|
26
|
+
describe {
|
|
27
|
+
A test series.
|
|
28
|
+
}
|
|
29
|
+
}`,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const result = await compileUniverse(testDir, [proseFile], true);
|
|
34
|
+
|
|
35
|
+
assert.strictEqual(result.success, true);
|
|
36
|
+
assert.ok(result.manifest);
|
|
37
|
+
|
|
38
|
+
// Check that manifest.json was written
|
|
39
|
+
const manifestPath = join(testDir, ".sprig", "manifest.json");
|
|
40
|
+
assert.ok(existsSync(manifestPath), "manifest.json should exist");
|
|
41
|
+
|
|
42
|
+
// Check manifest content
|
|
43
|
+
const manifestContent = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
44
|
+
assert.strictEqual(manifestContent.universes?.TestUniverse?.name, "TestUniverse");
|
|
45
|
+
assert.ok(manifestContent.generatedAt);
|
|
46
|
+
} finally {
|
|
47
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("compileUniverse does not write manifest when writeManifest is false", async () => {
|
|
52
|
+
const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
|
|
53
|
+
mkdirSync(testDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
writeFileSync(
|
|
56
|
+
join(testDir, "universe.prose"),
|
|
57
|
+
`universe TestUniverse {
|
|
58
|
+
describe {
|
|
59
|
+
A test universe.
|
|
60
|
+
}
|
|
61
|
+
}`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const proseFile = join(testDir, "test.prose");
|
|
65
|
+
writeFileSync(
|
|
66
|
+
proseFile,
|
|
67
|
+
`series TestSeries {
|
|
68
|
+
describe {
|
|
69
|
+
A test series.
|
|
70
|
+
}
|
|
71
|
+
}`,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const result = await compileUniverse(testDir, [proseFile], false);
|
|
76
|
+
|
|
77
|
+
assert.strictEqual(result.success, true);
|
|
78
|
+
assert.ok(result.manifest);
|
|
79
|
+
|
|
80
|
+
// Check that manifest.json was NOT written
|
|
81
|
+
const manifestPath = join(testDir, ".sprig", "manifest.json");
|
|
82
|
+
assert.ok(!existsSync(manifestPath), "manifest.json should not exist when writeManifest is false");
|
|
83
|
+
} finally {
|
|
84
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root.test.d.ts","sourceRoot":"","sources":["root.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { discoverUniverseRoot } from "../src/root.js";
|
|
7
|
+
test("discoverUniverseRoot finds universe.prose in current directory", () => {
|
|
8
|
+
const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
|
|
9
|
+
mkdirSync(testDir, { recursive: true });
|
|
10
|
+
writeFileSync(join(testDir, "universe.prose"), "universe Test {}");
|
|
11
|
+
try {
|
|
12
|
+
const root = discoverUniverseRoot(testDir);
|
|
13
|
+
assert.strictEqual(root, testDir);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
test("discoverUniverseRoot finds universe.prose in parent directory", () => {
|
|
20
|
+
const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
|
|
21
|
+
const subDir = join(testDir, "sub", "nested");
|
|
22
|
+
mkdirSync(subDir, { recursive: true });
|
|
23
|
+
writeFileSync(join(testDir, "universe.prose"), "universe Test {}");
|
|
24
|
+
try {
|
|
25
|
+
const root = discoverUniverseRoot(subDir);
|
|
26
|
+
assert.strictEqual(root, testDir);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
test("discoverUniverseRoot returns null when not found", () => {
|
|
33
|
+
const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
|
|
34
|
+
mkdirSync(testDir, { recursive: true });
|
|
35
|
+
try {
|
|
36
|
+
const root = discoverUniverseRoot(testDir);
|
|
37
|
+
assert.strictEqual(root, null);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=root.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root.test.js","sourceRoot":"","sources":["root.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAW,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,IAAI,CAAC,gEAAgE,EAAE,GAAG,EAAE;IAC1E,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC3D,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxC,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAEnE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACpC,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,+DAA+D,EAAE,GAAG,EAAE;IACzE,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC9C,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAEnE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAC1C,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACpC,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,kDAAkD,EAAE,GAAG,EAAE;IAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC3D,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAExC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACjC,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;AACH,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { discoverUniverseRoot } from "../src/root.js";
|
|
7
|
+
|
|
8
|
+
test("discoverUniverseRoot finds universe.prose in current directory", () => {
|
|
9
|
+
const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
|
|
10
|
+
mkdirSync(testDir, { recursive: true });
|
|
11
|
+
writeFileSync(join(testDir, "universe.prose"), "universe Test {}");
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const root = discoverUniverseRoot(testDir);
|
|
15
|
+
assert.strictEqual(root, testDir);
|
|
16
|
+
} finally {
|
|
17
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("discoverUniverseRoot finds universe.prose in parent directory", () => {
|
|
22
|
+
const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
|
|
23
|
+
const subDir = join(testDir, "sub", "nested");
|
|
24
|
+
mkdirSync(subDir, { recursive: true });
|
|
25
|
+
writeFileSync(join(testDir, "universe.prose"), "universe Test {}");
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const root = discoverUniverseRoot(subDir);
|
|
29
|
+
assert.strictEqual(root, testDir);
|
|
30
|
+
} finally {
|
|
31
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("discoverUniverseRoot returns null when not found", () => {
|
|
36
|
+
const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
|
|
37
|
+
mkdirSync(testDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const root = discoverUniverseRoot(testDir);
|
|
41
|
+
assert.strictEqual(root, null);
|
|
42
|
+
} finally {
|
|
43
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
20
|
+
}
|
|
21
|
+
|