sliftutils 0.6.5 → 0.7.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.
Files changed (47) hide show
  1. package/builders/electronBuild.ts +101 -0
  2. package/builders/electronBuildRun.js +4 -0
  3. package/builders/extensionBuild.ts +15 -9
  4. package/builders/hotReload.ts +96 -0
  5. package/builders/nodeJSBuild.ts +1 -1
  6. package/builders/setup.ts +192 -0
  7. package/builders/setupRun.js +4 -0
  8. package/builders/watch.ts +182 -0
  9. package/builders/watchRun.js +4 -0
  10. package/builders/webBuild.ts +2 -2
  11. package/builders/webBuildRun.js +1 -1
  12. package/builders/webRun.js +4 -0
  13. package/bundler/bundleEntry.ts +17 -9
  14. package/bundler/bundleEntryCaller.ts +1 -1
  15. package/bundler/bundleRequire.ts +21 -3
  16. package/electron/electronIndex.html +12 -0
  17. package/electron/electronMain.ts +29 -0
  18. package/electron/electronRenderer.tsx +33 -0
  19. package/extension/extBackground.ts +22 -0
  20. package/extension/extContentScript.ts +19 -0
  21. package/extension/manifest.json +29 -0
  22. package/misc/environment.ts +28 -0
  23. package/nodejs/server.ts +16 -0
  24. package/package.json +20 -3
  25. package/render-utils/browser.tsx +31 -0
  26. package/render-utils/index.html +12 -0
  27. package/spec.txt +32 -46
  28. package/storage/FileFolderAPI.tsx +1 -1
  29. package/storage/PendingManager.tsx +1 -1
  30. package/web/browser.tsx +37 -0
  31. package/web/index.html +32 -0
  32. /package/{web → render-utils}/DropdownCustom.tsx +0 -0
  33. /package/{web → render-utils}/FullscreenModal.tsx +0 -0
  34. /package/{web → render-utils}/GenericFormat.tsx +0 -0
  35. /package/{web → render-utils}/Input.tsx +0 -0
  36. /package/{web → render-utils}/InputLabel.tsx +0 -0
  37. /package/{web → render-utils}/InputPicker.tsx +0 -0
  38. /package/{web → render-utils}/LocalStorageParam.ts +0 -0
  39. /package/{web → render-utils}/SyncedController.ts +0 -0
  40. /package/{web → render-utils}/SyncedLoadingIndicator.tsx +0 -0
  41. /package/{web → render-utils}/Table.tsx +0 -0
  42. /package/{web → render-utils}/URLParam.ts +0 -0
  43. /package/{web → render-utils}/asyncObservable.ts +0 -0
  44. /package/{web → render-utils}/colors.tsx +0 -0
  45. /package/{web → render-utils}/mobxTyped.ts +0 -0
  46. /package/{web → render-utils}/modal.tsx +0 -0
  47. /package/{web → render-utils}/observer.tsx +0 -0
