create-glanceway-source 1.0.0 → 1.2.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 +77 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +580 -141
- package/package.json +19 -21
- package/templates/gitignore +24 -0
- package/templates/scripts/build.ts +103 -0
- package/templates/scripts/test.ts +475 -0
- package/{template → templates}/src/types.ts +24 -15
- package/{template → templates}/tsconfig.json +2 -3
- package/template/manifest.yaml.ejs +0 -6
- package/template/package.json.ejs +0 -14
- package/template/src/index.ts +0 -9
package/package.json
CHANGED
|
@@ -1,34 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-glanceway-source",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Scaffold a standalone Glanceway source project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"create-glanceway-source": "
|
|
7
|
+
"create-glanceway-source": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc"
|
|
8
11
|
},
|
|
9
12
|
"files": [
|
|
10
13
|
"dist",
|
|
11
|
-
"
|
|
14
|
+
"templates"
|
|
12
15
|
],
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"dev": "npm run build && node dist/index.js"
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"prompts": "^2.4.2"
|
|
16
18
|
},
|
|
17
|
-
"keywords": [
|
|
18
|
-
"glanceway",
|
|
19
|
-
"source",
|
|
20
|
-
"create",
|
|
21
|
-
"cli",
|
|
22
|
-
"template"
|
|
23
|
-
],
|
|
24
|
-
"author": "codytseng",
|
|
25
|
-
"license": "MIT",
|
|
26
19
|
"devDependencies": {
|
|
27
|
-
"@types/
|
|
28
|
-
"
|
|
29
|
-
"
|
|
20
|
+
"@types/archiver": "^7.0.0",
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"@types/prompts": "^2.4.9",
|
|
23
|
+
"archiver": "^7.0.1",
|
|
24
|
+
"esbuild": "^0.27.3",
|
|
25
|
+
"typescript": "^5.3.3",
|
|
26
|
+
"yaml": "^2.8.2"
|
|
30
27
|
},
|
|
31
28
|
"engines": {
|
|
32
29
|
"node": ">=18"
|
|
33
|
-
}
|
|
34
|
-
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT"
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build outputs
|
|
5
|
+
dist/
|
|
6
|
+
|
|
7
|
+
# IDE
|
|
8
|
+
.idea/
|
|
9
|
+
.vscode/
|
|
10
|
+
*.swp
|
|
11
|
+
*.swo
|
|
12
|
+
.claude/
|
|
13
|
+
|
|
14
|
+
# OS
|
|
15
|
+
.DS_Store
|
|
16
|
+
Thumbs.db
|
|
17
|
+
|
|
18
|
+
# Logs
|
|
19
|
+
*.log
|
|
20
|
+
npm-debug.log*
|
|
21
|
+
|
|
22
|
+
# Environment
|
|
23
|
+
.env
|
|
24
|
+
.env.local
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import * as esbuild from "esbuild";
|
|
5
|
+
import archiver from "archiver";
|
|
6
|
+
|
|
7
|
+
const ROOT_DIR = process.cwd();
|
|
8
|
+
const DIST_DIR = path.join(ROOT_DIR, "dist");
|
|
9
|
+
|
|
10
|
+
async function compileTypeScript(indexPath: string): Promise<string> {
|
|
11
|
+
const result = await esbuild.build({
|
|
12
|
+
entryPoints: [indexPath],
|
|
13
|
+
bundle: true,
|
|
14
|
+
platform: "neutral",
|
|
15
|
+
format: "iife",
|
|
16
|
+
globalName: "_source",
|
|
17
|
+
write: false,
|
|
18
|
+
external: ["node:*"],
|
|
19
|
+
footer: { js: "module.exports = _source.default;" },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return result.outputFiles[0].text;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function createZip(
|
|
26
|
+
outputPath: string,
|
|
27
|
+
files: { name: string; content: string | Buffer }[],
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const output = fs.createWriteStream(outputPath);
|
|
31
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
32
|
+
|
|
33
|
+
output.on("close", resolve);
|
|
34
|
+
archive.on("error", reject);
|
|
35
|
+
|
|
36
|
+
archive.pipe(output);
|
|
37
|
+
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
archive.append(file.content, { name: file.name });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
archive.finalize();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
const manifestPath = path.join(ROOT_DIR, "manifest.yaml");
|
|
48
|
+
const indexPath = path.join(ROOT_DIR, "src", "index.ts");
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(manifestPath)) {
|
|
51
|
+
console.error("Error: manifest.yaml not found");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(indexPath)) {
|
|
56
|
+
console.error("Error: src/index.ts not found");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Read manifest for version
|
|
61
|
+
const manifestContent = fs.readFileSync(manifestPath, "utf-8");
|
|
62
|
+
const manifest = parseYaml(manifestContent);
|
|
63
|
+
const version = manifest.version || "1.0.0";
|
|
64
|
+
|
|
65
|
+
console.log(`Building source v${version}...\n`);
|
|
66
|
+
|
|
67
|
+
// Compile TypeScript
|
|
68
|
+
console.log("Compiling TypeScript...");
|
|
69
|
+
const compiledJs = await compileTypeScript(indexPath);
|
|
70
|
+
|
|
71
|
+
// Create dist directory
|
|
72
|
+
fs.mkdirSync(DIST_DIR, { recursive: true });
|
|
73
|
+
|
|
74
|
+
// Write compiled JS
|
|
75
|
+
const jsPath = path.join(DIST_DIR, "index.js");
|
|
76
|
+
fs.writeFileSync(jsPath, compiledJs);
|
|
77
|
+
console.log(" Created dist/index.js");
|
|
78
|
+
|
|
79
|
+
// Copy manifest
|
|
80
|
+
const distManifestPath = path.join(DIST_DIR, "manifest.yaml");
|
|
81
|
+
fs.copyFileSync(manifestPath, distManifestPath);
|
|
82
|
+
console.log(" Created dist/manifest.yaml");
|
|
83
|
+
|
|
84
|
+
// Create versioned .gwsrc package
|
|
85
|
+
const versionedPath = path.join(DIST_DIR, `${version}.gwsrc`);
|
|
86
|
+
await createZip(versionedPath, [
|
|
87
|
+
{ name: "manifest.yaml", content: manifestContent },
|
|
88
|
+
{ name: "index.js", content: compiledJs },
|
|
89
|
+
]);
|
|
90
|
+
console.log(` Created dist/${version}.gwsrc`);
|
|
91
|
+
|
|
92
|
+
// Create latest.gwsrc
|
|
93
|
+
const latestPath = path.join(DIST_DIR, "latest.gwsrc");
|
|
94
|
+
fs.copyFileSync(versionedPath, latestPath);
|
|
95
|
+
console.log(" Created dist/latest.gwsrc");
|
|
96
|
+
|
|
97
|
+
console.log("\nBuild complete!");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
main().catch((err) => {
|
|
101
|
+
console.error(err);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import * as esbuild from "esbuild";
|
|
5
|
+
|
|
6
|
+
// ─── ANSI Colors ───────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const color = {
|
|
9
|
+
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
|
|
10
|
+
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
|
|
11
|
+
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
|
|
12
|
+
cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
|
|
13
|
+
dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
|
|
14
|
+
bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const ROOT_DIR = process.cwd();
|
|
20
|
+
|
|
21
|
+
interface ManifestConfig {
|
|
22
|
+
key: string;
|
|
23
|
+
name: string;
|
|
24
|
+
type: string;
|
|
25
|
+
required?: boolean;
|
|
26
|
+
default?: unknown;
|
|
27
|
+
description?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface EmittedItem {
|
|
31
|
+
id?: unknown;
|
|
32
|
+
title?: unknown;
|
|
33
|
+
subtitle?: unknown;
|
|
34
|
+
url?: unknown;
|
|
35
|
+
timestamp?: unknown;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Arg Parsing ───────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function parseArgs(): { config: Record<string, string> } {
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
const config: Record<string, string> = {};
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < args.length; i++) {
|
|
46
|
+
if (args[i] === "--config" && args[i + 1]) {
|
|
47
|
+
const val = args[++i];
|
|
48
|
+
const eqIdx = val.indexOf("=");
|
|
49
|
+
if (eqIdx > 0) {
|
|
50
|
+
config[val.slice(0, eqIdx)] = val.slice(eqIdx + 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { config };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Config Resolution ─────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function resolveConfig(
|
|
61
|
+
configEntries: ManifestConfig[] | undefined,
|
|
62
|
+
overrides: Record<string, string>,
|
|
63
|
+
): { resolved: Record<string, unknown>; warnings: string[] } | null {
|
|
64
|
+
const resolved: Record<string, unknown> = {};
|
|
65
|
+
const warnings: string[] = [];
|
|
66
|
+
|
|
67
|
+
if (!configEntries || configEntries.length === 0) {
|
|
68
|
+
return { resolved, warnings };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const entry of configEntries) {
|
|
72
|
+
const override = overrides[entry.key];
|
|
73
|
+
|
|
74
|
+
if (override !== undefined) {
|
|
75
|
+
if (entry.type === "list") {
|
|
76
|
+
resolved[entry.key] = override.split(",").map((s) => s.trim());
|
|
77
|
+
} else if (entry.type === "boolean") {
|
|
78
|
+
resolved[entry.key] = override === "true" || override === "1";
|
|
79
|
+
} else if (entry.type === "number") {
|
|
80
|
+
resolved[entry.key] = Number(override);
|
|
81
|
+
} else {
|
|
82
|
+
resolved[entry.key] = override;
|
|
83
|
+
}
|
|
84
|
+
} else if (entry.default !== undefined) {
|
|
85
|
+
resolved[entry.key] = entry.default;
|
|
86
|
+
} else if (entry.required) {
|
|
87
|
+
warnings.push(
|
|
88
|
+
`Required config "${entry.key}" has no default. Use --config ${entry.key}=VALUE to provide a value.`,
|
|
89
|
+
);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { resolved, warnings };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Item Validation ───────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
const MAX_MESSAGES = 5;
|
|
100
|
+
|
|
101
|
+
function validateItems(
|
|
102
|
+
items: EmittedItem[],
|
|
103
|
+
phase: string,
|
|
104
|
+
): { errors: string[]; warnings: string[] } {
|
|
105
|
+
const errors: string[] = [];
|
|
106
|
+
const warnings: string[] = [];
|
|
107
|
+
|
|
108
|
+
if (items.length > 500) {
|
|
109
|
+
errors.push(`${phase}: emitted ${items.length} items (max 500)`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (items.length === 0) {
|
|
113
|
+
warnings.push(`${phase}: zero items emitted`);
|
|
114
|
+
return { errors, warnings };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const seenIds = new Set<string>();
|
|
118
|
+
let errorCount = 0;
|
|
119
|
+
let warningCount = 0;
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < items.length; i++) {
|
|
122
|
+
const item = items[i];
|
|
123
|
+
const prefix = `${phase} item[${i}]`;
|
|
124
|
+
|
|
125
|
+
if (!item.id || typeof item.id !== "string" || item.id.trim() === "") {
|
|
126
|
+
if (errorCount < MAX_MESSAGES) {
|
|
127
|
+
errors.push(`${prefix}: "id" is missing or empty`);
|
|
128
|
+
}
|
|
129
|
+
errorCount++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hasTitle =
|
|
134
|
+
item.title && typeof item.title === "string" && item.title.trim() !== "";
|
|
135
|
+
const hasSubtitle =
|
|
136
|
+
item.subtitle &&
|
|
137
|
+
typeof item.subtitle === "string" &&
|
|
138
|
+
item.subtitle.trim() !== "";
|
|
139
|
+
if (!hasTitle && !hasSubtitle) {
|
|
140
|
+
if (errorCount < MAX_MESSAGES) {
|
|
141
|
+
errors.push(
|
|
142
|
+
`${prefix}: must have at least one of "title" or "subtitle"`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
errorCount++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (seenIds.has(item.id)) {
|
|
150
|
+
if (warningCount < MAX_MESSAGES) {
|
|
151
|
+
warnings.push(`${prefix}: duplicate id "${item.id}"`);
|
|
152
|
+
}
|
|
153
|
+
warningCount++;
|
|
154
|
+
}
|
|
155
|
+
seenIds.add(item.id);
|
|
156
|
+
|
|
157
|
+
if (item.subtitle !== undefined && typeof item.subtitle !== "string") {
|
|
158
|
+
if (warningCount < MAX_MESSAGES) {
|
|
159
|
+
warnings.push(`${prefix}: "subtitle" should be a string`);
|
|
160
|
+
}
|
|
161
|
+
warningCount++;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (item.url !== undefined) {
|
|
165
|
+
if (typeof item.url !== "string") {
|
|
166
|
+
if (warningCount < MAX_MESSAGES) {
|
|
167
|
+
warnings.push(`${prefix}: "url" should be a string`);
|
|
168
|
+
}
|
|
169
|
+
warningCount++;
|
|
170
|
+
} else if (
|
|
171
|
+
!item.url.startsWith("http://") &&
|
|
172
|
+
!item.url.startsWith("https://")
|
|
173
|
+
) {
|
|
174
|
+
if (warningCount < MAX_MESSAGES) {
|
|
175
|
+
warnings.push(
|
|
176
|
+
`${prefix}: "url" does not start with http(s)://`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
warningCount++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (errorCount > MAX_MESSAGES) {
|
|
185
|
+
errors.push(`... and ${errorCount - MAX_MESSAGES} more errors`);
|
|
186
|
+
}
|
|
187
|
+
if (warningCount > MAX_MESSAGES) {
|
|
188
|
+
warnings.push(`... and ${warningCount - MAX_MESSAGES} more warnings`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { errors, warnings };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── Mock API ──────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
function createMockApi(
|
|
197
|
+
config: Record<string, unknown>,
|
|
198
|
+
): {
|
|
199
|
+
api: any;
|
|
200
|
+
getEmitted: () => EmittedItem[][];
|
|
201
|
+
} {
|
|
202
|
+
const emitted: EmittedItem[][] = [];
|
|
203
|
+
const storage = new Map<string, string>();
|
|
204
|
+
|
|
205
|
+
const api = {
|
|
206
|
+
emit(items: EmittedItem[]) {
|
|
207
|
+
emitted.push(items);
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
async fetch(url: string, options?: any) {
|
|
211
|
+
const timeout = options?.timeout ?? 30000;
|
|
212
|
+
const controller = new AbortController();
|
|
213
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const resp = await globalThis.fetch(url, {
|
|
217
|
+
method: options?.method ?? "GET",
|
|
218
|
+
headers: options?.headers,
|
|
219
|
+
body: options?.body,
|
|
220
|
+
signal: controller.signal,
|
|
221
|
+
});
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
|
|
224
|
+
const text = await resp.text();
|
|
225
|
+
const headers: Record<string, string> = {};
|
|
226
|
+
resp.headers.forEach((v, k) => {
|
|
227
|
+
headers[k] = v;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
let json: unknown;
|
|
231
|
+
try {
|
|
232
|
+
json = JSON.parse(text);
|
|
233
|
+
} catch {
|
|
234
|
+
// not JSON
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
ok: resp.ok,
|
|
239
|
+
status: resp.status,
|
|
240
|
+
headers,
|
|
241
|
+
text,
|
|
242
|
+
json,
|
|
243
|
+
};
|
|
244
|
+
} catch (err: any) {
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
status: 0,
|
|
249
|
+
headers: {},
|
|
250
|
+
text: "",
|
|
251
|
+
error: err?.message ?? String(err),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
log(level: string, message: string) {
|
|
257
|
+
console.log(color.dim(` [source] ${level}: ${message}`));
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
storage: {
|
|
261
|
+
get(key: string) {
|
|
262
|
+
return storage.get(key);
|
|
263
|
+
},
|
|
264
|
+
set(key: string, value: string) {
|
|
265
|
+
storage.set(key, value);
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
config: {
|
|
270
|
+
get(key: string) {
|
|
271
|
+
return config[key];
|
|
272
|
+
},
|
|
273
|
+
getAll() {
|
|
274
|
+
return { ...config };
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
websocket: {
|
|
279
|
+
connect() {
|
|
280
|
+
throw new Error("WebSocket is not supported in test mode");
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
appVersion: "99.0.0",
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return { api, getEmitted: () => emitted };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Main ──────────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
async function main() {
|
|
293
|
+
const args = parseArgs();
|
|
294
|
+
|
|
295
|
+
const manifestPath = path.join(ROOT_DIR, "manifest.yaml");
|
|
296
|
+
const indexPath = path.join(ROOT_DIR, "src", "index.ts");
|
|
297
|
+
|
|
298
|
+
if (!fs.existsSync(manifestPath)) {
|
|
299
|
+
console.error("Error: manifest.yaml not found");
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!fs.existsSync(indexPath)) {
|
|
304
|
+
console.error("Error: src/index.ts not found");
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Read manifest and resolve config
|
|
309
|
+
const manifestContent = fs.readFileSync(manifestPath, "utf-8");
|
|
310
|
+
const manifest = parseYaml(manifestContent);
|
|
311
|
+
const configResult = resolveConfig(manifest.config, args.config);
|
|
312
|
+
|
|
313
|
+
if (!configResult) {
|
|
314
|
+
const warningMessages: string[] = [];
|
|
315
|
+
if (manifest.config) {
|
|
316
|
+
for (const entry of manifest.config as ManifestConfig[]) {
|
|
317
|
+
if (
|
|
318
|
+
args.config[entry.key] === undefined &&
|
|
319
|
+
entry.default === undefined &&
|
|
320
|
+
entry.required
|
|
321
|
+
) {
|
|
322
|
+
warningMessages.push(
|
|
323
|
+
`Required config "${entry.key}" has no default. Use --config ${entry.key}=VALUE to provide a value.`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
console.log(color.yellow("SKIP") + " Missing required config:");
|
|
329
|
+
for (const w of warningMessages) {
|
|
330
|
+
console.log(` ${color.yellow("warning")}: ${w}`);
|
|
331
|
+
}
|
|
332
|
+
process.exit(0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const { resolved: config, warnings } = configResult;
|
|
336
|
+
|
|
337
|
+
console.log(`Testing source...\n`);
|
|
338
|
+
|
|
339
|
+
// Compile TypeScript
|
|
340
|
+
let compiledJs: string;
|
|
341
|
+
try {
|
|
342
|
+
const result = await esbuild.build({
|
|
343
|
+
entryPoints: [indexPath],
|
|
344
|
+
bundle: true,
|
|
345
|
+
platform: "neutral",
|
|
346
|
+
format: "iife",
|
|
347
|
+
globalName: "_source",
|
|
348
|
+
write: false,
|
|
349
|
+
external: ["node:*"],
|
|
350
|
+
footer: { js: "module.exports = _source.default;" },
|
|
351
|
+
});
|
|
352
|
+
compiledJs = result.outputFiles[0].text;
|
|
353
|
+
console.log(" Compiled TypeScript successfully");
|
|
354
|
+
} catch (err: any) {
|
|
355
|
+
console.log(` ${color.red("FAIL")} Compilation failed: ${err.message ?? err}`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Execute compiled code
|
|
360
|
+
const { api, getEmitted } = createMockApi(config);
|
|
361
|
+
const start = Date.now();
|
|
362
|
+
|
|
363
|
+
let sourceMethods: any;
|
|
364
|
+
try {
|
|
365
|
+
const moduleObj = { exports: {} as any };
|
|
366
|
+
const fn = new Function("module", "exports", compiledJs);
|
|
367
|
+
fn(moduleObj, moduleObj.exports);
|
|
368
|
+
|
|
369
|
+
const factory = moduleObj.exports;
|
|
370
|
+
if (typeof factory !== "function") {
|
|
371
|
+
console.log(` ${color.red("FAIL")} Default export is not a function`);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
sourceMethods = await Promise.race([
|
|
376
|
+
factory(api),
|
|
377
|
+
new Promise((_, reject) =>
|
|
378
|
+
setTimeout(() => reject(new Error("Start phase timed out (30s)")), 30000),
|
|
379
|
+
),
|
|
380
|
+
]);
|
|
381
|
+
} catch (err: any) {
|
|
382
|
+
console.log(` ${color.red("FAIL")} Start phase error: ${err.message ?? err}`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Validate start phase items
|
|
387
|
+
const startEmitted = getEmitted();
|
|
388
|
+
const startItems =
|
|
389
|
+
startEmitted.length > 0 ? startEmitted[startEmitted.length - 1] : [];
|
|
390
|
+
const startValidation = validateItems(startItems, "start phase");
|
|
391
|
+
const errors = [...startValidation.errors];
|
|
392
|
+
warnings.push(...startValidation.warnings);
|
|
393
|
+
|
|
394
|
+
// Refresh phase
|
|
395
|
+
let refreshItemCount: number | undefined;
|
|
396
|
+
if (sourceMethods?.refresh) {
|
|
397
|
+
try {
|
|
398
|
+
const emittedBefore = getEmitted().length;
|
|
399
|
+
|
|
400
|
+
await Promise.race([
|
|
401
|
+
sourceMethods.refresh(),
|
|
402
|
+
new Promise((_, reject) =>
|
|
403
|
+
setTimeout(
|
|
404
|
+
() => reject(new Error("Refresh phase timed out (30s)")),
|
|
405
|
+
30000,
|
|
406
|
+
),
|
|
407
|
+
),
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
const allEmitted = getEmitted();
|
|
411
|
+
const refreshEmissions = allEmitted.slice(emittedBefore);
|
|
412
|
+
const refreshItems =
|
|
413
|
+
refreshEmissions.length > 0
|
|
414
|
+
? refreshEmissions[refreshEmissions.length - 1]
|
|
415
|
+
: [];
|
|
416
|
+
refreshItemCount = refreshItems.length;
|
|
417
|
+
|
|
418
|
+
const refreshValidation = validateItems(refreshItems, "refresh phase");
|
|
419
|
+
errors.push(...refreshValidation.errors);
|
|
420
|
+
warnings.push(...refreshValidation.warnings);
|
|
421
|
+
} catch (err: any) {
|
|
422
|
+
errors.push(`Refresh phase error: ${err.message ?? err}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Stop phase
|
|
427
|
+
if (sourceMethods?.stop) {
|
|
428
|
+
try {
|
|
429
|
+
await sourceMethods.stop();
|
|
430
|
+
} catch {
|
|
431
|
+
// ignore stop errors
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const durationMs = Date.now() - start;
|
|
436
|
+
|
|
437
|
+
// ─── Output Results ──────────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
const line = "\u2500".repeat(60);
|
|
440
|
+
const status = errors.length > 0 ? "fail" : "pass";
|
|
441
|
+
|
|
442
|
+
console.log(`\n${color.bold("Test Results")}`);
|
|
443
|
+
console.log(line);
|
|
444
|
+
|
|
445
|
+
const duration = color.dim(`(${durationMs}ms)`);
|
|
446
|
+
|
|
447
|
+
if (status === "pass") {
|
|
448
|
+
const counts =
|
|
449
|
+
refreshItemCount !== undefined
|
|
450
|
+
? `${startItems.length} items, refresh: ${refreshItemCount}`
|
|
451
|
+
: `${startItems.length} items`;
|
|
452
|
+
console.log(` ${color.green("PASS")} ${counts} ${duration}`);
|
|
453
|
+
for (const w of warnings) {
|
|
454
|
+
console.log(` ${color.yellow("warning")}: ${w}`);
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
console.log(` ${color.red("FAIL")} ${duration}`);
|
|
458
|
+
for (const e of errors) {
|
|
459
|
+
console.log(` ${color.red("error")}: ${e}`);
|
|
460
|
+
}
|
|
461
|
+
for (const w of warnings) {
|
|
462
|
+
console.log(` ${color.yellow("warning")}: ${w}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.log(line);
|
|
467
|
+
|
|
468
|
+
esbuild.stop();
|
|
469
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
main().catch((err) => {
|
|
473
|
+
console.error(err);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
});
|