@@ -0,0 +1,101 @@
1
+ import fs from "fs";
2
+ import { delay } from "socket-function/src/batching";
3
+ import { bundleEntryCaller } from "../bundler/bundleEntryCaller";
4
+ import yargs from "yargs";
5
+ import { formatTime } from "socket-function/src/formatting/format";
6
+ import path from "path";
7
+ import { getAllFiles } from "../misc/fs";
8
+
9
+ async function main() {
10
+ let time = Date.now();
11
+ let yargObj = yargs(process.argv)
12
+ .option("mainEntry", { type: "string", default: "./electron/electronMain.ts", desc: `Path to the main process entry point` })
13
+ .option("rendererEntry", { type: "string", default: "./electron/electronRenderer.tsx", desc: `Path to the renderer process entry point` })
14
+ .option("indexHtml", { type: "string", default: "./electron/electronIndex.html", desc: `Path to the index.html file` })
15
+ .option("assetsFolder", { type: "string", default: "./assets", desc: `Path to the assets folder` })
16
+ .option("outputFolder", { type: "string", default: "./build-electron", desc: `Output folder` })
17
+ .argv || {}
18
+ ;
19
+
20
+
21
+ // Wait for any async functions to load.
22
+ await delay(0);
23
+
24
+ let hasMainEntry = fs.existsSync(yargObj.mainEntry);
25
+ let hasRendererEntry = fs.existsSync(yargObj.rendererEntry);
26
+ let hasIndexHtml = fs.existsSync(yargObj.indexHtml);
27
+ let hasAssets = fs.existsSync(yargObj.assetsFolder);
28
+
29
+ if (!hasMainEntry) {
30
+ throw new Error(`Main process entry point not found at ${yargObj.mainEntry}. Please specify with the --mainEntry option.`);
31
+ }
32
+ if (!hasRendererEntry) {
33
+ throw new Error(`Renderer process entry point not found at ${yargObj.rendererEntry}. Please specify with the --rendererEntry option.`);
34
+ }
35
+
36
+ await fs.promises.mkdir(yargObj.outputFolder, { recursive: true });
37
+
38
+ // Build main and renderer processes in parallel
39
+ await Promise.all([
40
+ bundleEntryCaller({
41
+ entryPoint: yargObj.mainEntry,
42
+ outputFolder: yargObj.outputFolder,
43
+ }),
44
+ bundleEntryCaller({
45
+ entryPoint: yargObj.rendererEntry,
46
+ outputFolder: yargObj.outputFolder,
47
+ })
48
+ ]);
49
+
50
+ // Collect all files to copy
51
+ let filesToCopy: string[] = [];
52
+
53
+ if (hasIndexHtml) {
54
+ filesToCopy.push(yargObj.indexHtml);
55
+ }
56
+
57
+ // Add assets folder files if it exists
58
+ if (hasAssets) {
59
+ for await (const file of getAllFiles(yargObj.assetsFolder)) {
60
+ filesToCopy.push(file);
61
+ }
62
+ }
63
+
64
+ // Copy all files with timestamp checking
65
+ async function getTimestamp(filePath: string): Promise<number> {
66
+ try {
67
+ const stats = await fs.promises.stat(filePath);
68
+ return stats.mtimeMs;
69
+ } catch (error) {
70
+ return 0;
71
+ }
72
+ }
73
+
74
+ let filesCopied = 0;
75
+ let root = path.resolve(".");
76
+ for (const file of filesToCopy) {
77
+ let sourcePath = path.resolve(file);
78
+ if (!fs.existsSync(sourcePath)) {
79
+ console.warn(`Warning: File not found: ${file}`);
80
+ continue;
81
+ }
82
+ let relativePath = path.relative(root, sourcePath);
83
+ let destPath = path.join(yargObj.outputFolder, relativePath);
84
+
85
+ let sourceTimestamp = await getTimestamp(sourcePath);
86
+ let destTimestamp = await getTimestamp(destPath);
87
+ if (sourceTimestamp > destTimestamp) {
88
+ await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
89
+ await fs.promises.cp(sourcePath, destPath);
90
+ filesCopied++;
91
+ }
92
+ }
93
+ if (filesCopied > 0) {
94
+ console.log(`Copied ${filesCopied} changed files`);
95
+ }
96
+
97
+ let duration = Date.now() - time;
98
+ console.log(`Electron build completed in ${formatTime(duration)}`);
99
+ }
100
+ main().catch(console.error).finally(() => process.exit());
101
+
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ require("typenode");
3
+ require("./electronBuild.ts");
4
+
@@ -13,9 +13,9 @@ async function main() {
13
13
  // And copy the manifest.json
14
14
  // AND copy everything in ./assets which has updated
15
15
  let yargObj = yargs(process.argv)
16
- .option("backgroundEntry", { type: "string", default: "./extBackground.ts", desc: `Path to the entry point file` })
17
- .option("contentEntry", { type: "string", default: "./extContentScript.ts", desc: `Path to the entry point file` })
18
- .option("manifestPath", { type: "string", default: "./manifest.json", desc: `Path to the manifest.json file` })
16
+ .option("backgroundEntry", { type: "string", default: "./extension/extBackground.ts", desc: `Path to the entry point file` })
17
+ .option("contentEntry", { type: "string", default: "./extension/extContentScript.ts", desc: `Path to the entry point file` })
18
+ .option("manifestPath", { type: "string", default: "./extension/manifest.json", desc: `Path to the manifest.json file` })
19
19
  .option("assetsFolder", { type: "string", default: "./assets", desc: `Path to the assets folder` })
20
20
  .option("outputFolder", { type: "string", default: "./build-extension", desc: `Output folder` })
21
21
  .argv || {}
@@ -39,19 +39,25 @@ async function main() {
39
39
 
40
40
  await fs.promises.mkdir("./build-extension", { recursive: true });
41
41
 
42
+ // Build background and content scripts in parallel
43
+ let bundlePromises: Promise<void>[] = [];
42
44
  if (hasBackgroundEntry) {
43
- await bundleEntryCaller({
45
+ bundlePromises.push(bundleEntryCaller({
44
46
  entryPoint: yargObj.backgroundEntry,
45
47
  outputFolder: yargObj.outputFolder,
46
- });
48
+ }));
47
49
  }
48
50
  if (hasContentEntry) {
49
- await bundleEntryCaller({
51
+ bundlePromises.push(bundleEntryCaller({
50
52
  entryPoint: yargObj.contentEntry,
51
53
  outputFolder: yargObj.outputFolder,
52
- });
54
+ }));
53
55
  }
54
- await fs.promises.cp(yargObj.manifestPath, path.join(yargObj.outputFolder, "manifest.json"));
56
+
57
+ await Promise.all([
58
+ ...bundlePromises,
59
+ fs.promises.cp(yargObj.manifestPath, path.join(yargObj.outputFolder, "manifest.json"))
60
+ ]);
55
61
 
56
62
  // Parse manifest and collect referenced files
57
63
  let manifestContent = await fs.promises.readFile(yargObj.manifestPath, "utf-8");
@@ -133,6 +139,6 @@ async function main() {
133
139
  }
134
140
 
135
141
  let duration = Date.now() - time;
136
- console.log(`NodeJS build completed in ${formatTime(duration)}`);
142
+ console.log(`Extension build completed in ${formatTime(duration)}`);
137
143
  }
138
144
  main().catch(console.error).finally(() => process.exit());
@@ -0,0 +1,96 @@
1
+ import { isInBrowser, isInChromeExtension, isInChromeExtensionBackground, isInChromeExtensionContentScript } from "../misc/environment";
2
+
3
+ const DEFAULT_WATCH_PORT = 9876;
4
+
5
+ export async function enableHotReloading(config?: {
6
+ port?: number;
7
+ }) {
8
+ if (isInChromeExtensionBackground()) {
9
+ chromeExtensionBackgroundHotReload(config?.port);
10
+ } else if (isInChromeExtensionContentScript()) {
11
+ chromeExtensionContentScriptHotReload();
12
+ } else if (typeof window !== "undefined" && typeof window.location && typeof window.location.reload === "function") {
13
+ // For most reloadable environments, just refresh
14
+ watchPortHotReload(config?.port, () => {
15
+ window.location.reload();
16
+ });
17
+ }
18
+ }
19
+
20
+ function watchPortHotReload(port = DEFAULT_WATCH_PORT, onReload: () => void) {
21
+ let reconnectTimer: number | undefined;
22
+ let ws: WebSocket | undefined;
23
+
24
+ let everConnected = false;
25
+
26
+ function connect() {
27
+ try {
28
+ ws = new WebSocket(`ws://localhost:${port}`);
29
+
30
+ ws.onopen = () => {
31
+ console.log(`[Hot Reload] Connected to watch server on port ${port}`);
32
+ everConnected = true;
33
+ };
34
+
35
+ ws.onmessage = (event) => {
36
+ try {
37
+ let data = JSON.parse(event.data);
38
+ if (data.type === "build-complete" && data.success) {
39
+ console.log("[Hot Reload] Build complete, reloading page...");
40
+ onReload();
41
+ }
42
+ } catch (error) {
43
+ console.error("[Hot Reload] Failed to parse message:", error);
44
+ }
45
+ };
46
+
47
+ ws.onerror = (error) => {
48
+ console.warn(`[Hot Reload] WebSocket error. Use the watch script to enable watching.`);
49
+ };
50
+
51
+ ws.onclose = () => {
52
+ if (everConnected) {
53
+ console.log("[Hot Reload] Disconnected from watch server, reconnecting in 2s...");
54
+ if (reconnectTimer) {
55
+ clearTimeout(reconnectTimer);
56
+ }
57
+ reconnectTimer = setTimeout(() => {
58
+ connect();
59
+ }, 2000) as any;
60
+ }
61
+ };
62
+ } catch (error) {
63
+ console.error("[Hot Reload] Failed to connect:", error);
64
+ }
65
+ }
66
+
67
+ connect();
68
+ }
69
+
70
+ function chromeExtensionBackgroundHotReload(port = DEFAULT_WATCH_PORT) {
71
+ chrome.runtime.onConnect.addListener((port) => {
72
+ if (port.name === "hotReload") {
73
+ // Keep the port open so content scripts can detect when we disconnect
74
+ }
75
+ });
76
+
77
+ watchPortHotReload(port, () => {
78
+ chrome.runtime.reload();
79
+ });
80
+ }
81
+
82
+ function chromeExtensionContentScriptHotReload() {
83
+ let port = chrome.runtime.connect({ name: "hotReload" });
84
+
85
+ let startTime = Date.now();
86
+
87
+ port.onDisconnect.addListener(() => {
88
+ let timeToFail = Date.now() - startTime;
89
+ if (timeToFail > 10000) {
90
+ console.warn("[Hot Reload] Could not connect to background script. Make sure the background script calls enableHotReloading().");
91
+ return;
92
+ }
93
+ console.log("[Hot Reload] Extension reloaded, refreshing page...");
94
+ window.location.reload();
95
+ });
96
+ }
@@ -7,7 +7,7 @@ import { formatTime } from "socket-function/src/formatting/format";
7
7
  async function main() {
8
8
  let time = Date.now();
9
9
  let yargObj = yargs(process.argv)
10
- .option("entryPoint", { type: "string", default: "./server.ts", desc: `Path to the entry point file` })
10
+ .option("entryPoint", { type: "string", default: "./nodejs/server.ts", desc: `Path to the entry point file` })
11
11
  .option("outputFolder", { type: "string", default: "./build-nodejs", desc: `Output folder` })
12
12
  .argv || {}
13
13
  ;
@@ -0,0 +1,192 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ async function main() {
5
+ let targetDir = process.cwd();
6
+ let sourceDir = path.join(__dirname, "..");
7
+
8
+ console.log("Setting up sliftutils project...");
9
+ console.log(`Source: ${sourceDir}`);
10
+ console.log(`Target: ${targetDir}`);
11
+
12
+ // Directories and files to copy
13
+ let directoriesToScan = ["electron", "extension", "web", "nodejs", "assets", ".vscode"];
14
+ let rootFiles = [".cursorrules", ".eslintrc.js", ".gitignore", "tsconfig.json"];
15
+
16
+ // Import path mappings to convert relative imports to package imports
17
+ let importMappings: { [key: string]: string } = {
18
+ "../builders/": "sliftutils/builders/",
19
+ "../misc/": "sliftutils/misc/",
20
+ "../render-utils/": "sliftutils/render-utils/",
21
+ "../bundler/": "sliftutils/bundler/",
22
+ "../storage/": "sliftutils/storage/",
23
+ };
24
+
25
+ // Gather all files to copy
26
+ let filesToCopy: string[] = [];
27
+
28
+ // Add root files
29
+ for (let file of rootFiles) {
30
+ let sourcePath = path.join(sourceDir, file);
31
+ if (fs.existsSync(sourcePath)) {
32
+ filesToCopy.push(file);
33
+ }
34
+ }
35
+
36
+ // Gather files from directories
37
+ for (let dir of directoriesToScan) {
38
+ let sourcePath = path.join(sourceDir, dir);
39
+ if (fs.existsSync(sourcePath)) {
40
+ let filesInDir = gatherFilesRecursive(sourcePath, sourceDir);
41
+ filesToCopy.push(...filesInDir);
42
+ }
43
+ }
44
+
45
+ // Copy all files
46
+ console.log(`\nFound ${filesToCopy.length} files to copy\n`);
47
+
48
+ for (let relativePath of filesToCopy) {
49
+ let sourcePath = path.join(sourceDir, relativePath);
50
+ let targetPath = path.join(targetDir, relativePath);
51
+
52
+ // Check if target already exists
53
+ if (fs.existsSync(targetPath)) {
54
+ console.log(`Skipping ${relativePath} (already exists)`);
55
+ continue;
56
+ }
57
+
58
+ // Create directory if needed
59
+ let targetDirPath = path.dirname(targetPath);
60
+ if (!fs.existsSync(targetDirPath)) {
61
+ fs.mkdirSync(targetDirPath, { recursive: true });
62
+ }
63
+
64
+ // Copy file with import processing for .ts/.tsx files
65
+ if (relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) {
66
+ let content = fs.readFileSync(sourcePath, "utf8");
67
+ let processedContent = replaceImports(content, importMappings);
68
+ fs.writeFileSync(targetPath, processedContent, "utf8");
69
+ console.log(`Copied ${relativePath} (with import mapping)`);
70
+ } else {
71
+ fs.copyFileSync(sourcePath, targetPath);
72
+ console.log(`Copied ${relativePath}`);
73
+ }
74
+ }
75
+
76
+ // Update package.json with scripts
77
+ let packageJsonPath = path.join(targetDir, "package.json");
78
+ if (fs.existsSync(packageJsonPath)) {
79
+ console.log("\nUpdating package.json scripts...");
80
+ updatePackageJson(packageJsonPath);
81
+ } else {
82
+ console.warn("\nNo package.json found in target directory");
83
+ }
84
+
85
+ console.log("\nSetup complete!");
86
+ }
87
+
88
+ function gatherFilesRecursive(dir: string, baseDir: string): string[] {
89
+ let files: string[] = [];
90
+ let entries = fs.readdirSync(dir, { withFileTypes: true });
91
+
92
+ for (let entry of entries) {
93
+ let fullPath = path.join(dir, entry.name);
94
+
95
+ if (entry.isDirectory()) {
96
+ // Skip dist directories
97
+ if (entry.name === "dist") {
98
+ continue;
99
+ }
100
+ let subFiles = gatherFilesRecursive(fullPath, baseDir);
101
+ files.push(...subFiles);
102
+ } else {
103
+ // Add file as relative path from base directory
104
+ let relativePath = path.relative(baseDir, fullPath);
105
+ files.push(relativePath);
106
+ }
107
+ }
108
+
109
+ return files;
110
+ }
111
+
112
+ function replaceImports(content: string, importMappings: { [key: string]: string }): string {
113
+ let lines = content.split("\n");
114
+ let processedLines = lines.map(line => {
115
+ // Check if line contains an import or require statement
116
+ if (line.includes("import") || line.includes("require")) {
117
+ let processedLine = line;
118
+ for (let [oldPath, newPath] of Object.entries(importMappings)) {
119
+ processedLine = processedLine.replace(new RegExp(oldPath.replace(/\//g, "\\/"), "g"), newPath);
120
+ }
121
+ return processedLine;
122
+ }
123
+ return line;
124
+ });
125
+ return processedLines.join("\n");
126
+ }
127
+
128
+ function updatePackageJson(packageJsonPath: string) {
129
+ let packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
130
+
131
+ // Read our current package.json to get the type script
132
+ let sourcePackageJsonPath = path.join(__dirname, "..", "package.json");
133
+ let sourcePackageJson = JSON.parse(fs.readFileSync(sourcePackageJsonPath, "utf8"));
134
+
135
+ if (!packageJson.scripts) {
136
+ packageJson.scripts = {};
137
+ }
138
+
139
+ // Add type script
140
+ if (!packageJson.scripts.type) {
141
+ packageJson.scripts.type = sourcePackageJson.scripts.type;
142
+ console.log(" Added 'type' script");
143
+ }
144
+
145
+ // Add run commands
146
+ let runCommands = ["run-nodejs", "run-nodejs-dev", "run-web", "run-electron"];
147
+ for (let cmd of runCommands) {
148
+ if (!packageJson.scripts[cmd]) {
149
+ packageJson.scripts[cmd] = sourcePackageJson.scripts[cmd];
150
+ console.log(` Added '${cmd}' script`);
151
+ }
152
+ }
153
+
154
+ // Add build commands using bin references
155
+ let buildCommands = {
156
+ "build-nodejs": "build-nodejs",
157
+ "build-web": "build-web",
158
+ "build-extension": "build-extension",
159
+ "build-electron": "build-electron",
160
+ };
161
+
162
+ for (let [scriptName, binName] of Object.entries(buildCommands)) {
163
+ if (!packageJson.scripts[scriptName]) {
164
+ packageJson.scripts[scriptName] = binName;
165
+ console.log(` Added '${scriptName}' script`);
166
+ }
167
+ }
168
+
169
+ // Add watch commands with ports
170
+ let watchCommands = {
171
+ "watch-nodejs": "slift-watch --port 9876 \"nodejs/*.ts\" \"nodejs/*.tsx\" \"yarn build-nodejs\"",
172
+ "watch-web": "slift-watch --port 9877 \"web/*.ts\" \"web/*.tsx\" \"yarn build-web\"",
173
+ "watch-extension": "slift-watch --port 9878 \"extension/*.ts\" \"extension/*.tsx\" \"yarn build-extension\"",
174
+ "watch-electron": "slift-watch --port 9879 \"electron/*.ts\" \"electron/*.tsx\" \"yarn build-electron\"",
175
+ };
176
+
177
+ for (let [scriptName, command] of Object.entries(watchCommands)) {
178
+ if (!packageJson.scripts[scriptName]) {
179
+ packageJson.scripts[scriptName] = command;
180
+ console.log(` Added '${scriptName}' script`);
181
+ }
182
+ }
183
+
184
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, undefined, 4) + "\n", "utf8");
185
+ console.log(" package.json updated");
186
+ }
187
+
188
+ main().catch(error => {
189
+ console.error("Setup failed:", error);
190
+ process.exit(1);
191
+ });
192
+
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ require("typenode");
3
+ require("./setup.ts");
4
+
@@ -0,0 +1,182 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { runPromise } from "socket-function/src/runPromise";
4
+ import { WebSocketServer } from "ws";
5
+
6
+ const DEBOUNCE_DELAY_MS = 250;
7
+ const DEFAULT_WATCH_PORT = 9876;
8
+
9
+ async function main() {
10
+ let args = process.argv.slice(2);
11
+
12
+ if (args.length < 2) {
13
+ console.error("Usage: watch [--port PORT] <pattern1> [pattern2...] <command>");
14
+ console.error("Example: watch --port 9877 '*.ts' '*.tsx' 'yarn build'");
15
+ console.error(" - Patterns with wildcards must match the entire path");
16
+ console.error(" - Patterns without wildcards can appear anywhere in the path");
17
+ process.exit(1);
18
+ }
19
+
20
+ // Parse port parameter if present
21
+ let watchPort = DEFAULT_WATCH_PORT;
22
+ let argIndex = 0;
23
+
24
+ if (args[argIndex] === "--port") {
25
+ if (args.length < 4) {
26
+ console.error("--port requires a value");
27
+ process.exit(1);
28
+ }
29
+ watchPort = parseInt(args[argIndex + 1], 10);
30
+ if (isNaN(watchPort)) {
31
+ console.error(`Invalid port number: ${args[argIndex + 1]}`);
32
+ process.exit(1);
33
+ }
34
+ argIndex += 2;
35
+ }
36
+
37
+ let remainingArgs = args.slice(argIndex);
38
+ let patterns = remainingArgs.slice(0, -1);
39
+ let command = remainingArgs[remainingArgs.length - 1];
40
+
41
+ let currentDirectory = process.cwd();
42
+
43
+ // Setup WebSocket server (unless disabled with port <= 0)
44
+ let wss: WebSocketServer | undefined;
45
+ if (watchPort > 0) {
46
+ wss = new WebSocketServer({ port: watchPort });
47
+ console.log(`WebSocket server listening on port ${watchPort}. Use --port parameter to change this. Set it to 0 to disable watching.`);
48
+
49
+ wss.on("connection", (ws) => {
50
+ console.log(`[${new Date().toLocaleTimeString()}] WebSocket client connected`);
51
+ ws.on("close", () => {
52
+ console.log(`[${new Date().toLocaleTimeString()}] WebSocket client disconnected`);
53
+ });
54
+ });
55
+ } else {
56
+ console.log(`WebSocket server disabled (port <= 0)`);
57
+ }
58
+
59
+ console.log(`Watching patterns: ${patterns.join(", ")}`);
60
+ console.log(`Running command: ${command}`);
61
+ console.log("");
62
+
63
+ let debounceTimer: NodeJS.Timeout | undefined;
64
+ let isRunning = false;
65
+ let needsRerun = false;
66
+
67
+ async function executeCommand() {
68
+ if (isRunning) {
69
+ needsRerun = true;
70
+ return;
71
+ }
72
+
73
+ isRunning = true;
74
+ needsRerun = false;
75
+
76
+ try {
77
+ console.log(`\n[${new Date().toLocaleTimeString()}] Running: ${command}`);
78
+ await runPromise(command);
79
+ console.log(`[${new Date().toLocaleTimeString()}] Completed successfully`);
80
+
81
+ // Notify all connected WebSocket clients
82
+ if (wss) {
83
+ wss.clients.forEach((client) => {
84
+ if (client.readyState === 1) { // 1 = OPEN
85
+ client.send(JSON.stringify({ type: "build-complete", success: true }));
86
+ }
87
+ });
88
+ }
89
+ } catch (error) {
90
+ console.error(`[${new Date().toLocaleTimeString()}] Error:`, error);
91
+
92
+ // Notify clients about build error
93
+ if (wss) {
94
+ wss.clients.forEach((client) => {
95
+ if (client.readyState === 1) { // 1 = OPEN
96
+ client.send(JSON.stringify({ type: "build-complete", success: false }));
97
+ }
98
+ });
99
+ }
100
+ } finally {
101
+ isRunning = false;
102
+
103
+ if (needsRerun) {
104
+ console.log(`[${new Date().toLocaleTimeString()}] Detected changes during build, running again...`);
105
+ await executeCommand();
106
+ }
107
+ }
108
+ }
109
+
110
+ function scheduleRun() {
111
+ if (debounceTimer) {
112
+ clearTimeout(debounceTimer);
113
+ }
114
+
115
+ debounceTimer = setTimeout(() => {
116
+ void executeCommand();
117
+ }, DEBOUNCE_DELAY_MS);
118
+ }
119
+
120
+ function matchesPattern(filePath: string, pattern: string): boolean {
121
+ let relativePath = path.relative(currentDirectory, filePath).replace(/\\/g, "/").toLowerCase();
122
+ let lowerPattern = pattern.toLowerCase();
123
+
124
+ // If pattern contains wildcards, do full pattern matching
125
+ if (lowerPattern.includes("*")) {
126
+ // Convert wildcard pattern to regex
127
+ let regexPattern = lowerPattern
128
+ .replace(/\./g, "\\.") // Escape dots
129
+ .replace(/\*/g, ".*"); // * matches anything
130
+ let regex = new RegExp(`^${regexPattern}$`);
131
+ return regex.test(relativePath);
132
+ } else {
133
+ // No wildcards - pattern can appear anywhere in the path
134
+ return relativePath.includes(lowerPattern);
135
+ }
136
+ }
137
+
138
+ function shouldWatch(filePath: string): boolean {
139
+ for (let pattern of patterns) {
140
+ if (matchesPattern(filePath, pattern)) {
141
+ return true;
142
+ }
143
+ }
144
+ return false;
145
+ }
146
+
147
+ function watchDirectory(dir: string) {
148
+ try {
149
+ let watcher = fs.watch(dir, { recursive: true }, (eventType, filename) => {
150
+ if (!filename) return;
151
+
152
+ let fullPath = path.join(dir, filename);
153
+
154
+ if (shouldWatch(fullPath)) {
155
+ console.log(`[${new Date().toLocaleTimeString()}] Detected change: ${path.relative(currentDirectory, fullPath)}`);
156
+ scheduleRun();
157
+ }
158
+ });
159
+
160
+ watcher.on("error", (error) => {
161
+ console.error(`Watch error for ${dir}:`, error);
162
+ });
163
+ } catch (error) {
164
+ console.error(`Failed to watch directory ${dir}:`, error);
165
+ }
166
+ }
167
+
168
+ // Start watching the current directory
169
+ watchDirectory(currentDirectory);
170
+
171
+ // Run the command once on startup
172
+ console.log("Initial build starting...");
173
+ await executeCommand();
174
+
175
+ console.log("\nWatching for changes... (Press Ctrl+C to exit)");
176
+ }
177
+
178
+ main().catch(error => {
179
+ console.error("Fatal error:", error);
180
+ process.exit(1);
181
+ });
182
+
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ require("typenode");
3
+ require("./watch.ts");
4
+
@@ -9,8 +9,8 @@ import { getAllFiles } from "../misc/fs";
9
9
  async function main() {
10
10
  let time = Date.now();
11
11
  let yargObj = yargs(process.argv)
12
- .option("entryPoint", { type: "string", default: "./browser.tsx", desc: `Path to the entry point file` })
13
- .option("indexHtml", { type: "string", default: "./index.html", desc: `Path to the index.html file` })
12
+ .option("entryPoint", { type: "string", default: "./web/browser.tsx", desc: `Path to the entry point file` })
13
+ .option("indexHtml", { type: "string", default: "./web/index.html", desc: `Path to the index.html file` })
14
14
  .option("assetsFolder", { type: "string", default: "./assets", desc: `Path to the assets folder` })
15
15
  .option("outputFolder", { type: "string", default: "./build-web", desc: `Output folder` })
16
16
  .argv || {}
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- require("esbuild-runner/register");
2
+ require("typenode");
3
3
  require("./webBuild.ts");
4
4
 
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ const open = require("open");
3
+ open("./build-web/web/index.html");
4
+