From 78645dcd0b81fa403dcdc2e0087089c1f83d297a Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Sun, 14 Jun 2026 18:17:17 -0500 Subject: [PATCH 01/14] add TS TPA support on dev --- package.json | 104 ++++---- src/commands/dev/index.ts | 97 +++---- src/server/api/index.ts | 178 +++++++------ src/server/fs/index.ts | 515 ++++++++++++++++++++------------------ src/tools/build-ts-tpa.ts | 260 +++++++++++++++++++ tsconfig.json | 21 +- 6 files changed, 748 insertions(+), 427 deletions(-) create mode 100644 src/tools/build-ts-tpa.ts diff --git a/package.json b/package.json index 44c0ec2..caf3dca 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,54 @@ { - "name": "v7cli", - "version": "1.0.15", - "main": "index.js", - "scripts": { - "cli:publish": "npx tsc && npm publish" - }, - "bin": { - "v7cli": "dist/index.js" - }, - "type": "commonjs", - "keywords": [], - "author": "Izaak Kuipers (https://izkuipers.nl/)", - "license": "MIT", - "description": "A command-line tool for developing ArcOS apps", - "devDependencies": { - "@types/node": "^22.15.19", - "ts-node": "^10.9.2", - "typescript": "^5.8.3" - }, - "files": [ - "./dist", - "./package.json", - "./README.md", - "./LICENSE" - ], - "dependencies": { - "@clack/prompts": "^0.10.1", - "@types/colors": "^1.2.4", - "@types/cookie-parser": "^1.4.8", - "@types/cors": "^2.8.18", - "@types/express": "^5.0.2", - "@types/mime": "^4.0.0", - "@types/mime-types": "^2.1.4", - "@types/multer": "^1.4.12", - "@types/signale": "^1.4.7", - "@types/socket.io": "^3.0.2", - "axios": "^1.9.0", - "check-disk-space": "^3.4.0", - "colors.js": "^1.2.4", - "commander": "^14.0.0", - "cookie-parser": "^1.4.7", - "cors": "^2.8.5", - "express.js": "^1.0.0", - "mime": "^4.0.7", - "mime-types": "^3.0.1", - "multer": "^2.0.0", - "signale": "^1.4.0", - "socket.io": "^4.8.1", - "socket.io-server": "^1.0.0-b", - "tslib": "^2.8.1", - "zip-a-folder": "^3.1.9" - } + "name": "v7cli", + "version": "1.0.19", + "main": "index.js", + "scripts": { + "cli:publish": "npx tsc && npm publish" + }, + "bin": { + "v7cli": "dist/index.js" + }, + "type": "commonjs", + "keywords": [], + "author": "Izaak Kuipers (https://izkuipers.nl/)", + "license": "MIT", + "description": "A command-line tool for developing ArcOS apps", + "devDependencies": { + "@types/node": "^22.15.19", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "files": [ + "./dist", + "./package.json", + "./README.md", + "./LICENSE" + ], + "dependencies": { + "@clack/prompts": "^0.10.1", + "@types/colors": "^1.2.4", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.18", + "@types/express": "^5.0.2", + "@types/mime": "^4.0.0", + "@types/mime-types": "^2.1.4", + "@types/multer": "^1.4.12", + "@types/signale": "^1.4.7", + "@types/socket.io": "^3.0.2", + "axios": "^1.9.0", + "check-disk-space": "^3.4.0", + "colors.js": "^1.2.4", + "commander": "^14.0.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "express.js": "^1.0.0", + "mime": "^4.0.7", + "mime-types": "^3.0.1", + "multer": "^2.0.0", + "signale": "^1.4.0", + "socket.io": "^4.8.1", + "socket.io-server": "^1.0.0-b", + "tslib": "^2.8.1", + "zip-a-folder": "^3.1.9" + } } diff --git a/src/commands/dev/index.ts b/src/commands/dev/index.ts index f013066..30e710a 100644 --- a/src/commands/dev/index.ts +++ b/src/commands/dev/index.ts @@ -5,54 +5,67 @@ import { Project } from "../../project"; import { StartServer } from "../../server/api"; import signale from "signale"; import { getArcBuild } from "../../build"; +import { readdirSync } from "fs"; +import buildTSTPA, { tsFileRegex } from "../../tools/build-ts-tpa"; export default async function DevCommand() { - const project = new Project(cwd()); + const project = new Project(cwd()); - await project.readProjectFile(); + await project.readProjectFile(); - if (await project.areTypeDefsOutdated()) { - signale.warn( - "Type definitions are outdated. Please run `npx v7cli update` to update them." - ); - } + if (await project.areTypeDefsOutdated()) { + signale.warn( + "Type definitions are outdated. Please run `npx v7cli update` to update them.", + ); + } + + const appId = project.metadata?.metadata.appId; + + if (appId?.includes(".") || appId?.includes("-")) { + signale.error( + "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa", + ); + + process.exit(1); + } + + const buildHash = await getArcBuild(); - const appId = project.metadata?.metadata.appId; + if ( + project.metadata?.buildHash == null || + project.metadata.buildHash != buildHash + ) { + project.metadata!!.buildHash = buildHash; + } - if (appId?.includes(".") || appId?.includes("-")) { - signale.error( - "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa" + const containsTypescript = readdirSync(project.metadata!.payloadDir).some( + (val) => tsFileRegex.test(val), ); - process.exit(1); - } - - const buildHash = await getArcBuild() - - if (project.metadata?.buildHash == null || project.metadata.buildHash != buildHash) { - project.metadata!!.buildHash = buildHash; - } - - await StartServer(project); - - const name = "ArcOS v7 CLI".green.bold; - const version = `v${packageJson.version}`.gray; - const arrow = " > ".gray; - const port = project.metadata?.devPort || 3128; - const url = `http://localhost:${port}`; - const lis = "Listening on:".yellow; - const run = "Run command:".yellow; - const command = `devenv connect ${port}`.blue; - - console.log( - [ - "", - ` ${name} ${version}`, - "", - `${arrow}${lis} ${url}`, - `${arrow}${run} ${command} in ArcTerm to connect`, - "", - " READY.".green.bold, - ].join("\n") - ); + if (containsTypescript) { + buildTSTPA(project.path); + } + + await StartServer(project); + + const name = "ArcOS v7 CLI".green.bold; + const version = `v${packageJson.version}`.gray; + const arrow = " > ".gray; + const port = project.metadata?.devPort || 3128; + const url = `http://localhost:${port}`; + const lis = "Listening on:".yellow; + const run = "Run command:".yellow; + const command = `devenv connect ${port}`.blue; + + console.log( + [ + "", + ` ${name} ${version}`, + "", + `${arrow}${lis} ${url}`, + `${arrow}${run} ${command} in ArcTerm to connect`, + "", + " READY.".green.bold, + ].join("\n"), + ); } diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 3e214d0..a9ba3ff 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -9,109 +9,133 @@ import { RouteStore, RouteType } from "../../types/project"; import { corsOptions } from "./cors"; import { Routes } from "./routes"; import { SockLog, WebSock } from "../websocket"; -import { watch } from "fs"; +import { readdirSync, watch } from "fs"; import { join } from "path"; import "colors"; import signale, { Signale } from "signale"; +import buildTSTPA, { tsFileRegex } from "../../tools/build-ts-tpa"; export const App = express(); export const APILog = new Signale({ - scope: "API", - interactive: false, + scope: "API", + interactive: false, }); App.use(cors(corsOptions), cookieParser(), multer().any() as any); export async function StartServer(project: Project) { - App.options("*", cors(corsOptions)); - App.post("/fs/file/:path(*)", express.raw({ type: "*/*", limit: "1000mb" })); - App.set("trust proxy", true); - - return new Promise((r) => { - const server = App.listen(project.metadata?.devPort || 3128, () => { - assignRoutes(project, ...Routes()); - - project.websock = new WebSock(server, project.metadata!); - project.websock.start(); - - if (project.metadata?.noHotRelaunch) { - signale.warn("noHotRelaunch is set: not enabling file watcher."); - - return r(); - } - - let watchTimeout: NodeJS.Timeout | undefined; - - watch( - join(project.path, project.metadata!.payloadDir), - { persistent: true, recursive: true }, - (e, filename) => { - if (!watchTimeout) { - if (filename?.endsWith(".css")) { - APILog.warn(`${filename || e}: Change detected, reloading CSS`); - project.websock?.client?.sock.emit("refresh-css", filename); - } else { - APILog.warn( - `${filename || e}: Change detected, restarting ${ - project.metadata?.metadata.appId - }` - ); - project.websock?.client?.sock.emit("restart-tpa"); + App.options("*", cors(corsOptions)); + App.post( + "/fs/file/:path(*)", + express.raw({ type: "*/*", limit: "1000mb" }), + ); + App.set("trust proxy", true); + + const containsTypescript = readdirSync(project.metadata!.payloadDir).some( + (val) => tsFileRegex.test(val), + ); + + return new Promise((r) => { + const server = App.listen(project.metadata?.devPort || 3128, () => { + assignRoutes(project, ...Routes()); + + project.websock = new WebSock(server, project.metadata!); + project.websock.start(); + + if (project.metadata?.noHotRelaunch) { + signale.warn( + "noHotRelaunch is set: not enabling file watcher.", + ); + + return r(); } - watchTimeout = setTimeout(() => (watchTimeout = undefined), 200); - } - } - ); - r(); + let watchTimeout: NodeJS.Timeout | undefined; + let watchTimeoutTS = false; + + watch( + join(project.path, project.metadata!.payloadDir), + { persistent: true, recursive: true }, + async (e, filename) => { + if (!watchTimeout && !watchTimeoutTS) { + if (containsTypescript) watchTimeoutTS = true; + if (filename?.endsWith(".css")) { + APILog.warn( + `${filename || e}: Change detected, reloading CSS`, + ); + project.websock?.client?.sock.emit( + "refresh-css", + filename, + ); + } else { + APILog.warn( + `${filename || e}: Change detected, restarting ${ + project.metadata?.metadata.appId + }`, + ); + if (containsTypescript) { + await buildTSTPA(project.path); + } + project.websock?.client?.sock.emit("restart-tpa"); + } + watchTimeoutTS = false; + watchTimeout = setTimeout( + () => (watchTimeout = undefined), + 200, + ); + } + }, + ); + + r(); + }); }); - }); } export function assignRoute(route: RouteType, project: Project) { - const fun = MethodTranslations()[route.method]; + const fun = MethodTranslations()[route.method]; - fun(route.path, (req: Request, res: Response) => { - const stop = (c = 400, json?: object) => { - if (json) { - res.status(c).json(json); - } else { - res.status(c).end(); - } + fun(route.path, (req: Request, res: Response) => { + const stop = (c = 400, json?: object) => { + if (json) { + res.status(c).json(json); + } else { + res.status(c).end(); + } - return ""; - }; + return ""; + }; - try { - route.callback(req, res, stop, project); - APILog.info(`${route.method.blue} ${route.path}`); - } catch (e) { - APILog.error(`${route.method.blue} ${route.path}: ${`${e}`.red}`); - stop(500); - } + try { + route.callback(req, res, stop, project); + APILog.info(`${route.method.blue} ${route.path}`); + } catch (e) { + APILog.error(`${route.method.blue} ${route.path}: ${`${e}`.red}`); + stop(500); + } - return; - }); + return; + }); - return true; + return true; } export function assignRoutes(project: Project, ...stores: RouteStore[]) { - for (const store of stores) { - for (const [method, path, callback, maxRequests] of store) { - assignRoute({ method, path, callback, maxRequests }, project); + for (const store of stores) { + for (const [method, path, callback, maxRequests] of store) { + assignRoute({ method, path, callback, maxRequests }, project); + } } - } } export function MethodTranslations(): Record any> { - return { - get: App.get.bind(App), - post: App.post.bind(App), - options: App.options.bind(App), - delete: App.delete.bind(App), - put: App.put.bind(App), - all: App.all.bind(App), - patch: App.patch.bind(App), - head: App.head.bind(App), - }; + return { + get: App.get.bind(App), + post: App.post.bind(App), + options: App.options.bind(App), + delete: App.delete.bind(App), + put: App.put.bind(App), + all: App.all.bind(App), + patch: App.patch.bind(App), + head: App.head.bind(App), + }; } diff --git a/src/server/fs/index.ts b/src/server/fs/index.ts index 473b2de..63e1f1c 100644 --- a/src/server/fs/index.ts +++ b/src/server/fs/index.ts @@ -1,303 +1,326 @@ -import { createReadStream, existsSync, statSync } from "fs"; +import { createReadStream, existsSync, readdirSync, statSync } from "fs"; import fs from "fs/promises"; import mime from "mime-types"; import path, { join } from "path"; import { tryJsonParse } from "../../json"; import { Project } from "../../project"; import { - DirectoryReadReturn, - FileEntry, - FolderEntry, - RecursiveDirectoryReadReturn, - UserQuota, + DirectoryReadReturn, + FileEntry, + FolderEntry, + RecursiveDirectoryReadReturn, + UserQuota, } from "../../types/fs"; import { platform } from "os"; import checkDiskSpace from "check-disk-space"; +import { tsFileRegex } from "../../tools/build-ts-tpa"; export class Filesystem { - private path: string; - accessors: Record = {}; // R + private path: string; + accessors: Record = {}; // R + + constructor(projectPath: string, payloadDir: string) { + const containsTypescript = readdirSync(payloadDir).some((val) => + tsFileRegex.test(val), + ); + + if (containsTypescript) { + this.path = join(projectPath, "dist"); + } else { + this.path = join(projectPath, payloadDir); + } + } - constructor(projectPath: string, payloadDir: string) { - this.path = join(projectPath, payloadDir); - } + private resolvePath(relativePath?: string): string { + const resolvedPath = relativePath + ? path.resolve(this.path, relativePath) + : this.path; - private resolvePath(relativePath?: string): string { - const resolvedPath = relativePath - ? path.resolve(this.path, relativePath) - : this.path; + if (!resolvedPath.startsWith(this.path)) + throw new Error("Invalid path; breaks out of project payload"); - if (!resolvedPath.startsWith(this.path)) - throw new Error("Invalid path; breaks out of project payload"); + return resolvedPath; + } - return resolvedPath; - } + private async calculateFolderSize(folderPath?: string) { + const resolvedPath = this.resolvePath(folderPath); - private async calculateFolderSize(folderPath?: string) { - const resolvedPath = this.resolvePath(folderPath); + const calculateSize = async (directory: string): Promise => { + const entries = await fs.readdir(directory, { + withFileTypes: true, + }); - const calculateSize = async (directory: string): Promise => { - const entries = await fs.readdir(directory, { withFileTypes: true }); + let totalSize = 0; - let totalSize = 0; + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + totalSize += await calculateSize(entryPath); + } else if (entry.isFile()) { + totalSize += statSync(entryPath).size; + } + } - for (const entry of entries) { - const entryPath = path.join(directory, entry.name); - if (entry.isDirectory()) { - totalSize += await calculateSize(entryPath); - } else if (entry.isFile()) { - totalSize += statSync(entryPath).size; - } - } + return totalSize; + }; - return totalSize; - }; + return calculateSize(resolvedPath); + } - return calculateSize(resolvedPath); - } + private async countFiles(folderPath?: string) { + const resolvedPath = this.resolvePath(folderPath); - private async countFiles(folderPath?: string) { - const resolvedPath = this.resolvePath(folderPath); + const calculate = async (directory: string): Promise => { + let count = 0; + const entries = await fs.readdir(directory, { + withFileTypes: true, + }); - const calculate = async (directory: string): Promise => { - let count = 0; - const entries = await fs.readdir(directory, { withFileTypes: true }); + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); - for (const entry of entries) { - const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) count += await calculate(entryPath); - if (entry.isDirectory()) count += await calculate(entryPath); + count++; + } - count++; - } + return count; + }; - return count; - }; + return await calculate(resolvedPath); + } - return await calculate(resolvedPath); - } + private async countFolders(folderPath?: string) { + const resolvedPath = this.resolvePath(folderPath); - private async countFolders(folderPath?: string) { - const resolvedPath = this.resolvePath(folderPath); + const calculate = async (directory: string): Promise => { + let count = 0; + const entries = await fs.readdir(directory, { + withFileTypes: true, + }); - const calculate = async (directory: string): Promise => { - let count = 0; - const entries = await fs.readdir(directory, { withFileTypes: true }); + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); - for (const entry of entries) { - const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + count++; + count += await calculate(entryPath); + } + } - if (entry.isDirectory()) { - count++; - count += await calculate(entryPath); - } - } - - return count; - }; - - return await calculate(resolvedPath); - } - - public async writeFile(filePath: string, content: string | Buffer) { - const resolvedPath = this.resolvePath(filePath); - - await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); - await fs.writeFile(resolvedPath, content); - } - - public async readFile(filePath: string): Promise { - const resolvedPath = this.resolvePath(filePath); - - return fs.readFile(resolvedPath); - } - - public async pipeFile(filePath: string) { - const resolvedPath = this.resolvePath(filePath); - - return createReadStream(resolvedPath); - } - - public async createDirectory(dirPath: string): Promise { - const resolvedPath = await this.resolvePath(dirPath); - - await fs.mkdir(resolvedPath, { recursive: true }); - } - - public async readDirectory( - dirPath?: string, - populateShortcuts = true - ): Promise { - const resolvedPath = this.resolvePath(dirPath); - const dirEntries = await fs.readdir(resolvedPath, { withFileTypes: true }); - const size = await this.calculateFolderSize(dirPath); - const fileCount = await this.countFiles(dirPath); - const folderCount = await this.countFolders(dirPath); - const shortcuts = populateShortcuts - ? await this.bulk(".arclnk", dirPath) - : {}; - const directoryReadReturn: DirectoryReadReturn = { - dirs: [], - files: [], - totalSize: size, - totalFiles: fileCount, - totalFolders: folderCount, - shortcuts, - }; - - for (const entry of dirEntries) { - const entryPath = path.join(resolvedPath, entry.name); - const stats = statSync(entryPath); - - if (entry.isDirectory()) { - const folderEntry: FolderEntry = { - itemId: "", - name: entry.name, - dateCreated: stats.birthtime, - dateModified: stats.mtime, + return count; }; - directoryReadReturn.dirs.push(folderEntry); - } else if (entry.isFile()) { - const fileEntry: FileEntry = { - itemId: "", - name: entry.name, - size: stats.size, - mimeType: mime.lookup(entryPath) || "application/octet-stream", - dateCreated: stats.birthtime, - dateModified: stats.mtime, - }; - directoryReadReturn.files.push(fileEntry); - } + + return await calculate(resolvedPath); } - return directoryReadReturn; - } - - public async getDirectoryTree( - dirPath?: string - ): Promise { - const resolvedPath = this.resolvePath(dirPath); - - const getTree = async ( - currentPath: string - ): Promise => { - const dirEntries = await fs.readdir(currentPath, { withFileTypes: true }); - const shortcuts = (await this.bulk(".arclnk", currentPath)) || {}; - - const dirs: (FolderEntry & { children: RecursiveDirectoryReadReturn })[] = - []; - const files: FileEntry[] = []; - - for (const entry of dirEntries) { - const entryPath = path.join(currentPath, entry.name); - const stats = statSync(entryPath); - - if (entry.isDirectory()) { - const subTree = await getTree(entryPath); - const folderEntry: FolderEntry & { - children: RecursiveDirectoryReadReturn; - } = { - itemId: "", - name: entry.name, - dateCreated: stats.birthtime, - dateModified: stats.mtime, - children: subTree, - }; - dirs.push(folderEntry); - } else if (entry.isFile()) { - const fileEntry: FileEntry = { - itemId: "", - name: entry.name, - size: stats.size, - mimeType: mime.lookup(entryPath) || "application/octet-stream", - dateCreated: stats.birthtime, - dateModified: stats.mtime, - }; - files.push(fileEntry); - } - } + public async writeFile(filePath: string, content: string | Buffer) { + const resolvedPath = this.resolvePath(filePath); - return { dirs, files, shortcuts }; - }; + await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); + await fs.writeFile(resolvedPath, content); + } - return getTree(resolvedPath); - } + public async readFile(filePath: string): Promise { + const resolvedPath = this.resolvePath(filePath); - public async moveItem(sourcePath: string, destPath: string): Promise { - const resolvedSource = this.resolvePath(sourcePath); - const resolvedDest = this.resolvePath(destPath); + return fs.readFile(resolvedPath); + } - await fs.rename(resolvedSource, resolvedDest); - } + public async pipeFile(filePath: string) { + const resolvedPath = this.resolvePath(filePath); - public async copyItem(sourcePath: string, destPath: string): Promise { - const resolvedSource = this.resolvePath(sourcePath); - const resolvedDest = this.resolvePath(destPath); + return createReadStream(resolvedPath); + } - await fs.cp(resolvedSource, resolvedDest, { recursive: true }); - } + public async createDirectory(dirPath: string): Promise { + const resolvedPath = await this.resolvePath(dirPath); - public async deleteItem(itemPath: string): Promise { - const resolvedPath = this.resolvePath(itemPath); + await fs.mkdir(resolvedPath, { recursive: true }); + } - await fs.rm(resolvedPath, { recursive: true }); - } + public async readDirectory( + dirPath?: string, + populateShortcuts = true, + ): Promise { + const resolvedPath = this.resolvePath(dirPath); + const dirEntries = await fs.readdir(resolvedPath, { + withFileTypes: true, + }); + const size = await this.calculateFolderSize(dirPath); + const fileCount = await this.countFiles(dirPath); + const folderCount = await this.countFolders(dirPath); + const shortcuts = populateShortcuts + ? await this.bulk(".arclnk", dirPath) + : {}; + const directoryReadReturn: DirectoryReadReturn = { + dirs: [], + files: [], + totalSize: size, + totalFiles: fileCount, + totalFolders: folderCount, + shortcuts, + }; - public async exists(itemPath: string): Promise { - try { - const resolvedPath = await this.resolvePath(itemPath); + for (const entry of dirEntries) { + const entryPath = path.join(resolvedPath, entry.name); + const stats = statSync(entryPath); + + if (entry.isDirectory()) { + const folderEntry: FolderEntry = { + itemId: "", + name: entry.name, + dateCreated: stats.birthtime, + dateModified: stats.mtime, + }; + directoryReadReturn.dirs.push(folderEntry); + } else if (entry.isFile()) { + const fileEntry: FileEntry = { + itemId: "", + name: entry.name, + size: stats.size, + mimeType: + mime.lookup(entryPath) || "application/octet-stream", + dateCreated: stats.birthtime, + dateModified: stats.mtime, + }; + directoryReadReturn.files.push(fileEntry); + } + } - return existsSync(resolvedPath); - } catch { - return false; + return directoryReadReturn; } - } - - public async quota(): Promise { - const path = platform() === "win32" ? "c:" : "/"; - const usage = await checkDiskSpace(path); - - return { - used: usage.size - usage.free, - free: usage.free, - max: usage.size, - percentage: (100 / usage.size) * (usage.size - usage.free), - }; - } - - public async stat(filePath: string) { - const resolvedPath = this.resolvePath(filePath); - try { - return statSync(resolvedPath); - } catch { - return undefined; + + public async getDirectoryTree( + dirPath?: string, + ): Promise { + const resolvedPath = this.resolvePath(dirPath); + + const getTree = async ( + currentPath: string, + ): Promise => { + const dirEntries = await fs.readdir(currentPath, { + withFileTypes: true, + }); + const shortcuts = (await this.bulk(".arclnk", currentPath)) || {}; + + const dirs: (FolderEntry & { + children: RecursiveDirectoryReadReturn; + })[] = []; + const files: FileEntry[] = []; + + for (const entry of dirEntries) { + const entryPath = path.join(currentPath, entry.name); + const stats = statSync(entryPath); + + if (entry.isDirectory()) { + const subTree = await getTree(entryPath); + const folderEntry: FolderEntry & { + children: RecursiveDirectoryReadReturn; + } = { + itemId: "", + name: entry.name, + dateCreated: stats.birthtime, + dateModified: stats.mtime, + children: subTree, + }; + dirs.push(folderEntry); + } else if (entry.isFile()) { + const fileEntry: FileEntry = { + itemId: "", + name: entry.name, + size: stats.size, + mimeType: + mime.lookup(entryPath) || + "application/octet-stream", + dateCreated: stats.birthtime, + dateModified: stats.mtime, + }; + files.push(fileEntry); + } + } + + return { dirs, files, shortcuts }; + }; + + return getTree(resolvedPath); } - } - public async createReadStream( - filePath: string, - start?: number, - end?: number - ) { - const resolvedPath = this.resolvePath(filePath); + public async moveItem(sourcePath: string, destPath: string): Promise { + const resolvedSource = this.resolvePath(sourcePath); + const resolvedDest = this.resolvePath(destPath); - return createReadStream(resolvedPath, { start, end }); - } + await fs.rename(resolvedSource, resolvedDest); + } - async bulk(extension: string, path: string = "") { - const directory = await this.readDirectory(path, false); - const result: Record = {}; + public async copyItem(sourcePath: string, destPath: string): Promise { + const resolvedSource = this.resolvePath(sourcePath); + const resolvedDest = this.resolvePath(destPath); - for (const file of directory.files) { - if (!file.name.endsWith(extension)) continue; + await fs.cp(resolvedSource, resolvedDest, { recursive: true }); + } + + public async deleteItem(itemPath: string): Promise { + const resolvedPath = this.resolvePath(itemPath); + + await fs.rm(resolvedPath, { recursive: true }); + } - const contents = await this.readFile(join(path, file.name)); + public async exists(itemPath: string): Promise { + try { + const resolvedPath = await this.resolvePath(itemPath); - result[file.name] = tryJsonParse(contents.toString()) || contents; + return existsSync(resolvedPath); + } catch { + return false; + } + } - if (JSON.stringify(result).length > 1e5) return result; + public async quota(): Promise { + const path = platform() === "win32" ? "c:" : "/"; + const usage = await checkDiskSpace(path); + + return { + used: usage.size - usage.free, + free: usage.free, + max: usage.size, + percentage: (100 / usage.size) * (usage.size - usage.free), + }; + } + + public async stat(filePath: string) { + const resolvedPath = this.resolvePath(filePath); + try { + return statSync(resolvedPath); + } catch { + return undefined; + } } - return result; - } + public async createReadStream( + filePath: string, + start?: number, + end?: number, + ) { + const resolvedPath = this.resolvePath(filePath); + + return createReadStream(resolvedPath, { start, end }); + } + + async bulk(extension: string, path: string = "") { + const directory = await this.readDirectory(path, false); + const result: Record = {}; + + for (const file of directory.files) { + if (!file.name.endsWith(extension)) continue; + + const contents = await this.readFile(join(path, file.name)); + + result[file.name] = tryJsonParse(contents.toString()) || contents; + + if (JSON.stringify(result).length > 1e5) return result; + } + + return result; + } } diff --git a/src/tools/build-ts-tpa.ts b/src/tools/build-ts-tpa.ts new file mode 100644 index 0000000..9ddaf38 --- /dev/null +++ b/src/tools/build-ts-tpa.ts @@ -0,0 +1,260 @@ +import fs from "fs"; +import path, { resolve } from "path"; +import process from "process"; +import { spawn, type SpawnOptionsWithoutStdio } from "child_process"; +import { once } from "events"; +import { program } from "commander"; + +const resourceFileRegex = /(?!\w+\.[em]?ts$|\w+\.[em]?js$)(^\w+\.?\w*)/gm; +const scriptFileRegex = /(\w+\.[me]?ts$|\w+\.[me]?js$)/gm; +export const jsFileRegex = /(\w+\.[me]?js$)/gm; +export const tsFileRegex = /(\w+\.[me]?ts$)/gm; + +// this should NOT be manually updated +let printDebug = false; + +function debugPrint(message?: any, ...optionalParams: any[]) { + if (printDebug) console.debug(message, ...optionalParams); +} + +function conditionalFSRemove(source: fs.PathLike, cwd: fs.PathLike) { + if (fs.existsSync(source)) { + const fileStat = fs.statSync(source); + + console.log( + `Removing './${ + cwd + ? path.relative(cwd.toString(), source.toString()) + : path.basename(source.toString()) + }'..`, + ); + fs.rmSync(source, fileStat.isDirectory() ? { recursive: true } : {}); + } +} + +function FSCopy( + source: fs.PathLike, + destination: fs.PathLike, + cwd: fs.PathLike, +) { + if (fs.existsSync(source)) { + const sourceFileStat = fs.statSync(source); + + // it's ugly i know 😭 + console.log( + "Copying", + `'./${ + cwd + ? path.relative(cwd.toString(), source.toString()) + : path.basename(source.toString()) + }'`, + "->", + `'./${ + cwd + ? path.relative(cwd.toString(), destination.toString()) + : path.basename(destination.toString()) + }'..`, + ); + fs.cpSync( + source.toString(), + destination.toString(), + sourceFileStat.isDirectory() ? { recursive: true } : {}, + ); + } +} + +async function runCommand( + command: string, + args: string[] | undefined, + onError: (err: Error) => void, + commandOpts?: SpawnOptionsWithoutStdio, + afterRun?: () => void, +) { + const cmd = spawn(command, args, commandOpts); + + cmd.stdout.on("data", (data) => { + process.stdout.write(data); + }); + + cmd.stderr.on("data", (data) => { + process.stderr.write(data); + }); + + cmd.on("error", onError); + + afterRun?.(); + + const [code] = await once(cmd, "close"); + + return code; +} + +function removeDeletedFiles( + srcRoot: fs.PathLike, + distRoot: fs.PathLike, + cwd: fs.PathLike, +) { + const srcFiles = fs + .readdirSync(srcRoot, { + recursive: true, + }) + .map((val) => { + if (tsFileRegex.test(val.toString())) { + debugPrint( + "converted filename:", + path + .basename(val.toString(), path.extname(val.toString())) + .concat(".js"), + ); + return path + .basename(val.toString(), path.extname(val.toString())) + .concat(".js"); + } else return val; + }); + + const distFiles = fs.readdirSync(distRoot, { + recursive: true, + }); + + debugPrint("srcFiles:", srcFiles); + debugPrint("distFiles:", distFiles); + + distFiles.forEach((val) => { + if (!srcFiles.includes(val)) { + console.log("removeDeletedFiles"); + conditionalFSRemove( + path.resolve(distRoot.toString(), val.toString()), + cwd, + ); + } + }); +} + +function copyNewFiles(srcRoot: string, distRoot: string, cwd: fs.PathLike) { + const srcFiles = fs + .readdirSync(srcRoot, { + recursive: true, + }) + .filter((val) => { + return val.toString().match(resourceFileRegex)?.[0]; + }); + + const distFiles = fs + .readdirSync(distRoot, { + recursive: true, + }) + .filter((val) => { + return val.toString().match(resourceFileRegex)?.[0]; + }); + + debugPrint("srcFiles:", srcFiles); + debugPrint("distFiles:", distFiles); + + srcFiles.forEach((val) => { + if (!distFiles.includes(val)) { + console.log("copyNewFiles"); + FSCopy( + path.resolve(srcRoot.toString(), val.toString()), + path.resolve(distRoot.toString(), val.toString()), + cwd, + ); + } + }); +} + +function fixExports(distRoot: string) { + const jsFiles = fs.readdirSync(distRoot, { + recursive: true, + }); + const sortedJsFiles = jsFiles.filter((val) => { + return val.toString().match(scriptFileRegex)?.[0]; + }); + + sortedJsFiles.forEach((val) => { + const jsPath = resolve(distRoot, val.toString()); + + debugPrint("jsPath:", jsPath); + + try { + const data = fs.readFileSync(jsPath); + + // this shit is annoying to deal with + const exportRegex = + /export\b\s*(?:(?:default\s*)?{\s*((?:[^,{}]+,?)+\b)\s*}|(?:default\s*)?([^,;\s]+));?/gm; + + let contents = data.toString(); + + while (exportRegex.test(contents)) { + contents = contents.replace( + exportRegex, + (subStr: string, ...args: any[]) => { + debugPrint( + `export replace subStr: '${subStr}'\nargs:`, + args, + "\n", + ); + + const hasCurlyBrackets = subStr.includes("{"); + + return `return ${hasCurlyBrackets ? "{ " : ""}${args[0] ?? args[1]}${hasCurlyBrackets ? " }" : ""};`; + }, + ); + + fs.writeFileSync(jsPath, contents); + } + } catch (err) { + throw err; + } + }); +} + +async function compileAndCopySrc( + distRoot: string, + tmpRoot: string, + tmpSrc: string, + cwd: fs.PathLike, +) { + const pathToTsc = require.resolve("typescript/bin/tsc"); + + await runCommand( + "node", + [pathToTsc, "--rootDir", ".", "--outDir", "./tmp"], + (err) => { + throw err; + }, + ); + + if (fs.existsSync(tmpSrc)) { + const tmpSrcFiles = fs.readdirSync(tmpSrc); + tmpSrcFiles.forEach(async (val) => { + const tmpSrcPath = resolve(tmpSrc, val); + const distPath = resolve(distRoot, val); + FSCopy(tmpSrcPath, distPath, cwd); + conditionalFSRemove(tmpSrcPath, cwd); + }); + conditionalFSRemove(tmpRoot, cwd); + + fixExports(distRoot); + } +} + +export async function buildTSTPA( + cwd: fs.PathLike, + debugOutput: boolean = false, +) { + printDebug = debugOutput; + + const srcRoot = resolve(cwd.toString(), "src"); + const distRoot = resolve(cwd.toString(), "dist"); + const tmpRoot = resolve(cwd.toString(), "tmp"); + const tmpSrc = resolve(tmpRoot, "src"); + + // clear out files for a fresh output + removeDeletedFiles(srcRoot, distRoot, cwd); + + copyNewFiles(srcRoot, distRoot, cwd); + + await compileAndCopySrc(distRoot, tmpRoot, tmpSrc, cwd); +} + +export default buildTSTPA; diff --git a/tsconfig.json b/tsconfig.json index 893e405..dac4d9d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,13 @@ { - "compilerOptions": { - "target": "ES2020", - "module": "CommonJS", - "lib": ["ES2020"], - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "resolveJsonModule": true - } + "include": ["src/**/*"], + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true + } } From c8097aed2e3a0e22efa1a61a4f4b1ccf67ffb067 Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:25:17 -0500 Subject: [PATCH 02/14] fix file updating not sending to dist --- src/tools/build-ts-tpa.ts | 123 ++++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 37 deletions(-) diff --git a/src/tools/build-ts-tpa.ts b/src/tools/build-ts-tpa.ts index 9ddaf38..776674e 100644 --- a/src/tools/build-ts-tpa.ts +++ b/src/tools/build-ts-tpa.ts @@ -5,6 +5,10 @@ import { spawn, type SpawnOptionsWithoutStdio } from "child_process"; import { once } from "events"; import { program } from "commander"; +// this shit is annoying to deal with +const exportRegex = + /export\b\s*(?:(?:default\s*)?{\s*((?:[^,{}]+,?)+\b)\s*}|(?:default\s*)?([^,;\s]+));?/gm; + const resourceFileRegex = /(?!\w+\.[em]?ts$|\w+\.[em]?js$)(^\w+\.?\w*)/gm; const scriptFileRegex = /(\w+\.[me]?ts$|\w+\.[me]?js$)/gm; export const jsFileRegex = /(\w+\.[me]?js$)/gm; @@ -17,7 +21,7 @@ function debugPrint(message?: any, ...optionalParams: any[]) { if (printDebug) console.debug(message, ...optionalParams); } -function conditionalFSRemove(source: fs.PathLike, cwd: fs.PathLike) { +function conditionalFSRemove(source: fs.PathLike, cwd?: fs.PathLike) { if (fs.existsSync(source)) { const fileStat = fs.statSync(source); @@ -35,7 +39,7 @@ function conditionalFSRemove(source: fs.PathLike, cwd: fs.PathLike) { function FSCopy( source: fs.PathLike, destination: fs.PathLike, - cwd: fs.PathLike, + cwd?: fs.PathLike, ) { if (fs.existsSync(source)) { const sourceFileStat = fs.statSync(source); @@ -63,6 +67,23 @@ function FSCopy( } } +function FSWriteFile( + destination: fs.PathLike, + data: string | NodeJS.ArrayBufferView, + cwd?: fs.PathLike, +) { + console.log( + "Writing data to ", + `'./${ + cwd + ? path.relative(cwd.toString(), destination.toString()) + : path.basename(destination.toString()) + }'..`, + ); + + fs.writeFileSync(destination, data); +} + async function runCommand( command: string, args: string[] | undefined, @@ -151,17 +172,33 @@ function copyNewFiles(srcRoot: string, distRoot: string, cwd: fs.PathLike) { debugPrint("distFiles:", distFiles); srcFiles.forEach((val) => { + const srcFilePath = path.resolve(srcRoot.toString(), val.toString()); + const distFilePath = path.resolve(distRoot.toString(), val.toString()); + if (!distFiles.includes(val)) { console.log("copyNewFiles"); - FSCopy( - path.resolve(srcRoot.toString(), val.toString()), - path.resolve(distRoot.toString(), val.toString()), - cwd, - ); + FSCopy(srcFilePath, distFilePath, cwd); + } else { + const srcFileContents = fs.readFileSync(srcFilePath); + const distFileContents = fs.readFileSync(distFilePath); + + if (srcFileContents.compare(distFileContents) !== 0) { + FSWriteFile(distFilePath, srcFileContents, cwd); + } } }); } +function replaceExport(contents: string) { + return contents.replace(exportRegex, (subStr: string, ...args: any[]) => { + debugPrint(`export replace subStr: '${subStr}'\nargs:`, args, "\n"); + + const hasCurlyBrackets = subStr.includes("{"); + + return `return ${hasCurlyBrackets ? "{ " : ""}${args[0] ?? args[1]}${hasCurlyBrackets ? " }" : ""};`; + }); +} + function fixExports(distRoot: string) { const jsFiles = fs.readdirSync(distRoot, { recursive: true, @@ -178,27 +215,10 @@ function fixExports(distRoot: string) { try { const data = fs.readFileSync(jsPath); - // this shit is annoying to deal with - const exportRegex = - /export\b\s*(?:(?:default\s*)?{\s*((?:[^,{}]+,?)+\b)\s*}|(?:default\s*)?([^,;\s]+));?/gm; - let contents = data.toString(); while (exportRegex.test(contents)) { - contents = contents.replace( - exportRegex, - (subStr: string, ...args: any[]) => { - debugPrint( - `export replace subStr: '${subStr}'\nargs:`, - args, - "\n", - ); - - const hasCurlyBrackets = subStr.includes("{"); - - return `return ${hasCurlyBrackets ? "{ " : ""}${args[0] ?? args[1]}${hasCurlyBrackets ? " }" : ""};`; - }, - ); + contents = replaceExport(contents); fs.writeFileSync(jsPath, contents); } @@ -218,24 +238,54 @@ async function compileAndCopySrc( await runCommand( "node", - [pathToTsc, "--rootDir", ".", "--outDir", "./tmp"], + [ + pathToTsc, + "--rootDir", + cwd.toString(), + "--outDir", + resolve(cwd.toString(), "tmp"), + ], (err) => { throw err; }, ); - if (fs.existsSync(tmpSrc)) { - const tmpSrcFiles = fs.readdirSync(tmpSrc); - tmpSrcFiles.forEach(async (val) => { - const tmpSrcPath = resolve(tmpSrc, val); - const distPath = resolve(distRoot, val); - FSCopy(tmpSrcPath, distPath, cwd); - conditionalFSRemove(tmpSrcPath, cwd); + const distFiles = fs + .readdirSync(distRoot, { + recursive: true, + }) + .filter((val) => { + return val.toString().match(scriptFileRegex)?.[0]; }); - conditionalFSRemove(tmpRoot, cwd); - fixExports(distRoot); - } + const tmpSrcFiles = fs.readdirSync(tmpSrc, { + recursive: true, + }); + + debugPrint("distFiles:", distFiles); + debugPrint("tmpSrcFiles:", tmpSrcFiles); + + tmpSrcFiles.forEach(async (val) => { + const tmpSrcFilePath = path.resolve(tmpSrc, val.toString()); + const distFilePath = path.resolve(distRoot, val.toString()); + + const tmpSrcFileContents = fs.readFileSync(tmpSrcFilePath); + const distFileContents = fs.readFileSync(distFilePath); + + let tmpSrcContents = tmpSrcFileContents.toString(); + while (exportRegex.test(tmpSrcContents)) { + tmpSrcContents = replaceExport(tmpSrcContents); + } + + if (tmpSrcContents !== distFileContents.toString()) { + FSCopy(tmpSrcFilePath, distFilePath, cwd); + conditionalFSRemove(tmpSrcFilePath, cwd); + } + }); + + conditionalFSRemove(tmpRoot, cwd); + + fixExports(distRoot); } export async function buildTSTPA( @@ -249,7 +299,6 @@ export async function buildTSTPA( const tmpRoot = resolve(cwd.toString(), "tmp"); const tmpSrc = resolve(tmpRoot, "src"); - // clear out files for a fresh output removeDeletedFiles(srcRoot, distRoot, cwd); copyNewFiles(srcRoot, distRoot, cwd); From 1c892204de872693e7d8e09d8815a2bba7068f5f Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:01:05 -0500 Subject: [PATCH 03/14] add TS TPA support on build --- src/commands/build/index.ts | 112 +++++++++++++++++--------------- src/tools/build-ts-tpa.ts | 124 +++++++++++++++++++++--------------- 2 files changed, 133 insertions(+), 103 deletions(-) diff --git a/src/commands/build/index.ts b/src/commands/build/index.ts index 80bfb47..d795d37 100644 --- a/src/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -1,70 +1,82 @@ import { intro, spinner } from "@clack/prompts"; -import { cp, mkdir, rm, writeFile } from "fs/promises"; +import { cp, mkdir, rm, writeFile, readdir } from "fs/promises"; import { join } from "path"; import { cwd } from "process"; import { zip } from "zip-a-folder"; import { Project } from "../../project"; import signale from "signale"; +import buildTSTPA, { tsFileRegex } from "../../tools/build-ts-tpa"; export default async function BuildCommand() { - try { - const project = new Project(cwd()); - await project.readProjectFile(); + try { + const project = new Project(cwd()); + await project.readProjectFile(); - if (!project.metadata) return; + if (!project.metadata) return; - if (await project.areTypeDefsOutdated()) { - signale.warn( - "Type definitions are outdated. Please run `npx v7cli update` to update them." - ); - } + if (await project.areTypeDefsOutdated()) { + signale.warn( + "Type definitions are outdated. Please run `npx v7cli update` to update them.", + ); + } - const appId = project.metadata?.metadata.appId; + const appId = project.metadata?.metadata.appId; - if (appId?.includes(".") || appId?.includes("-")) { - signale.error( - "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa" - ); - process.exit(1); - } + if (appId?.includes(".") || appId?.includes("-")) { + signale.error( + "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa", + ); + process.exit(1); + } + + intro("Build ArcOS Package"); + const spin = spinner(); + spin.start("Building..."); - intro("Build ArcOS Package"); - const spin = spinner(); - spin.start("Building..."); + spin.message("Creating temp directory"); + await mkdir(join(project.path, ".arcdev-build"), { recursive: true }); - spin.message("Creating temp directory"); - await mkdir(join(project.path, ".arcdev-build"), { recursive: true }); + // DO THE BUILD HERE + const containsTypescript = await readdir( + project.metadata.payloadDir, + ).then((data) => { + return data.some((val) => tsFileRegex.test(val)); + }); + const payloadDir = containsTypescript + ? join(project.path, "dist") + : join(project.path, project.metadata.payloadDir); - spin.message("Copying payload"); - await cp( - join(project.path, project.metadata.payloadDir), - ".arcdev-build/payload", - { - recursive: true, - } - ); + if (containsTypescript) { + spin.message("Compiling project"); + await buildTSTPA(project.path, true); + } - spin.message("Writing _metadata.json"); - await writeFile( - join(project.path, ".arcdev-build", "_metadata.json"), - JSON.stringify(project.metadata.metadata, null, 2), - "utf-8" - ); + spin.message("Copying payload"); + await cp(payloadDir, ".arcdev-build/payload", { + recursive: true, + }); - spin.message("Bundling package"); - await zip( - join(project.path, ".arcdev-build"), - join(project.path, project.metadata.outFile) - ); + spin.message("Writing _metadata.json"); + await writeFile( + join(project.path, ".arcdev-build", "_metadata.json"), + JSON.stringify(project.metadata.metadata, null, 2), + "utf-8", + ); - spin.message("Removing temp directory"); - await rm(join(project.path, ".arcdev-build"), { - recursive: true, - force: true, - }); + spin.message("Bundling package"); + await zip( + join(project.path, ".arcdev-build"), + join(project.path, project.metadata.outFile), + ); - spin.stop("Done!"); - } catch (e) { - console.log(e); - } + spin.message("Removing temp directory"); + await rm(join(project.path, ".arcdev-build"), { + recursive: true, + force: true, + }); + + spin.stop("Done!"); + } catch (e) { + console.log(e); + } } diff --git a/src/tools/build-ts-tpa.ts b/src/tools/build-ts-tpa.ts index 776674e..3e09f60 100644 --- a/src/tools/build-ts-tpa.ts +++ b/src/tools/build-ts-tpa.ts @@ -15,23 +15,27 @@ export const jsFileRegex = /(\w+\.[me]?js$)/gm; export const tsFileRegex = /(\w+\.[me]?ts$)/gm; // this should NOT be manually updated -let printDebug = false; +let silentMode = false; +let printDebugMsgs = false; -function debugPrint(message?: any, ...optionalParams: any[]) { - if (printDebug) console.debug(message, ...optionalParams); +function printDebug(message?: any, ...optionalParams: any[]) { + if (printDebugMsgs && !silentMode) + console.debug(message, ...optionalParams); } function conditionalFSRemove(source: fs.PathLike, cwd?: fs.PathLike) { if (fs.existsSync(source)) { const fileStat = fs.statSync(source); - console.log( - `Removing './${ - cwd - ? path.relative(cwd.toString(), source.toString()) - : path.basename(source.toString()) - }'..`, - ); + if (!silentMode) { + console.log( + `Removing './${ + cwd + ? path.relative(cwd.toString(), source.toString()) + : path.basename(source.toString()) + }'..`, + ); + } fs.rmSync(source, fileStat.isDirectory() ? { recursive: true } : {}); } } @@ -45,20 +49,22 @@ function FSCopy( const sourceFileStat = fs.statSync(source); // it's ugly i know 😭 - console.log( - "Copying", - `'./${ - cwd - ? path.relative(cwd.toString(), source.toString()) - : path.basename(source.toString()) - }'`, - "->", - `'./${ - cwd - ? path.relative(cwd.toString(), destination.toString()) - : path.basename(destination.toString()) - }'..`, - ); + if (!silentMode) { + console.log( + "Copying", + `'./${ + cwd + ? path.relative(cwd.toString(), source.toString()) + : path.basename(source.toString()) + }'`, + "->", + `'./${ + cwd + ? path.relative(cwd.toString(), destination.toString()) + : path.basename(destination.toString()) + }'..`, + ); + } fs.cpSync( source.toString(), destination.toString(), @@ -72,14 +78,16 @@ function FSWriteFile( data: string | NodeJS.ArrayBufferView, cwd?: fs.PathLike, ) { - console.log( - "Writing data to ", - `'./${ - cwd - ? path.relative(cwd.toString(), destination.toString()) - : path.basename(destination.toString()) - }'..`, - ); + if (!silentMode) { + console.log( + "Writing data to ", + `'./${ + cwd + ? path.relative(cwd.toString(), destination.toString()) + : path.basename(destination.toString()) + }'..`, + ); + } fs.writeFileSync(destination, data); } @@ -121,7 +129,7 @@ function removeDeletedFiles( }) .map((val) => { if (tsFileRegex.test(val.toString())) { - debugPrint( + printDebug( "converted filename:", path .basename(val.toString(), path.extname(val.toString())) @@ -137,12 +145,11 @@ function removeDeletedFiles( recursive: true, }); - debugPrint("srcFiles:", srcFiles); - debugPrint("distFiles:", distFiles); + printDebug("srcFiles:", srcFiles); + printDebug("distFiles:", distFiles); distFiles.forEach((val) => { if (!srcFiles.includes(val)) { - console.log("removeDeletedFiles"); conditionalFSRemove( path.resolve(distRoot.toString(), val.toString()), cwd, @@ -168,15 +175,14 @@ function copyNewFiles(srcRoot: string, distRoot: string, cwd: fs.PathLike) { return val.toString().match(resourceFileRegex)?.[0]; }); - debugPrint("srcFiles:", srcFiles); - debugPrint("distFiles:", distFiles); + printDebug("srcFiles:", srcFiles); + printDebug("distFiles:", distFiles); srcFiles.forEach((val) => { const srcFilePath = path.resolve(srcRoot.toString(), val.toString()); const distFilePath = path.resolve(distRoot.toString(), val.toString()); if (!distFiles.includes(val)) { - console.log("copyNewFiles"); FSCopy(srcFilePath, distFilePath, cwd); } else { const srcFileContents = fs.readFileSync(srcFilePath); @@ -191,7 +197,7 @@ function copyNewFiles(srcRoot: string, distRoot: string, cwd: fs.PathLike) { function replaceExport(contents: string) { return contents.replace(exportRegex, (subStr: string, ...args: any[]) => { - debugPrint(`export replace subStr: '${subStr}'\nargs:`, args, "\n"); + printDebug(`export replace subStr: '${subStr}'\nargs:`, args, "\n"); const hasCurlyBrackets = subStr.includes("{"); @@ -210,7 +216,7 @@ function fixExports(distRoot: string) { sortedJsFiles.forEach((val) => { const jsPath = resolve(distRoot, val.toString()); - debugPrint("jsPath:", jsPath); + printDebug("jsPath:", jsPath); try { const data = fs.readFileSync(jsPath); @@ -262,24 +268,29 @@ async function compileAndCopySrc( recursive: true, }); - debugPrint("distFiles:", distFiles); - debugPrint("tmpSrcFiles:", tmpSrcFiles); + printDebug("distFiles:", distFiles); + printDebug("tmpSrcFiles:", tmpSrcFiles); tmpSrcFiles.forEach(async (val) => { const tmpSrcFilePath = path.resolve(tmpSrc, val.toString()); const distFilePath = path.resolve(distRoot, val.toString()); - const tmpSrcFileContents = fs.readFileSync(tmpSrcFilePath); - const distFileContents = fs.readFileSync(distFilePath); - - let tmpSrcContents = tmpSrcFileContents.toString(); - while (exportRegex.test(tmpSrcContents)) { - tmpSrcContents = replaceExport(tmpSrcContents); - } - - if (tmpSrcContents !== distFileContents.toString()) { + if (!fs.existsSync(distFilePath)) { FSCopy(tmpSrcFilePath, distFilePath, cwd); conditionalFSRemove(tmpSrcFilePath, cwd); + } else { + const tmpSrcFileContents = fs.readFileSync(tmpSrcFilePath); + const distFileContents = fs.readFileSync(distFilePath); + + let tmpSrcContents = tmpSrcFileContents.toString(); + while (exportRegex.test(tmpSrcContents)) { + tmpSrcContents = replaceExport(tmpSrcContents); + } + + if (tmpSrcContents !== distFileContents.toString()) { + FSCopy(tmpSrcFilePath, distFilePath, cwd); + conditionalFSRemove(tmpSrcFilePath, cwd); + } } }); @@ -290,15 +301,22 @@ async function compileAndCopySrc( export async function buildTSTPA( cwd: fs.PathLike, + silent: boolean = false, debugOutput: boolean = false, ) { - printDebug = debugOutput; + silentMode = silent; + printDebugMsgs = debugOutput; const srcRoot = resolve(cwd.toString(), "src"); const distRoot = resolve(cwd.toString(), "dist"); const tmpRoot = resolve(cwd.toString(), "tmp"); const tmpSrc = resolve(tmpRoot, "src"); + if (!fs.existsSync(distRoot)) { + fs.mkdirSync(distRoot, { + recursive: true, + }); + } removeDeletedFiles(srcRoot, distRoot, cwd); copyNewFiles(srcRoot, distRoot, cwd); From 551473a9a24e76f0274db8696d4f0e6c2ea9ff47 Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:03:44 -0500 Subject: [PATCH 04/14] add ts option on new project (no impl) --- src/commands/new/index.ts | 215 +++++++++++++++++++++----------------- src/server/fs/index.ts | 5 + 2 files changed, 125 insertions(+), 95 deletions(-) diff --git a/src/commands/new/index.ts b/src/commands/new/index.ts index 38939e9..c4e92fa 100644 --- a/src/commands/new/index.ts +++ b/src/commands/new/index.ts @@ -1,4 +1,12 @@ -import { cancel, intro, isCancel, outro, spinner, text } from "@clack/prompts"; +import { + cancel, + intro, + isCancel, + outro, + select, + spinner, + text, +} from "@clack/prompts"; import { Command } from "commander"; import { userInfo } from "os"; import { join } from "path"; @@ -9,101 +17,118 @@ import { TpaWizard } from "../../tpa/wizard"; import { PackageMetadata } from "../../types/package"; export default async function NewCommand(this: Command, destination: string) { - intro(`Create ArcOS Project - ${destination}`); - - const name = await text({ - message: "What do you want to name your app?", - initialValue: "", - validate(value) { - if (!value) return "A name is required"; - }, - }); - - if (isCancel(name)) abort(); - - const description = await text({ - message: "Please give a description for your app", - initialValue: "", - - validate(value) { - if (!value) return `A description is required`; - if (value.length > 512) - return `Too long! Pick a description under 512 characters.`; - }, - }); - - if (isCancel(description)) abort(); - - const author = await text({ - message: "Who's the author?", - initialValue: userInfo().username, - validate(value) { - if (!value) return `An author is required`; - if (value.length > 32) - return `Too long! Pick a name under 32 characters.`; - }, - }); - - if (isCancel(author)) abort(); - - const version = await text({ - message: "What version is your app?", - initialValue: "1.0.0", - validate(value) { - if (value.length !== 5 || value[1] !== "." || value[3] !== ".") - return "Need a version in an x.x.x format"; - }, - }); - - if (isCancel(version)) abort(); - - const appId = await text({ - message: "What ID do you want your app to have?", - initialValue: "", - validate(value) { - if (!value.includes("_") || value.includes(".") || value.includes("-")) - return "The ID has to be the format 'author_appId', and it may not include any periods or dashes."; - }, - }); - - if (isCancel(appId)) abort(); - - const installLocation = await text({ - message: "Where will this app install?", - initialValue: `U:/Applications/${appId.toString()}`, - validate(value) { - if (!value.startsWith(`U:/Applications/`) || value === `U:/Applications/`) - return "This has to be an ArcOS path that starts with 'U:/Applications/'"; - }, - }); - - if (isCancel(installLocation)) abort(); - - const metadata: PackageMetadata = { - name: name.toString(), - description: description.toString(), - author: author.toString(), - version: version.toString(), - installLocation: installLocation.toString(), - appId: appId.toString(), - }; - - const spin = spinner(); - spin.start("Initializing project"); - - const project = new Project(join(cwd(), destination)); - - await project.initialize(metadata, `${metadata.appId}.arc`, "src"); - - spin.stop("Done."); - outro(); - - const app = await TpaWizard(metadata); - - scaffoldProject(app, project); + intro(`Create ArcOS Project - ${destination}`); + + const name = await text({ + message: "What do you want to name your app?", + initialValue: "", + validate(value) { + if (!value) return "A name is required"; + }, + }); + + if (isCancel(name)) abort(); + + const description = await text({ + message: "Please give a description for your app", + initialValue: "", + + validate(value) { + if (!value) return `A description is required`; + if (value.length > 512) + return `Too long! Pick a description under 512 characters.`; + }, + }); + + if (isCancel(description)) abort(); + + const author = await text({ + message: "Who's the author?", + initialValue: userInfo().username, + validate(value) { + if (!value) return `An author is required`; + if (value.length > 32) + return `Too long! Pick a name under 32 characters.`; + }, + }); + + if (isCancel(author)) abort(); + + const version = await text({ + message: "What version is your app?", + initialValue: "1.0.0", + validate(value) { + if (value.length !== 5 || value[1] !== "." || value[3] !== ".") + return "Need a version in an x.x.x format"; + }, + }); + + if (isCancel(version)) abort(); + + const appId = await text({ + message: "What ID do you want your app to have?", + initialValue: "", + validate(value) { + if ( + !value.includes("_") || + value.includes(".") || + value.includes("-") + ) + return "The ID has to be the format 'author_appId', and it may not include any periods or dashes."; + }, + }); + + if (isCancel(appId)) abort(); + + const installLocation = await text({ + message: "Where will this app install?", + initialValue: `U:/Applications/${appId.toString()}`, + validate(value) { + if ( + !value.startsWith(`U:/Applications/`) || + value === `U:/Applications/` + ) + return "This has to be an ArcOS path that starts with 'U:/Applications/'"; + }, + }); + + if (isCancel(installLocation)) abort(); + + const isTypeScriptProject = await select({ + message: "Do you want to enable experimental TS support?", + options: [ + { value: "true", label: "Sure!" }, + { value: "false", label: "No thanks." }, + ], + }); + + if (isCancel(isTypeScriptProject)) abort(); + + const metadata: PackageMetadata = { + name: name.toString(), + description: description.toString(), + author: author.toString(), + version: version.toString(), + installLocation: installLocation.toString(), + appId: appId.toString(), + }; + + const spin = spinner(); + spin.start("Initializing project"); + + const project = new Project(join(cwd(), destination)); + + await project.initialize(metadata, `${metadata.appId}.arc`, "src"); + + spin.stop("Done."); + outro(); + + const app = await TpaWizard(metadata); + + scaffoldProject(app, project); } export function abort(): any { - cancel("Aborted."); - return process.exit(0) as any; + cancel("Aborted."); + return process.exit(0) as any; } diff --git a/src/server/fs/index.ts b/src/server/fs/index.ts index 63e1f1c..2dac1c3 100644 --- a/src/server/fs/index.ts +++ b/src/server/fs/index.ts @@ -20,6 +20,11 @@ export class Filesystem { accessors: Record = {}; // R constructor(projectPath: string, payloadDir: string) { + if (!existsSync(payloadDir)) { + this.path = join(projectPath, payloadDir); + return; + } + const containsTypescript = readdirSync(payloadDir).some((val) => tsFileRegex.test(val), ); From b6f8a52b9cf94699e1104f56363143abba59626b Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Sun, 14 Jun 2026 23:59:41 -0500 Subject: [PATCH 05/14] fix MANY bugs --- src/commands/build/index.ts | 13 ++- src/commands/dev/index.ts | 10 +-- src/server/api/index.ts | 12 ++- src/server/fs/index.ts | 8 +- src/tools/build-ts-tpa.ts | 164 ++++++++++++++++++++++++++++++------ 5 files changed, 153 insertions(+), 54 deletions(-) diff --git a/src/commands/build/index.ts b/src/commands/build/index.ts index d795d37..21ea14e 100644 --- a/src/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -5,7 +5,7 @@ import { cwd } from "process"; import { zip } from "zip-a-folder"; import { Project } from "../../project"; import signale from "signale"; -import buildTSTPA, { tsFileRegex } from "../../tools/build-ts-tpa"; +import buildTSTPA, { containsTypescript } from "../../tools/build-ts-tpa"; export default async function BuildCommand() { try { @@ -37,16 +37,13 @@ export default async function BuildCommand() { await mkdir(join(project.path, ".arcdev-build"), { recursive: true }); // DO THE BUILD HERE - const containsTypescript = await readdir( - project.metadata.payloadDir, - ).then((data) => { - return data.some((val) => tsFileRegex.test(val)); - }); - const payloadDir = containsTypescript + const containsTS = containsTypescript(project.metadata.payloadDir); + + const payloadDir = containsTS ? join(project.path, "dist") : join(project.path, project.metadata.payloadDir); - if (containsTypescript) { + if (containsTS) { spin.message("Compiling project"); await buildTSTPA(project.path, true); } diff --git a/src/commands/dev/index.ts b/src/commands/dev/index.ts index 30e710a..702d27e 100644 --- a/src/commands/dev/index.ts +++ b/src/commands/dev/index.ts @@ -6,7 +6,7 @@ import { StartServer } from "../../server/api"; import signale from "signale"; import { getArcBuild } from "../../build"; import { readdirSync } from "fs"; -import buildTSTPA, { tsFileRegex } from "../../tools/build-ts-tpa"; +import buildTSTPA, { containsTypescript } from "../../tools/build-ts-tpa"; export default async function DevCommand() { const project = new Project(cwd()); @@ -38,12 +38,10 @@ export default async function DevCommand() { project.metadata!!.buildHash = buildHash; } - const containsTypescript = readdirSync(project.metadata!.payloadDir).some( - (val) => tsFileRegex.test(val), - ); + const containsTS = containsTypescript(project.metadata!.payloadDir); - if (containsTypescript) { - buildTSTPA(project.path); + if (containsTS) { + await buildTSTPA(project.path, false, true); } await StartServer(project); diff --git a/src/server/api/index.ts b/src/server/api/index.ts index a9ba3ff..62e7594 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -13,7 +13,7 @@ import { readdirSync, watch } from "fs"; import { join } from "path"; import "colors"; import signale, { Signale } from "signale"; -import buildTSTPA, { tsFileRegex } from "../../tools/build-ts-tpa"; +import buildTSTPA, { containsTypescript } from "../../tools/build-ts-tpa"; export const App = express(); export const APILog = new Signale({ @@ -30,9 +30,7 @@ export async function StartServer(project: Project) { ); App.set("trust proxy", true); - const containsTypescript = readdirSync(project.metadata!.payloadDir).some( - (val) => tsFileRegex.test(val), - ); + const containsTS = containsTypescript(project.metadata!.payloadDir); return new Promise((r) => { const server = App.listen(project.metadata?.devPort || 3128, () => { @@ -57,7 +55,7 @@ export async function StartServer(project: Project) { { persistent: true, recursive: true }, async (e, filename) => { if (!watchTimeout && !watchTimeoutTS) { - if (containsTypescript) watchTimeoutTS = true; + if (containsTS) watchTimeoutTS = true; if (filename?.endsWith(".css")) { APILog.warn( `${filename || e}: Change detected, reloading CSS`, @@ -72,8 +70,8 @@ export async function StartServer(project: Project) { project.metadata?.metadata.appId }`, ); - if (containsTypescript) { - await buildTSTPA(project.path); + if (containsTS) { + await buildTSTPA(project.path, false, true); } project.websock?.client?.sock.emit("restart-tpa"); } diff --git a/src/server/fs/index.ts b/src/server/fs/index.ts index 2dac1c3..1f723f6 100644 --- a/src/server/fs/index.ts +++ b/src/server/fs/index.ts @@ -13,7 +13,7 @@ import { } from "../../types/fs"; import { platform } from "os"; import checkDiskSpace from "check-disk-space"; -import { tsFileRegex } from "../../tools/build-ts-tpa"; +import { containsTypescript } from "../../tools/build-ts-tpa"; export class Filesystem { private path: string; @@ -25,11 +25,9 @@ export class Filesystem { return; } - const containsTypescript = readdirSync(payloadDir).some((val) => - tsFileRegex.test(val), - ); + const containsTS = containsTypescript(payloadDir); - if (containsTypescript) { + if (containsTS) { this.path = join(projectPath, "dist"); } else { this.path = join(projectPath, payloadDir); diff --git a/src/tools/build-ts-tpa.ts b/src/tools/build-ts-tpa.ts index 3e09f60..d58779b 100644 --- a/src/tools/build-ts-tpa.ts +++ b/src/tools/build-ts-tpa.ts @@ -3,21 +3,26 @@ import path, { resolve } from "path"; import process from "process"; import { spawn, type SpawnOptionsWithoutStdio } from "child_process"; import { once } from "events"; -import { program } from "commander"; // this shit is annoying to deal with const exportRegex = - /export\b\s*(?:(?:default\s*)?{\s*((?:[^,{}]+,?)+\b)\s*}|(?:default\s*)?([^,;\s]+));?/gm; + /export\b\s*(?:(?:default\s*)?{\s*((?:[^,{}]+,?)+\b)\s*}|(?:default\s*)?([^,;\s]+));?/m; -const resourceFileRegex = /(?!\w+\.[em]?ts$|\w+\.[em]?js$)(^\w+\.?\w*)/gm; -const scriptFileRegex = /(\w+\.[me]?ts$|\w+\.[me]?js$)/gm; -export const jsFileRegex = /(\w+\.[me]?js$)/gm; -export const tsFileRegex = /(\w+\.[me]?ts$)/gm; +const resourceFileRegex = /(?!\w+\.[em]?ts$|\w+\.[em]?js$)(^\w+\.?\w*)/m; +const scriptFileRegex = /(\w+\.[me]?ts$|\w+\.[me]?js$)/m; +export const jsFileRegex = /(\w+\.[me]?js$)/m; +export const tsFileRegex = /(\w+\.[me]?ts$)/m; // this should NOT be manually updated let silentMode = false; let printDebugMsgs = false; +async function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + function printDebug(message?: any, ...optionalParams: any[]) { if (printDebugMsgs && !silentMode) console.debug(message, ...optionalParams); @@ -123,22 +128,24 @@ function removeDeletedFiles( distRoot: fs.PathLike, cwd: fs.PathLike, ) { + printDebug("Checking for deleted files to remove in dist..\n-----"); const srcFiles = fs .readdirSync(srcRoot, { recursive: true, }) .map((val) => { - if (tsFileRegex.test(val.toString())) { - printDebug( - "converted filename:", - path - .basename(val.toString(), path.extname(val.toString())) - .concat(".js"), - ); - return path - .basename(val.toString(), path.extname(val.toString())) - .concat(".js"); - } else return val; + const srcPath = path.parse(val.toString()); + + const tsFileTestResult = tsFileRegex.test(srcPath.base); + printDebug(`tsFileTest '${srcPath.base}':`, tsFileTestResult); + if (tsFileTestResult) { + printDebug("basename:", srcPath.base); + printDebug("basename (removed ext):", srcPath.name); + printDebug("converted filename:", srcPath.name.concat(".js")); + return path.join(srcPath.dir, srcPath.name).concat(".js"); + } else { + return val; + } }); const distFiles = fs.readdirSync(distRoot, { @@ -156,35 +163,67 @@ function removeDeletedFiles( ); } }); + printDebug("-----\n"); } function copyNewFiles(srcRoot: string, distRoot: string, cwd: fs.PathLike) { + printDebug("Checking for files to copy to dist..\n-----"); const srcFiles = fs .readdirSync(srcRoot, { recursive: true, + withFileTypes: true, + }) + .map((val) => { + const srcPath = path.parse( + path.relative(srcRoot, path.join(val.parentPath, val.name)), + ); + const resFileTestResult = resourceFileRegex.test(srcPath.base); + + if (resFileTestResult && !val.isDirectory()) { + printDebug( + `Value '${path.join(srcPath.dir, srcPath.base)}' is a resource file`, + ); + return path.join(srcPath.dir, srcPath.base); + } }) .filter((val) => { - return val.toString().match(resourceFileRegex)?.[0]; + return val !== undefined; }); const distFiles = fs .readdirSync(distRoot, { recursive: true, + withFileTypes: true, + }) + .map((val) => { + const srcPath = path.parse( + path.relative(distRoot, path.join(val.parentPath, val.name)), + ); + const resFileTestResult = resourceFileRegex.test(srcPath.base); + + if (resFileTestResult && !val.isDirectory()) { + printDebug( + `Value '${path.join(srcPath.dir, srcPath.base)}' is a resource file`, + ); + return path.join(srcPath.dir, srcPath.base); + } }) .filter((val) => { - return val.toString().match(resourceFileRegex)?.[0]; + return val !== undefined; }); printDebug("srcFiles:", srcFiles); printDebug("distFiles:", distFiles); srcFiles.forEach((val) => { - const srcFilePath = path.resolve(srcRoot.toString(), val.toString()); - const distFilePath = path.resolve(distRoot.toString(), val.toString()); + const srcFilePath = path.resolve(srcRoot, val.toString()); + const distFilePath = path.resolve(distRoot, val.toString()); if (!distFiles.includes(val)) { + printDebug(`'${val}' was missing from dist. Copying over.`); FSCopy(srcFilePath, distFilePath, cwd); } else { + printDebug(`'${val}' is present in dist. Updating the file..`); const srcFileContents = fs.readFileSync(srcFilePath); const distFileContents = fs.readFileSync(distFilePath); @@ -193,11 +232,30 @@ function copyNewFiles(srcRoot: string, distRoot: string, cwd: fs.PathLike) { } } }); + printDebug("-----\n"); +} + +export function containsTypescript(searchPath: fs.PathLike) { + printDebug("Checking if folder contains TypeScript files."); + const fileList = fs.readdirSync(searchPath, { recursive: true }); + + for (const val of fileList) { + const srcPath = path.parse(val.toString()); + const result = tsFileRegex.test(srcPath.base); + printDebug(`tsFileTest '${srcPath.base}':`, result); + if (result) { + printDebug("The given folder does contain TypeScript files."); + return true; + } + } + + printDebug("The given folder does not contain any TypeScript files."); + return false; } function replaceExport(contents: string) { return contents.replace(exportRegex, (subStr: string, ...args: any[]) => { - printDebug(`export replace subStr: '${subStr}'\nargs:`, args, "\n"); + // printDebug(`export replace subStr: '${subStr}'\nargs:`, args, "\n"); const hasCurlyBrackets = subStr.includes("{"); @@ -240,8 +298,17 @@ async function compileAndCopySrc( tmpSrc: string, cwd: fs.PathLike, ) { + printDebug("Attempting to compile source..\n-----"); + const pathToTsc = require.resolve("typescript/bin/tsc"); + if (!fs.existsSync(resolve(cwd.toString(), "tsconfig.json"))) { + console.error( + "The project directory must have a properly configured 'tsconfig.json' file.", + ); + process.exit(-1); + } + await runCommand( "node", [ @@ -259,22 +326,58 @@ async function compileAndCopySrc( const distFiles = fs .readdirSync(distRoot, { recursive: true, + withFileTypes: true, + }) + .map((val) => { + const srcPath = path.parse( + path.relative(distRoot, path.join(val.parentPath, val.name)), + ); + const scriptFileTestResult = scriptFileRegex.test(srcPath.base); + + printDebug( + `scriptFileTest '${path.join(srcPath.dir, srcPath.base)}':`, + scriptFileTestResult, + ); + + if (scriptFileTestResult && !val.isDirectory()) { + printDebug( + `Value '${path.join(srcPath.dir, srcPath.base)}' is a script file`, + ); + return path.join(srcPath.dir, srcPath.base); + } }) .filter((val) => { - return val.toString().match(scriptFileRegex)?.[0]; + return val !== undefined; }); + // .filter((val) => { + // return val.toString().match(scriptFileRegex)?.[0]; + // }); - const tmpSrcFiles = fs.readdirSync(tmpSrc, { - recursive: true, - }); + const tmpSrcFiles = fs + .readdirSync(tmpSrc, { + recursive: true, + withFileTypes: true, + }) + .map((val) => { + const srcPath = path.parse( + path.relative(tmpSrc, path.join(val.parentPath, val.name)), + ); + if (!val.isDirectory()) { + return path.join(srcPath.dir, srcPath.base); + } + }) + .filter((val) => { + return val !== undefined; + }); printDebug("distFiles:", distFiles); printDebug("tmpSrcFiles:", tmpSrcFiles); - tmpSrcFiles.forEach(async (val) => { + for (const val of tmpSrcFiles) { const tmpSrcFilePath = path.resolve(tmpSrc, val.toString()); const distFilePath = path.resolve(distRoot, val.toString()); + // await sleep(1000); if (!fs.existsSync(distFilePath)) { FSCopy(tmpSrcFilePath, distFilePath, cwd); conditionalFSRemove(tmpSrcFilePath, cwd); @@ -292,11 +395,13 @@ async function compileAndCopySrc( conditionalFSRemove(tmpSrcFilePath, cwd); } } - }); + } conditionalFSRemove(tmpRoot, cwd); fixExports(distRoot); + + printDebug("-----\n"); } export async function buildTSTPA( @@ -317,6 +422,9 @@ export async function buildTSTPA( recursive: true, }); } + + printDebug(); + removeDeletedFiles(srcRoot, distRoot, cwd); copyNewFiles(srcRoot, distRoot, cwd); From 9723012a04ac8a71471727e3fb1bc1e851b2092b Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Mon, 15 Jun 2026 00:02:22 -0500 Subject: [PATCH 06/14] =?UTF-8?q?forgot=20to=20disable=20the=20debug=20tex?= =?UTF-8?q?t=20=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/dev/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/dev/index.ts b/src/commands/dev/index.ts index 702d27e..e9d3935 100644 --- a/src/commands/dev/index.ts +++ b/src/commands/dev/index.ts @@ -41,7 +41,7 @@ export default async function DevCommand() { const containsTS = containsTypescript(project.metadata!.payloadDir); if (containsTS) { - await buildTSTPA(project.path, false, true); + await buildTSTPA(project.path); } await StartServer(project); From 3a46c057a945e27bd7029e9cba65f2436a2ab43d Mon Sep 17 00:00:00 2001 From: Izaak Kuipers Date: Mon, 15 Jun 2026 11:35:19 +0200 Subject: [PATCH 07/14] Automated refactor to fix indents etc --- package.json | 104 ++-- src/commands/build/index.ts | 112 ++-- src/commands/dev/index.ts | 95 ++-- src/commands/new/index.ts | 229 ++++----- src/commands/update.ts | 6 +- src/index.ts | 4 +- src/json.ts | 15 +- src/project/index.ts | 24 +- src/server/api/index.ts | 187 ++++--- src/server/api/routes/fs/cp.ts | 2 +- src/server/api/routes/fs/direct/get.ts | 2 +- src/server/api/routes/fs/mv.ts | 2 +- src/server/api/routes/fs/tree/get.ts | 2 +- src/server/fs/index.ts | 540 ++++++++++---------- src/server/websocket/index.ts | 6 +- src/tools/build-ts-tpa.ts | 675 ++++++++++++------------- src/tpa/index.ts | 6 +- src/tpa/wizard.ts | 1 - src/types/project.ts | 6 +- tsconfig.json | 22 +- 20 files changed, 1008 insertions(+), 1032 deletions(-) diff --git a/package.json b/package.json index caf3dca..064334d 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,54 @@ { - "name": "v7cli", - "version": "1.0.19", - "main": "index.js", - "scripts": { - "cli:publish": "npx tsc && npm publish" - }, - "bin": { - "v7cli": "dist/index.js" - }, - "type": "commonjs", - "keywords": [], - "author": "Izaak Kuipers (https://izkuipers.nl/)", - "license": "MIT", - "description": "A command-line tool for developing ArcOS apps", - "devDependencies": { - "@types/node": "^22.15.19", - "ts-node": "^10.9.2", - "typescript": "^5.8.3" - }, - "files": [ - "./dist", - "./package.json", - "./README.md", - "./LICENSE" - ], - "dependencies": { - "@clack/prompts": "^0.10.1", - "@types/colors": "^1.2.4", - "@types/cookie-parser": "^1.4.8", - "@types/cors": "^2.8.18", - "@types/express": "^5.0.2", - "@types/mime": "^4.0.0", - "@types/mime-types": "^2.1.4", - "@types/multer": "^1.4.12", - "@types/signale": "^1.4.7", - "@types/socket.io": "^3.0.2", - "axios": "^1.9.0", - "check-disk-space": "^3.4.0", - "colors.js": "^1.2.4", - "commander": "^14.0.0", - "cookie-parser": "^1.4.7", - "cors": "^2.8.5", - "express.js": "^1.0.0", - "mime": "^4.0.7", - "mime-types": "^3.0.1", - "multer": "^2.0.0", - "signale": "^1.4.0", - "socket.io": "^4.8.1", - "socket.io-server": "^1.0.0-b", - "tslib": "^2.8.1", - "zip-a-folder": "^3.1.9" - } + "name": "v7cli", + "version": "1.0.19", + "main": "index.js", + "scripts": { + "cli:publish": "npx tsc && npm publish" + }, + "bin": { + "v7cli": "dist/index.js" + }, + "type": "commonjs", + "keywords": [], + "author": "Izaak Kuipers (https://izkuipers.nl/)", + "license": "MIT", + "description": "A command-line tool for developing ArcOS apps", + "devDependencies": { + "@types/node": "^22.15.19", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "files": [ + "./dist", + "./package.json", + "./README.md", + "./LICENSE" + ], + "dependencies": { + "@clack/prompts": "^0.10.1", + "@types/colors": "^1.2.4", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.18", + "@types/express": "^5.0.2", + "@types/mime": "^4.0.0", + "@types/mime-types": "^2.1.4", + "@types/multer": "^1.4.12", + "@types/signale": "^1.4.7", + "@types/socket.io": "^3.0.2", + "axios": "^1.9.0", + "check-disk-space": "^3.4.0", + "colors.js": "^1.2.4", + "commander": "^14.0.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "express.js": "^1.0.0", + "mime": "^4.0.7", + "mime-types": "^3.0.1", + "multer": "^2.0.0", + "signale": "^1.4.0", + "socket.io": "^4.8.1", + "socket.io-server": "^1.0.0-b", + "tslib": "^2.8.1", + "zip-a-folder": "^3.1.9" + } } diff --git a/src/commands/build/index.ts b/src/commands/build/index.ts index 21ea14e..c8689a0 100644 --- a/src/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -1,79 +1,79 @@ import { intro, spinner } from "@clack/prompts"; -import { cp, mkdir, rm, writeFile, readdir } from "fs/promises"; +import { cp, mkdir, rm, writeFile } from "fs/promises"; import { join } from "path"; import { cwd } from "process"; +import signale from "signale"; import { zip } from "zip-a-folder"; import { Project } from "../../project"; -import signale from "signale"; import buildTSTPA, { containsTypescript } from "../../tools/build-ts-tpa"; export default async function BuildCommand() { - try { - const project = new Project(cwd()); - await project.readProjectFile(); + try { + const project = new Project(cwd()); + await project.readProjectFile(); - if (!project.metadata) return; + if (!project.metadata) return; - if (await project.areTypeDefsOutdated()) { - signale.warn( - "Type definitions are outdated. Please run `npx v7cli update` to update them.", - ); - } + if (await project.areTypeDefsOutdated()) { + signale.warn( + "Type definitions are outdated. Please run `npx v7cli update` to update them.", + ); + } - const appId = project.metadata?.metadata.appId; + const appId = project.metadata?.metadata.appId; - if (appId?.includes(".") || appId?.includes("-")) { - signale.error( - "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa", - ); - process.exit(1); - } + if (appId?.includes(".") || appId?.includes("-")) { + signale.error( + "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa", + ); + process.exit(1); + } - intro("Build ArcOS Package"); - const spin = spinner(); - spin.start("Building..."); + intro("Build ArcOS Package"); + const spin = spinner(); + spin.start("Building..."); - spin.message("Creating temp directory"); - await mkdir(join(project.path, ".arcdev-build"), { recursive: true }); + spin.message("Creating temp directory"); + await mkdir(join(project.path, ".arcdev-build"), { recursive: true }); - // DO THE BUILD HERE - const containsTS = containsTypescript(project.metadata.payloadDir); + // DO THE BUILD HERE + const containsTS = containsTypescript(project.metadata.payloadDir); - const payloadDir = containsTS - ? join(project.path, "dist") - : join(project.path, project.metadata.payloadDir); + const payloadDir = containsTS + ? join(project.path, "dist") + : join(project.path, project.metadata.payloadDir); - if (containsTS) { - spin.message("Compiling project"); - await buildTSTPA(project.path, true); - } + if (containsTS) { + spin.message("Compiling project"); + await buildTSTPA(project.path, true); + } - spin.message("Copying payload"); - await cp(payloadDir, ".arcdev-build/payload", { - recursive: true, - }); + spin.message("Copying payload"); + await cp(payloadDir, ".arcdev-build/payload", { + recursive: true, + }); - spin.message("Writing _metadata.json"); - await writeFile( - join(project.path, ".arcdev-build", "_metadata.json"), - JSON.stringify(project.metadata.metadata, null, 2), - "utf-8", - ); + spin.message("Writing _metadata.json"); + await writeFile( + join(project.path, ".arcdev-build", "_metadata.json"), + JSON.stringify(project.metadata.metadata, null, 2), + "utf-8", + ); - spin.message("Bundling package"); - await zip( - join(project.path, ".arcdev-build"), - join(project.path, project.metadata.outFile), - ); + spin.message("Bundling package"); + await zip( + join(project.path, ".arcdev-build"), + join(project.path, project.metadata.outFile), + ); - spin.message("Removing temp directory"); - await rm(join(project.path, ".arcdev-build"), { - recursive: true, - force: true, - }); + spin.message("Removing temp directory"); + await rm(join(project.path, ".arcdev-build"), { + recursive: true, + force: true, + }); - spin.stop("Done!"); - } catch (e) { - console.log(e); - } + spin.stop("Done!"); + } catch (e) { + console.log(e); + } } diff --git a/src/commands/dev/index.ts b/src/commands/dev/index.ts index e9d3935..cdd8ce6 100644 --- a/src/commands/dev/index.ts +++ b/src/commands/dev/index.ts @@ -1,69 +1,68 @@ import "colors"; import { cwd } from "process"; +import signale from "signale"; import packageJson from "../../../package.json"; +import { getArcBuild } from "../../build"; import { Project } from "../../project"; import { StartServer } from "../../server/api"; -import signale from "signale"; -import { getArcBuild } from "../../build"; -import { readdirSync } from "fs"; import buildTSTPA, { containsTypescript } from "../../tools/build-ts-tpa"; export default async function DevCommand() { - const project = new Project(cwd()); + const project = new Project(cwd()); - await project.readProjectFile(); + await project.readProjectFile(); - if (await project.areTypeDefsOutdated()) { - signale.warn( - "Type definitions are outdated. Please run `npx v7cli update` to update them.", - ); - } + if (await project.areTypeDefsOutdated()) { + signale.warn( + "Type definitions are outdated. Please run `npx v7cli update` to update them.", + ); + } - const appId = project.metadata?.metadata.appId; + const appId = project.metadata?.metadata.appId; - if (appId?.includes(".") || appId?.includes("-")) { - signale.error( - "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa", - ); + if (appId?.includes(".") || appId?.includes("-")) { + signale.error( + "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa", + ); - process.exit(1); - } + process.exit(1); + } - const buildHash = await getArcBuild(); + const buildHash = await getArcBuild(); - if ( - project.metadata?.buildHash == null || - project.metadata.buildHash != buildHash - ) { - project.metadata!!.buildHash = buildHash; - } + if ( + project.metadata?.buildHash == null || + project.metadata.buildHash != buildHash + ) { + project.metadata!!.buildHash = buildHash; + } - const containsTS = containsTypescript(project.metadata!.payloadDir); + const containsTS = containsTypescript(project.metadata!.payloadDir); - if (containsTS) { - await buildTSTPA(project.path); - } + if (containsTS) { + await buildTSTPA(project.path); + } - await StartServer(project); + await StartServer(project); - const name = "ArcOS v7 CLI".green.bold; - const version = `v${packageJson.version}`.gray; - const arrow = " > ".gray; - const port = project.metadata?.devPort || 3128; - const url = `http://localhost:${port}`; - const lis = "Listening on:".yellow; - const run = "Run command:".yellow; - const command = `devenv connect ${port}`.blue; + const name = "ArcOS v7 CLI".green.bold; + const version = `v${packageJson.version}`.gray; + const arrow = " > ".gray; + const port = project.metadata?.devPort || 3128; + const url = `http://localhost:${port}`; + const lis = "Listening on:".yellow; + const run = "Run command:".yellow; + const command = `devenv connect ${port}`.blue; - console.log( - [ - "", - ` ${name} ${version}`, - "", - `${arrow}${lis} ${url}`, - `${arrow}${run} ${command} in ArcTerm to connect`, - "", - " READY.".green.bold, - ].join("\n"), - ); + console.log( + [ + "", + ` ${name} ${version}`, + "", + `${arrow}${lis} ${url}`, + `${arrow}${run} ${command} in ArcTerm to connect`, + "", + " READY.".green.bold, + ].join("\n"), + ); } diff --git a/src/commands/new/index.ts b/src/commands/new/index.ts index c4e92fa..67fe59d 100644 --- a/src/commands/new/index.ts +++ b/src/commands/new/index.ts @@ -1,11 +1,11 @@ import { - cancel, - intro, - isCancel, - outro, - select, - spinner, - text, + cancel, + intro, + isCancel, + outro, + select, + spinner, + text, } from "@clack/prompts"; import { Command } from "commander"; import { userInfo } from "os"; @@ -17,118 +17,111 @@ import { TpaWizard } from "../../tpa/wizard"; import { PackageMetadata } from "../../types/package"; export default async function NewCommand(this: Command, destination: string) { - intro(`Create ArcOS Project - ${destination}`); - - const name = await text({ - message: "What do you want to name your app?", - initialValue: "", - validate(value) { - if (!value) return "A name is required"; - }, - }); - - if (isCancel(name)) abort(); - - const description = await text({ - message: "Please give a description for your app", - initialValue: "", - - validate(value) { - if (!value) return `A description is required`; - if (value.length > 512) - return `Too long! Pick a description under 512 characters.`; - }, - }); - - if (isCancel(description)) abort(); - - const author = await text({ - message: "Who's the author?", - initialValue: userInfo().username, - validate(value) { - if (!value) return `An author is required`; - if (value.length > 32) - return `Too long! Pick a name under 32 characters.`; - }, - }); - - if (isCancel(author)) abort(); - - const version = await text({ - message: "What version is your app?", - initialValue: "1.0.0", - validate(value) { - if (value.length !== 5 || value[1] !== "." || value[3] !== ".") - return "Need a version in an x.x.x format"; - }, - }); - - if (isCancel(version)) abort(); - - const appId = await text({ - message: "What ID do you want your app to have?", - initialValue: "", - validate(value) { - if ( - !value.includes("_") || - value.includes(".") || - value.includes("-") - ) - return "The ID has to be the format 'author_appId', and it may not include any periods or dashes."; - }, - }); - - if (isCancel(appId)) abort(); - - const installLocation = await text({ - message: "Where will this app install?", - initialValue: `U:/Applications/${appId.toString()}`, - validate(value) { - if ( - !value.startsWith(`U:/Applications/`) || - value === `U:/Applications/` - ) - return "This has to be an ArcOS path that starts with 'U:/Applications/'"; - }, - }); - - if (isCancel(installLocation)) abort(); - - const isTypeScriptProject = await select({ - message: "Do you want to enable experimental TS support?", - options: [ - { value: "true", label: "Sure!" }, - { value: "false", label: "No thanks." }, - ], - }); - - if (isCancel(isTypeScriptProject)) abort(); - - const metadata: PackageMetadata = { - name: name.toString(), - description: description.toString(), - author: author.toString(), - version: version.toString(), - installLocation: installLocation.toString(), - appId: appId.toString(), - }; - - const spin = spinner(); - spin.start("Initializing project"); - - const project = new Project(join(cwd(), destination)); - - await project.initialize(metadata, `${metadata.appId}.arc`, "src"); - - spin.stop("Done."); - outro(); - - const app = await TpaWizard(metadata); - - scaffoldProject(app, project); + intro(`Create ArcOS Project - ${destination}`); + + const name = await text({ + message: "What do you want to name your app?", + initialValue: "", + validate(value) { + if (!value) return "A name is required"; + }, + }); + + if (isCancel(name)) abort(); + + const description = await text({ + message: "Please give a description for your app", + initialValue: "", + + validate(value) { + if (!value) return `A description is required`; + if (value.length > 512) + return `Too long! Pick a description under 512 characters.`; + }, + }); + + if (isCancel(description)) abort(); + + const author = await text({ + message: "Who's the author?", + initialValue: userInfo().username, + validate(value) { + if (!value) return `An author is required`; + if (value.length > 32) + return `Too long! Pick a name under 32 characters.`; + }, + }); + + if (isCancel(author)) abort(); + + const version = await text({ + message: "What version is your app?", + initialValue: "1.0.0", + validate(value) { + if (value.length !== 5 || value[1] !== "." || value[3] !== ".") + return "Need a version in an x.x.x format"; + }, + }); + + if (isCancel(version)) abort(); + + const appId = await text({ + message: "What ID do you want your app to have?", + initialValue: "", + validate(value) { + if (!value.includes("_") || value.includes(".") || value.includes("-")) + return "The ID has to be the format 'author_appId', and it may not include any periods or dashes."; + }, + }); + + if (isCancel(appId)) abort(); + + const installLocation = await text({ + message: "Where will this app install?", + initialValue: `U:/Applications/${appId.toString()}`, + validate(value) { + if (!value.startsWith(`U:/Applications/`) || value === `U:/Applications/`) + return "This has to be an ArcOS path that starts with 'U:/Applications/'"; + }, + }); + + if (isCancel(installLocation)) abort(); + + const isTypeScriptProject = await select({ + message: "Do you want to enable experimental TS support?", + options: [ + { value: "true", label: "Sure!" }, + { value: "false", label: "No thanks." }, + ], + }); + + if (isCancel(isTypeScriptProject)) abort(); + + const metadata: PackageMetadata = { + name: name.toString(), + description: description.toString(), + author: author.toString(), + version: version.toString(), + installLocation: installLocation.toString(), + appId: appId.toString(), + }; + + const spin = spinner(); + spin.start("Initializing project"); + + const project = new Project(join(cwd(), destination)); + + await project.initialize(metadata, `${metadata.appId}.arc`, "src"); + + spin.stop("Done."); + outro(); + + const app = await TpaWizard(metadata); + + scaffoldProject(app, project); } export function abort(): any { - cancel("Aborted."); - return process.exit(0) as any; + cancel("Aborted."); + return process.exit(0) as any; } diff --git a/src/commands/update.ts b/src/commands/update.ts index ede0cf4..4396706 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -1,7 +1,7 @@ +import { spinner } from "@clack/prompts"; import { cwd } from "process"; -import { Project } from "../project"; import signale from "signale"; -import { spinner } from "@clack/prompts"; +import { Project } from "../project"; export default async function UpdateCommand() { const project = new Project(cwd()); @@ -17,7 +17,7 @@ export default async function UpdateCommand() { const appId = project.metadata?.metadata.appId; if (appId?.includes(".") || appId?.includes("-")) { signale.error( - "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa" + "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa", ); process.exit(1); diff --git a/src/index.ts b/src/index.ts index e068d41..fbacba9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,9 @@ import { Command } from "commander"; import packageJson from "../package.json"; -import NewCommand from "./commands/new"; -import DevCommand from "./commands/dev"; import BuildCommand from "./commands/build"; +import DevCommand from "./commands/dev"; +import NewCommand from "./commands/new"; import UpdateCommand from "./commands/update"; const program = new Command(); diff --git a/src/json.ts b/src/json.ts index 668ecaa..712514b 100644 --- a/src/json.ts +++ b/src/json.ts @@ -10,10 +10,13 @@ export function keysToLowerCase(obj: any): any { if (Array.isArray(obj)) { return obj.map(keysToLowerCase); } else if (obj !== null && typeof obj === "object") { - return Object.entries(obj).reduce((acc, [key, value]) => { - acc[key.toLowerCase()] = keysToLowerCase(value); - return acc; - }, {} as Record); + return Object.entries(obj).reduce( + (acc, [key, value]) => { + acc[key.toLowerCase()] = keysToLowerCase(value); + return acc; + }, + {} as Record, + ); } return obj; } @@ -21,7 +24,7 @@ export type ValidationObject = { [key: string]: any }; export function validateObject( target: ValidationObject, - validation: ValidationObject + validation: ValidationObject, ): boolean { if (typeof validation !== "object" || validation === null) return false; @@ -41,7 +44,7 @@ export function validateObject( if ( !validationValue.every((val, index) => - validateObject(targetValue[index], val) + validateObject(targetValue[index], val), ) ) return false; diff --git a/src/project/index.ts b/src/project/index.ts index 28d84f4..0226e28 100644 --- a/src/project/index.ts +++ b/src/project/index.ts @@ -25,11 +25,11 @@ export class Project { outFile: string, payloadDir: string, repository?: string, - devPort?: number + devPort?: number, ) { if (existsSync(this.path) && (await readdir(this.path)).length) { signale.error( - "Cannot initialize project: directory exists and is not empty" + "Cannot initialize project: directory exists and is not empty", ); process.exit(1); } @@ -40,13 +40,13 @@ export class Project { outFile, payloadDir, repository, - devPort + devPort, ); await mkdir(join(this.path, payloadDir)); await mkdir(join(this.path, ".vscode")); await writeFile( join(this.path, ".gitignore"), - `${this.metadata!.metadata.appId}.arc\n.arcdev-build/` + `${this.metadata!.metadata.appId}.arc\n.arcdev-build/`, ); await writeFile( join(this.path, ".vscode/settings.json"), @@ -57,8 +57,8 @@ export class Project { }, }, null, - 2 - ) + 2, + ), ); await writeFile( @@ -79,9 +79,9 @@ export class Project { include: ["./src/**/*"], }, null, - 2 + 2, ), - "utf-8" + "utf-8", ); await this.writeTypeDefs(); @@ -98,7 +98,7 @@ export class Project { outFile: string, payloadDir: string, repository?: string, - devPort?: number + devPort?: number, ) { const meta: ProjectMetadata = { metadata, @@ -113,7 +113,7 @@ export class Project { await writeFile( join(this.path, "project.arc.json"), JSON.stringify(meta, null, 2), - "utf-8" + "utf-8", ); await this.readProjectFile(); } @@ -122,7 +122,7 @@ export class Project { await writeFile( join(this.path, "project.arc.json"), JSON.stringify(this.metadata!, null, 2), - "utf-8" + "utf-8", ); } @@ -130,7 +130,7 @@ export class Project { try { const contents = await readFile( join(this.path, "project.arc.json"), - "utf-8" + "utf-8", ); this.metadata = JSON.parse(contents); diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 62e7594..108f344 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -1,139 +1,126 @@ -import type { Request, Response } from "express"; -import express from "express"; +import "colors"; import cookieParser from "cookie-parser"; import cors from "cors"; +import type { Request, Response } from "express"; +import express from "express"; +import { watch } from "fs"; import multer from "multer"; +import { join } from "path"; +import signale, { Signale } from "signale"; import { Project } from "../../project"; +import buildTSTPA, { containsTypescript } from "../../tools/build-ts-tpa"; import { Method } from "../../types/api"; import { RouteStore, RouteType } from "../../types/project"; +import { WebSock } from "../websocket"; import { corsOptions } from "./cors"; import { Routes } from "./routes"; -import { SockLog, WebSock } from "../websocket"; -import { readdirSync, watch } from "fs"; -import { join } from "path"; -import "colors"; -import signale, { Signale } from "signale"; -import buildTSTPA, { containsTypescript } from "../../tools/build-ts-tpa"; export const App = express(); export const APILog = new Signale({ - scope: "API", - interactive: false, + scope: "API", + interactive: false, }); App.use(cors(corsOptions), cookieParser(), multer().any() as any); export async function StartServer(project: Project) { - App.options("*", cors(corsOptions)); - App.post( - "/fs/file/:path(*)", - express.raw({ type: "*/*", limit: "1000mb" }), - ); - App.set("trust proxy", true); + App.options("*", cors(corsOptions)); + App.post("/fs/file/:path(*)", express.raw({ type: "*/*", limit: "1000mb" })); + App.set("trust proxy", true); + + const containsTS = containsTypescript(project.metadata!.payloadDir); + + return new Promise((r) => { + const server = App.listen(project.metadata?.devPort || 3128, () => { + assignRoutes(project, ...Routes()); - const containsTS = containsTypescript(project.metadata!.payloadDir); + project.websock = new WebSock(server, project.metadata!); + project.websock.start(); - return new Promise((r) => { - const server = App.listen(project.metadata?.devPort || 3128, () => { - assignRoutes(project, ...Routes()); + if (project.metadata?.noHotRelaunch) { + signale.warn("noHotRelaunch is set: not enabling file watcher."); - project.websock = new WebSock(server, project.metadata!); - project.websock.start(); + return r(); + } - if (project.metadata?.noHotRelaunch) { - signale.warn( - "noHotRelaunch is set: not enabling file watcher.", - ); + let watchTimeout: NodeJS.Timeout | undefined; + let watchTimeoutTS = false; - return r(); + watch( + join(project.path, project.metadata!.payloadDir), + { persistent: true, recursive: true }, + async (e, filename) => { + if (!watchTimeout && !watchTimeoutTS) { + if (containsTS) watchTimeoutTS = true; + if (filename?.endsWith(".css")) { + APILog.warn(`${filename || e}: Change detected, reloading CSS`); + project.websock?.client?.sock.emit("refresh-css", filename); + } else { + APILog.warn( + `${filename || e}: Change detected, restarting ${ + project.metadata?.metadata.appId + }`, + ); + if (containsTS) { + await buildTSTPA(project.path, false, true); + } + project.websock?.client?.sock.emit("restart-tpa"); } + watchTimeoutTS = false; + watchTimeout = setTimeout(() => (watchTimeout = undefined), 200); + } + }, + ); - let watchTimeout: NodeJS.Timeout | undefined; - let watchTimeoutTS = false; - - watch( - join(project.path, project.metadata!.payloadDir), - { persistent: true, recursive: true }, - async (e, filename) => { - if (!watchTimeout && !watchTimeoutTS) { - if (containsTS) watchTimeoutTS = true; - if (filename?.endsWith(".css")) { - APILog.warn( - `${filename || e}: Change detected, reloading CSS`, - ); - project.websock?.client?.sock.emit( - "refresh-css", - filename, - ); - } else { - APILog.warn( - `${filename || e}: Change detected, restarting ${ - project.metadata?.metadata.appId - }`, - ); - if (containsTS) { - await buildTSTPA(project.path, false, true); - } - project.websock?.client?.sock.emit("restart-tpa"); - } - watchTimeoutTS = false; - watchTimeout = setTimeout( - () => (watchTimeout = undefined), - 200, - ); - } - }, - ); - - r(); - }); + r(); }); + }); } export function assignRoute(route: RouteType, project: Project) { - const fun = MethodTranslations()[route.method]; + const fun = MethodTranslations()[route.method]; - fun(route.path, (req: Request, res: Response) => { - const stop = (c = 400, json?: object) => { - if (json) { - res.status(c).json(json); - } else { - res.status(c).end(); - } + fun(route.path, (req: Request, res: Response) => { + const stop = (c = 400, json?: object) => { + if (json) { + res.status(c).json(json); + } else { + res.status(c).end(); + } - return ""; - }; + return ""; + }; - try { - route.callback(req, res, stop, project); - APILog.info(`${route.method.blue} ${route.path}`); - } catch (e) { - APILog.error(`${route.method.blue} ${route.path}: ${`${e}`.red}`); - stop(500); - } + try { + route.callback(req, res, stop, project); + APILog.info(`${route.method.blue} ${route.path}`); + } catch (e) { + APILog.error(`${route.method.blue} ${route.path}: ${`${e}`.red}`); + stop(500); + } - return; - }); + return; + }); - return true; + return true; } export function assignRoutes(project: Project, ...stores: RouteStore[]) { - for (const store of stores) { - for (const [method, path, callback, maxRequests] of store) { - assignRoute({ method, path, callback, maxRequests }, project); - } + for (const store of stores) { + for (const [method, path, callback, maxRequests] of store) { + assignRoute({ method, path, callback, maxRequests }, project); } + } } export function MethodTranslations(): Record any> { - return { - get: App.get.bind(App), - post: App.post.bind(App), - options: App.options.bind(App), - delete: App.delete.bind(App), - put: App.put.bind(App), - all: App.all.bind(App), - patch: App.patch.bind(App), - head: App.head.bind(App), - }; + return { + get: App.get.bind(App), + post: App.post.bind(App), + options: App.options.bind(App), + delete: App.delete.bind(App), + put: App.put.bind(App), + all: App.all.bind(App), + patch: App.patch.bind(App), + head: App.head.bind(App), + }; } diff --git a/src/server/api/routes/fs/cp.ts b/src/server/api/routes/fs/cp.ts index 5ccc75c..3ce3241 100644 --- a/src/server/api/routes/fs/cp.ts +++ b/src/server/api/routes/fs/cp.ts @@ -9,7 +9,7 @@ export const FsCp: RouteArrayed = [ try { await project.filesystem?.copyItem( req.params.source, - req.body.destination + req.body.destination, ); return stop(200); diff --git a/src/server/api/routes/fs/direct/get.ts b/src/server/api/routes/fs/direct/get.ts index b758b9d..dfc7834 100644 --- a/src/server/api/routes/fs/direct/get.ts +++ b/src/server/api/routes/fs/direct/get.ts @@ -37,7 +37,7 @@ export const FsDirectGet: RouteArrayed = [ const fileStream = await project.filesystem?.createReadStream( path, start, - finalEnd + finalEnd, ); fileStream?.pipe(res); diff --git a/src/server/api/routes/fs/mv.ts b/src/server/api/routes/fs/mv.ts index 820d440..d25ccc1 100644 --- a/src/server/api/routes/fs/mv.ts +++ b/src/server/api/routes/fs/mv.ts @@ -9,7 +9,7 @@ export const FsMv: RouteArrayed = [ try { await project.filesystem?.moveItem( req.params.source, - req.body.destination + req.body.destination, ); return stop(200); diff --git a/src/server/api/routes/fs/tree/get.ts b/src/server/api/routes/fs/tree/get.ts index 209a122..eba10cd 100644 --- a/src/server/api/routes/fs/tree/get.ts +++ b/src/server/api/routes/fs/tree/get.ts @@ -8,7 +8,7 @@ export const FstreeGetPath: RouteArrayed = [ try { const contents = await project.filesystem?.getDirectoryTree( - req.params.path + req.params.path, ); return res.json(contents); diff --git a/src/server/fs/index.ts b/src/server/fs/index.ts index 1f723f6..7f46c3e 100644 --- a/src/server/fs/index.ts +++ b/src/server/fs/index.ts @@ -1,329 +1,325 @@ -import { createReadStream, existsSync, readdirSync, statSync } from "fs"; +import checkDiskSpace from "check-disk-space"; +import { createReadStream, existsSync, statSync } from "fs"; import fs from "fs/promises"; import mime from "mime-types"; +import { platform } from "os"; import path, { join } from "path"; import { tryJsonParse } from "../../json"; -import { Project } from "../../project"; +import { containsTypescript } from "../../tools/build-ts-tpa"; import { - DirectoryReadReturn, - FileEntry, - FolderEntry, - RecursiveDirectoryReadReturn, - UserQuota, + DirectoryReadReturn, + FileEntry, + FolderEntry, + RecursiveDirectoryReadReturn, + UserQuota, } from "../../types/fs"; -import { platform } from "os"; -import checkDiskSpace from "check-disk-space"; -import { containsTypescript } from "../../tools/build-ts-tpa"; export class Filesystem { - private path: string; - accessors: Record = {}; // R + private path: string; + accessors: Record = {}; // R - constructor(projectPath: string, payloadDir: string) { - if (!existsSync(payloadDir)) { - this.path = join(projectPath, payloadDir); - return; - } - - const containsTS = containsTypescript(payloadDir); - - if (containsTS) { - this.path = join(projectPath, "dist"); - } else { - this.path = join(projectPath, payloadDir); - } + constructor(projectPath: string, payloadDir: string) { + if (!existsSync(payloadDir)) { + this.path = join(projectPath, payloadDir); + return; } - private resolvePath(relativePath?: string): string { - const resolvedPath = relativePath - ? path.resolve(this.path, relativePath) - : this.path; + const containsTS = containsTypescript(payloadDir); - if (!resolvedPath.startsWith(this.path)) - throw new Error("Invalid path; breaks out of project payload"); - - return resolvedPath; + if (containsTS) { + this.path = join(projectPath, "dist"); + } else { + this.path = join(projectPath, payloadDir); } + } - private async calculateFolderSize(folderPath?: string) { - const resolvedPath = this.resolvePath(folderPath); - - const calculateSize = async (directory: string): Promise => { - const entries = await fs.readdir(directory, { - withFileTypes: true, - }); - - let totalSize = 0; + private resolvePath(relativePath?: string): string { + const resolvedPath = relativePath + ? path.resolve(this.path, relativePath) + : this.path; - for (const entry of entries) { - const entryPath = path.join(directory, entry.name); - if (entry.isDirectory()) { - totalSize += await calculateSize(entryPath); - } else if (entry.isFile()) { - totalSize += statSync(entryPath).size; - } - } + if (!resolvedPath.startsWith(this.path)) + throw new Error("Invalid path; breaks out of project payload"); - return totalSize; - }; + return resolvedPath; + } - return calculateSize(resolvedPath); - } + private async calculateFolderSize(folderPath?: string) { + const resolvedPath = this.resolvePath(folderPath); - private async countFiles(folderPath?: string) { - const resolvedPath = this.resolvePath(folderPath); + const calculateSize = async (directory: string): Promise => { + const entries = await fs.readdir(directory, { + withFileTypes: true, + }); - const calculate = async (directory: string): Promise => { - let count = 0; - const entries = await fs.readdir(directory, { - withFileTypes: true, - }); + let totalSize = 0; - for (const entry of entries) { - const entryPath = path.join(directory, entry.name); + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + totalSize += await calculateSize(entryPath); + } else if (entry.isFile()) { + totalSize += statSync(entryPath).size; + } + } - if (entry.isDirectory()) count += await calculate(entryPath); + return totalSize; + }; - count++; - } + return calculateSize(resolvedPath); + } - return count; - }; + private async countFiles(folderPath?: string) { + const resolvedPath = this.resolvePath(folderPath); - return await calculate(resolvedPath); - } + const calculate = async (directory: string): Promise => { + let count = 0; + const entries = await fs.readdir(directory, { + withFileTypes: true, + }); - private async countFolders(folderPath?: string) { - const resolvedPath = this.resolvePath(folderPath); + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); - const calculate = async (directory: string): Promise => { - let count = 0; - const entries = await fs.readdir(directory, { - withFileTypes: true, - }); + if (entry.isDirectory()) count += await calculate(entryPath); - for (const entry of entries) { - const entryPath = path.join(directory, entry.name); + count++; + } - if (entry.isDirectory()) { - count++; - count += await calculate(entryPath); - } - } + return count; + }; - return count; - }; + return await calculate(resolvedPath); + } - return await calculate(resolvedPath); - } + private async countFolders(folderPath?: string) { + const resolvedPath = this.resolvePath(folderPath); - public async writeFile(filePath: string, content: string | Buffer) { - const resolvedPath = this.resolvePath(filePath); + const calculate = async (directory: string): Promise => { + let count = 0; + const entries = await fs.readdir(directory, { + withFileTypes: true, + }); - await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); - await fs.writeFile(resolvedPath, content); - } - - public async readFile(filePath: string): Promise { - const resolvedPath = this.resolvePath(filePath); - - return fs.readFile(resolvedPath); - } - - public async pipeFile(filePath: string) { - const resolvedPath = this.resolvePath(filePath); + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); - return createReadStream(resolvedPath); - } - - public async createDirectory(dirPath: string): Promise { - const resolvedPath = await this.resolvePath(dirPath); - - await fs.mkdir(resolvedPath, { recursive: true }); - } - - public async readDirectory( - dirPath?: string, - populateShortcuts = true, - ): Promise { - const resolvedPath = this.resolvePath(dirPath); - const dirEntries = await fs.readdir(resolvedPath, { - withFileTypes: true, - }); - const size = await this.calculateFolderSize(dirPath); - const fileCount = await this.countFiles(dirPath); - const folderCount = await this.countFolders(dirPath); - const shortcuts = populateShortcuts - ? await this.bulk(".arclnk", dirPath) - : {}; - const directoryReadReturn: DirectoryReadReturn = { - dirs: [], - files: [], - totalSize: size, - totalFiles: fileCount, - totalFolders: folderCount, - shortcuts, - }; - - for (const entry of dirEntries) { - const entryPath = path.join(resolvedPath, entry.name); - const stats = statSync(entryPath); - - if (entry.isDirectory()) { - const folderEntry: FolderEntry = { - itemId: "", - name: entry.name, - dateCreated: stats.birthtime, - dateModified: stats.mtime, - }; - directoryReadReturn.dirs.push(folderEntry); - } else if (entry.isFile()) { - const fileEntry: FileEntry = { - itemId: "", - name: entry.name, - size: stats.size, - mimeType: - mime.lookup(entryPath) || "application/octet-stream", - dateCreated: stats.birthtime, - dateModified: stats.mtime, - }; - directoryReadReturn.files.push(fileEntry); - } + if (entry.isDirectory()) { + count++; + count += await calculate(entryPath); } - - return directoryReadReturn; - } - - public async getDirectoryTree( - dirPath?: string, - ): Promise { - const resolvedPath = this.resolvePath(dirPath); - - const getTree = async ( - currentPath: string, - ): Promise => { - const dirEntries = await fs.readdir(currentPath, { - withFileTypes: true, - }); - const shortcuts = (await this.bulk(".arclnk", currentPath)) || {}; - - const dirs: (FolderEntry & { - children: RecursiveDirectoryReadReturn; - })[] = []; - const files: FileEntry[] = []; - - for (const entry of dirEntries) { - const entryPath = path.join(currentPath, entry.name); - const stats = statSync(entryPath); - - if (entry.isDirectory()) { - const subTree = await getTree(entryPath); - const folderEntry: FolderEntry & { - children: RecursiveDirectoryReadReturn; - } = { - itemId: "", - name: entry.name, - dateCreated: stats.birthtime, - dateModified: stats.mtime, - children: subTree, - }; - dirs.push(folderEntry); - } else if (entry.isFile()) { - const fileEntry: FileEntry = { - itemId: "", - name: entry.name, - size: stats.size, - mimeType: - mime.lookup(entryPath) || - "application/octet-stream", - dateCreated: stats.birthtime, - dateModified: stats.mtime, - }; - files.push(fileEntry); - } - } - - return { dirs, files, shortcuts }; + } + + return count; + }; + + return await calculate(resolvedPath); + } + + public async writeFile(filePath: string, content: string | Buffer) { + const resolvedPath = this.resolvePath(filePath); + + await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); + await fs.writeFile(resolvedPath, content); + } + + public async readFile(filePath: string): Promise { + const resolvedPath = this.resolvePath(filePath); + + return fs.readFile(resolvedPath); + } + + public async pipeFile(filePath: string) { + const resolvedPath = this.resolvePath(filePath); + + return createReadStream(resolvedPath); + } + + public async createDirectory(dirPath: string): Promise { + const resolvedPath = await this.resolvePath(dirPath); + + await fs.mkdir(resolvedPath, { recursive: true }); + } + + public async readDirectory( + dirPath?: string, + populateShortcuts = true, + ): Promise { + const resolvedPath = this.resolvePath(dirPath); + const dirEntries = await fs.readdir(resolvedPath, { + withFileTypes: true, + }); + const size = await this.calculateFolderSize(dirPath); + const fileCount = await this.countFiles(dirPath); + const folderCount = await this.countFolders(dirPath); + const shortcuts = populateShortcuts + ? await this.bulk(".arclnk", dirPath) + : {}; + const directoryReadReturn: DirectoryReadReturn = { + dirs: [], + files: [], + totalSize: size, + totalFiles: fileCount, + totalFolders: folderCount, + shortcuts, + }; + + for (const entry of dirEntries) { + const entryPath = path.join(resolvedPath, entry.name); + const stats = statSync(entryPath); + + if (entry.isDirectory()) { + const folderEntry: FolderEntry = { + itemId: "", + name: entry.name, + dateCreated: stats.birthtime, + dateModified: stats.mtime, }; - - return getTree(resolvedPath); + directoryReadReturn.dirs.push(folderEntry); + } else if (entry.isFile()) { + const fileEntry: FileEntry = { + itemId: "", + name: entry.name, + size: stats.size, + mimeType: mime.lookup(entryPath) || "application/octet-stream", + dateCreated: stats.birthtime, + dateModified: stats.mtime, + }; + directoryReadReturn.files.push(fileEntry); + } } - public async moveItem(sourcePath: string, destPath: string): Promise { - const resolvedSource = this.resolvePath(sourcePath); - const resolvedDest = this.resolvePath(destPath); + return directoryReadReturn; + } + + public async getDirectoryTree( + dirPath?: string, + ): Promise { + const resolvedPath = this.resolvePath(dirPath); + + const getTree = async ( + currentPath: string, + ): Promise => { + const dirEntries = await fs.readdir(currentPath, { + withFileTypes: true, + }); + const shortcuts = (await this.bulk(".arclnk", currentPath)) || {}; + + const dirs: (FolderEntry & { + children: RecursiveDirectoryReadReturn; + })[] = []; + const files: FileEntry[] = []; + + for (const entry of dirEntries) { + const entryPath = path.join(currentPath, entry.name); + const stats = statSync(entryPath); + + if (entry.isDirectory()) { + const subTree = await getTree(entryPath); + const folderEntry: FolderEntry & { + children: RecursiveDirectoryReadReturn; + } = { + itemId: "", + name: entry.name, + dateCreated: stats.birthtime, + dateModified: stats.mtime, + children: subTree, + }; + dirs.push(folderEntry); + } else if (entry.isFile()) { + const fileEntry: FileEntry = { + itemId: "", + name: entry.name, + size: stats.size, + mimeType: mime.lookup(entryPath) || "application/octet-stream", + dateCreated: stats.birthtime, + dateModified: stats.mtime, + }; + files.push(fileEntry); + } + } - await fs.rename(resolvedSource, resolvedDest); - } + return { dirs, files, shortcuts }; + }; - public async copyItem(sourcePath: string, destPath: string): Promise { - const resolvedSource = this.resolvePath(sourcePath); - const resolvedDest = this.resolvePath(destPath); + return getTree(resolvedPath); + } - await fs.cp(resolvedSource, resolvedDest, { recursive: true }); - } + public async moveItem(sourcePath: string, destPath: string): Promise { + const resolvedSource = this.resolvePath(sourcePath); + const resolvedDest = this.resolvePath(destPath); - public async deleteItem(itemPath: string): Promise { - const resolvedPath = this.resolvePath(itemPath); + await fs.rename(resolvedSource, resolvedDest); + } - await fs.rm(resolvedPath, { recursive: true }); - } + public async copyItem(sourcePath: string, destPath: string): Promise { + const resolvedSource = this.resolvePath(sourcePath); + const resolvedDest = this.resolvePath(destPath); - public async exists(itemPath: string): Promise { - try { - const resolvedPath = await this.resolvePath(itemPath); + await fs.cp(resolvedSource, resolvedDest, { recursive: true }); + } - return existsSync(resolvedPath); - } catch { - return false; - } - } + public async deleteItem(itemPath: string): Promise { + const resolvedPath = this.resolvePath(itemPath); - public async quota(): Promise { - const path = platform() === "win32" ? "c:" : "/"; - const usage = await checkDiskSpace(path); + await fs.rm(resolvedPath, { recursive: true }); + } - return { - used: usage.size - usage.free, - free: usage.free, - max: usage.size, - percentage: (100 / usage.size) * (usage.size - usage.free), - }; - } + public async exists(itemPath: string): Promise { + try { + const resolvedPath = await this.resolvePath(itemPath); - public async stat(filePath: string) { - const resolvedPath = this.resolvePath(filePath); - try { - return statSync(resolvedPath); - } catch { - return undefined; - } + return existsSync(resolvedPath); + } catch { + return false; } - - public async createReadStream( - filePath: string, - start?: number, - end?: number, - ) { - const resolvedPath = this.resolvePath(filePath); - - return createReadStream(resolvedPath, { start, end }); + } + + public async quota(): Promise { + const path = platform() === "win32" ? "c:" : "/"; + const usage = await checkDiskSpace(path); + + return { + used: usage.size - usage.free, + free: usage.free, + max: usage.size, + percentage: (100 / usage.size) * (usage.size - usage.free), + }; + } + + public async stat(filePath: string) { + const resolvedPath = this.resolvePath(filePath); + try { + return statSync(resolvedPath); + } catch { + return undefined; } + } - async bulk(extension: string, path: string = "") { - const directory = await this.readDirectory(path, false); - const result: Record = {}; + public async createReadStream( + filePath: string, + start?: number, + end?: number, + ) { + const resolvedPath = this.resolvePath(filePath); - for (const file of directory.files) { - if (!file.name.endsWith(extension)) continue; + return createReadStream(resolvedPath, { start, end }); + } - const contents = await this.readFile(join(path, file.name)); + async bulk(extension: string, path: string = "") { + const directory = await this.readDirectory(path, false); + const result: Record = {}; - result[file.name] = tryJsonParse(contents.toString()) || contents; + for (const file of directory.files) { + if (!file.name.endsWith(extension)) continue; - if (JSON.stringify(result).length > 1e5) return result; - } + const contents = await this.readFile(join(path, file.name)); + + result[file.name] = tryJsonParse(contents.toString()) || contents; - return result; + if (JSON.stringify(result).length > 1e5) return result; } + + return result; + } } diff --git a/src/server/websocket/index.ts b/src/server/websocket/index.ts index f60af88..a60b4fc 100644 --- a/src/server/websocket/index.ts +++ b/src/server/websocket/index.ts @@ -1,8 +1,8 @@ import type { Server as HttpServer } from "http"; -import { Server, Socket } from "socket.io"; -import { ProjectMetadata } from "../../types/project"; import { Signale } from "signale"; +import { Server, Socket } from "socket.io"; import { LogItem, LogLevel } from "../../types/logging"; +import { ProjectMetadata } from "../../types/project"; export const SockLog = new Signale({ scope: "SIO", @@ -31,7 +31,7 @@ export class WebSock { onConnection(sock: Socket) { if (this.client) { SockLog.warn( - `Only one client is allowed at a time. Disconnecting ${sock.id.blue}` + `Only one client is allowed at a time. Disconnecting ${sock.id.blue}`, ); this.client.sock.disconnect(); } diff --git a/src/tools/build-ts-tpa.ts b/src/tools/build-ts-tpa.ts index d58779b..7f44e70 100644 --- a/src/tools/build-ts-tpa.ts +++ b/src/tools/build-ts-tpa.ts @@ -1,12 +1,12 @@ +import { spawn, type SpawnOptionsWithoutStdio } from "child_process"; +import { once } from "events"; import fs from "fs"; import path, { resolve } from "path"; import process from "process"; -import { spawn, type SpawnOptionsWithoutStdio } from "child_process"; -import { once } from "events"; // this shit is annoying to deal with const exportRegex = - /export\b\s*(?:(?:default\s*)?{\s*((?:[^,{}]+,?)+\b)\s*}|(?:default\s*)?([^,;\s]+));?/m; + /export\b\s*(?:(?:default\s*)?{\s*((?:[^,{}]+,?)+\b)\s*}|(?:default\s*)?([^,;\s]+));?/m; const resourceFileRegex = /(?!\w+\.[em]?ts$|\w+\.[em]?js$)(^\w+\.?\w*)/m; const scriptFileRegex = /(\w+\.[me]?ts$|\w+\.[me]?js$)/m; @@ -18,418 +18,417 @@ let silentMode = false; let printDebugMsgs = false; async function sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); } function printDebug(message?: any, ...optionalParams: any[]) { - if (printDebugMsgs && !silentMode) - console.debug(message, ...optionalParams); + if (printDebugMsgs && !silentMode) console.debug(message, ...optionalParams); } function conditionalFSRemove(source: fs.PathLike, cwd?: fs.PathLike) { - if (fs.existsSync(source)) { - const fileStat = fs.statSync(source); - - if (!silentMode) { - console.log( - `Removing './${ - cwd - ? path.relative(cwd.toString(), source.toString()) - : path.basename(source.toString()) - }'..`, - ); - } - fs.rmSync(source, fileStat.isDirectory() ? { recursive: true } : {}); + if (fs.existsSync(source)) { + const fileStat = fs.statSync(source); + + if (!silentMode) { + console.log( + `Removing './${ + cwd + ? path.relative(cwd.toString(), source.toString()) + : path.basename(source.toString()) + }'..`, + ); } + fs.rmSync(source, fileStat.isDirectory() ? { recursive: true } : {}); + } } function FSCopy( - source: fs.PathLike, - destination: fs.PathLike, - cwd?: fs.PathLike, + source: fs.PathLike, + destination: fs.PathLike, + cwd?: fs.PathLike, ) { - if (fs.existsSync(source)) { - const sourceFileStat = fs.statSync(source); - - // it's ugly i know 😭 - if (!silentMode) { - console.log( - "Copying", - `'./${ - cwd - ? path.relative(cwd.toString(), source.toString()) - : path.basename(source.toString()) - }'`, - "->", - `'./${ - cwd - ? path.relative(cwd.toString(), destination.toString()) - : path.basename(destination.toString()) - }'..`, - ); - } - fs.cpSync( - source.toString(), - destination.toString(), - sourceFileStat.isDirectory() ? { recursive: true } : {}, - ); + if (fs.existsSync(source)) { + const sourceFileStat = fs.statSync(source); + + // it's ugly i know 😭 + if (!silentMode) { + console.log( + "Copying", + `'./${ + cwd + ? path.relative(cwd.toString(), source.toString()) + : path.basename(source.toString()) + }'`, + "->", + `'./${ + cwd + ? path.relative(cwd.toString(), destination.toString()) + : path.basename(destination.toString()) + }'..`, + ); } + fs.cpSync( + source.toString(), + destination.toString(), + sourceFileStat.isDirectory() ? { recursive: true } : {}, + ); + } } function FSWriteFile( - destination: fs.PathLike, - data: string | NodeJS.ArrayBufferView, - cwd?: fs.PathLike, + destination: fs.PathLike, + data: string | NodeJS.ArrayBufferView, + cwd?: fs.PathLike, ) { - if (!silentMode) { - console.log( - "Writing data to ", - `'./${ - cwd - ? path.relative(cwd.toString(), destination.toString()) - : path.basename(destination.toString()) - }'..`, - ); - } + if (!silentMode) { + console.log( + "Writing data to ", + `'./${ + cwd + ? path.relative(cwd.toString(), destination.toString()) + : path.basename(destination.toString()) + }'..`, + ); + } - fs.writeFileSync(destination, data); + fs.writeFileSync(destination, data); } async function runCommand( - command: string, - args: string[] | undefined, - onError: (err: Error) => void, - commandOpts?: SpawnOptionsWithoutStdio, - afterRun?: () => void, + command: string, + args: string[] | undefined, + onError: (err: Error) => void, + commandOpts?: SpawnOptionsWithoutStdio, + afterRun?: () => void, ) { - const cmd = spawn(command, args, commandOpts); + const cmd = spawn(command, args, commandOpts); - cmd.stdout.on("data", (data) => { - process.stdout.write(data); - }); + cmd.stdout.on("data", (data) => { + process.stdout.write(data); + }); - cmd.stderr.on("data", (data) => { - process.stderr.write(data); - }); + cmd.stderr.on("data", (data) => { + process.stderr.write(data); + }); - cmd.on("error", onError); + cmd.on("error", onError); - afterRun?.(); + afterRun?.(); - const [code] = await once(cmd, "close"); + const [code] = await once(cmd, "close"); - return code; + return code; } function removeDeletedFiles( - srcRoot: fs.PathLike, - distRoot: fs.PathLike, - cwd: fs.PathLike, + srcRoot: fs.PathLike, + distRoot: fs.PathLike, + cwd: fs.PathLike, ) { - printDebug("Checking for deleted files to remove in dist..\n-----"); - const srcFiles = fs - .readdirSync(srcRoot, { - recursive: true, - }) - .map((val) => { - const srcPath = path.parse(val.toString()); - - const tsFileTestResult = tsFileRegex.test(srcPath.base); - printDebug(`tsFileTest '${srcPath.base}':`, tsFileTestResult); - if (tsFileTestResult) { - printDebug("basename:", srcPath.base); - printDebug("basename (removed ext):", srcPath.name); - printDebug("converted filename:", srcPath.name.concat(".js")); - return path.join(srcPath.dir, srcPath.name).concat(".js"); - } else { - return val; - } - }); - - const distFiles = fs.readdirSync(distRoot, { - recursive: true, + printDebug("Checking for deleted files to remove in dist..\n-----"); + const srcFiles = fs + .readdirSync(srcRoot, { + recursive: true, + }) + .map((val) => { + const srcPath = path.parse(val.toString()); + + const tsFileTestResult = tsFileRegex.test(srcPath.base); + printDebug(`tsFileTest '${srcPath.base}':`, tsFileTestResult); + if (tsFileTestResult) { + printDebug("basename:", srcPath.base); + printDebug("basename (removed ext):", srcPath.name); + printDebug("converted filename:", srcPath.name.concat(".js")); + return path.join(srcPath.dir, srcPath.name).concat(".js"); + } else { + return val; + } }); - printDebug("srcFiles:", srcFiles); - printDebug("distFiles:", distFiles); + const distFiles = fs.readdirSync(distRoot, { + recursive: true, + }); - distFiles.forEach((val) => { - if (!srcFiles.includes(val)) { - conditionalFSRemove( - path.resolve(distRoot.toString(), val.toString()), - cwd, - ); - } - }); - printDebug("-----\n"); + printDebug("srcFiles:", srcFiles); + printDebug("distFiles:", distFiles); + + distFiles.forEach((val) => { + if (!srcFiles.includes(val)) { + conditionalFSRemove( + path.resolve(distRoot.toString(), val.toString()), + cwd, + ); + } + }); + printDebug("-----\n"); } function copyNewFiles(srcRoot: string, distRoot: string, cwd: fs.PathLike) { - printDebug("Checking for files to copy to dist..\n-----"); - const srcFiles = fs - .readdirSync(srcRoot, { - recursive: true, - withFileTypes: true, - }) - .map((val) => { - const srcPath = path.parse( - path.relative(srcRoot, path.join(val.parentPath, val.name)), - ); - const resFileTestResult = resourceFileRegex.test(srcPath.base); - - if (resFileTestResult && !val.isDirectory()) { - printDebug( - `Value '${path.join(srcPath.dir, srcPath.base)}' is a resource file`, - ); - return path.join(srcPath.dir, srcPath.base); - } - }) - .filter((val) => { - return val !== undefined; - }); - - const distFiles = fs - .readdirSync(distRoot, { - recursive: true, - withFileTypes: true, - }) - .map((val) => { - const srcPath = path.parse( - path.relative(distRoot, path.join(val.parentPath, val.name)), - ); - const resFileTestResult = resourceFileRegex.test(srcPath.base); - - if (resFileTestResult && !val.isDirectory()) { - printDebug( - `Value '${path.join(srcPath.dir, srcPath.base)}' is a resource file`, - ); - return path.join(srcPath.dir, srcPath.base); - } - }) - .filter((val) => { - return val !== undefined; - }); - - printDebug("srcFiles:", srcFiles); - printDebug("distFiles:", distFiles); - - srcFiles.forEach((val) => { - const srcFilePath = path.resolve(srcRoot, val.toString()); - const distFilePath = path.resolve(distRoot, val.toString()); - - if (!distFiles.includes(val)) { - printDebug(`'${val}' was missing from dist. Copying over.`); - FSCopy(srcFilePath, distFilePath, cwd); - } else { - printDebug(`'${val}' is present in dist. Updating the file..`); - const srcFileContents = fs.readFileSync(srcFilePath); - const distFileContents = fs.readFileSync(distFilePath); - - if (srcFileContents.compare(distFileContents) !== 0) { - FSWriteFile(distFilePath, srcFileContents, cwd); - } - } + printDebug("Checking for files to copy to dist..\n-----"); + const srcFiles = fs + .readdirSync(srcRoot, { + recursive: true, + withFileTypes: true, + }) + .map((val) => { + const srcPath = path.parse( + path.relative(srcRoot, path.join(val.parentPath, val.name)), + ); + const resFileTestResult = resourceFileRegex.test(srcPath.base); + + if (resFileTestResult && !val.isDirectory()) { + printDebug( + `Value '${path.join(srcPath.dir, srcPath.base)}' is a resource file`, + ); + return path.join(srcPath.dir, srcPath.base); + } + }) + .filter((val) => { + return val !== undefined; + }); + + const distFiles = fs + .readdirSync(distRoot, { + recursive: true, + withFileTypes: true, + }) + .map((val) => { + const srcPath = path.parse( + path.relative(distRoot, path.join(val.parentPath, val.name)), + ); + const resFileTestResult = resourceFileRegex.test(srcPath.base); + + if (resFileTestResult && !val.isDirectory()) { + printDebug( + `Value '${path.join(srcPath.dir, srcPath.base)}' is a resource file`, + ); + return path.join(srcPath.dir, srcPath.base); + } + }) + .filter((val) => { + return val !== undefined; }); - printDebug("-----\n"); + + printDebug("srcFiles:", srcFiles); + printDebug("distFiles:", distFiles); + + srcFiles.forEach((val) => { + const srcFilePath = path.resolve(srcRoot, val.toString()); + const distFilePath = path.resolve(distRoot, val.toString()); + + if (!distFiles.includes(val)) { + printDebug(`'${val}' was missing from dist. Copying over.`); + FSCopy(srcFilePath, distFilePath, cwd); + } else { + printDebug(`'${val}' is present in dist. Updating the file..`); + const srcFileContents = fs.readFileSync(srcFilePath); + const distFileContents = fs.readFileSync(distFilePath); + + if (srcFileContents.compare(distFileContents) !== 0) { + FSWriteFile(distFilePath, srcFileContents, cwd); + } + } + }); + printDebug("-----\n"); } export function containsTypescript(searchPath: fs.PathLike) { - printDebug("Checking if folder contains TypeScript files."); - const fileList = fs.readdirSync(searchPath, { recursive: true }); - - for (const val of fileList) { - const srcPath = path.parse(val.toString()); - const result = tsFileRegex.test(srcPath.base); - printDebug(`tsFileTest '${srcPath.base}':`, result); - if (result) { - printDebug("The given folder does contain TypeScript files."); - return true; - } + printDebug("Checking if folder contains TypeScript files."); + const fileList = fs.readdirSync(searchPath, { recursive: true }); + + for (const val of fileList) { + const srcPath = path.parse(val.toString()); + const result = tsFileRegex.test(srcPath.base); + printDebug(`tsFileTest '${srcPath.base}':`, result); + if (result) { + printDebug("The given folder does contain TypeScript files."); + return true; } + } - printDebug("The given folder does not contain any TypeScript files."); - return false; + printDebug("The given folder does not contain any TypeScript files."); + return false; } function replaceExport(contents: string) { - return contents.replace(exportRegex, (subStr: string, ...args: any[]) => { - // printDebug(`export replace subStr: '${subStr}'\nargs:`, args, "\n"); + return contents.replace(exportRegex, (subStr: string, ...args: any[]) => { + // printDebug(`export replace subStr: '${subStr}'\nargs:`, args, "\n"); - const hasCurlyBrackets = subStr.includes("{"); + const hasCurlyBrackets = subStr.includes("{"); - return `return ${hasCurlyBrackets ? "{ " : ""}${args[0] ?? args[1]}${hasCurlyBrackets ? " }" : ""};`; - }); + return `return ${hasCurlyBrackets ? "{ " : ""}${args[0] ?? args[1]}${hasCurlyBrackets ? " }" : ""};`; + }); } function fixExports(distRoot: string) { - const jsFiles = fs.readdirSync(distRoot, { - recursive: true, - }); - const sortedJsFiles = jsFiles.filter((val) => { - return val.toString().match(scriptFileRegex)?.[0]; - }); + const jsFiles = fs.readdirSync(distRoot, { + recursive: true, + }); + const sortedJsFiles = jsFiles.filter((val) => { + return val.toString().match(scriptFileRegex)?.[0]; + }); - sortedJsFiles.forEach((val) => { - const jsPath = resolve(distRoot, val.toString()); + sortedJsFiles.forEach((val) => { + const jsPath = resolve(distRoot, val.toString()); - printDebug("jsPath:", jsPath); + printDebug("jsPath:", jsPath); - try { - const data = fs.readFileSync(jsPath); + try { + const data = fs.readFileSync(jsPath); - let contents = data.toString(); + let contents = data.toString(); - while (exportRegex.test(contents)) { - contents = replaceExport(contents); + while (exportRegex.test(contents)) { + contents = replaceExport(contents); - fs.writeFileSync(jsPath, contents); - } - } catch (err) { - throw err; - } - }); + fs.writeFileSync(jsPath, contents); + } + } catch (err) { + throw err; + } + }); } async function compileAndCopySrc( - distRoot: string, - tmpRoot: string, - tmpSrc: string, - cwd: fs.PathLike, + distRoot: string, + tmpRoot: string, + tmpSrc: string, + cwd: fs.PathLike, ) { - printDebug("Attempting to compile source..\n-----"); - - const pathToTsc = require.resolve("typescript/bin/tsc"); + printDebug("Attempting to compile source..\n-----"); - if (!fs.existsSync(resolve(cwd.toString(), "tsconfig.json"))) { - console.error( - "The project directory must have a properly configured 'tsconfig.json' file.", - ); - process.exit(-1); - } + const pathToTsc = require.resolve("typescript/bin/tsc"); - await runCommand( - "node", - [ - pathToTsc, - "--rootDir", - cwd.toString(), - "--outDir", - resolve(cwd.toString(), "tmp"), - ], - (err) => { - throw err; - }, + if (!fs.existsSync(resolve(cwd.toString(), "tsconfig.json"))) { + console.error( + "The project directory must have a properly configured 'tsconfig.json' file.", ); + process.exit(-1); + } + + await runCommand( + "node", + [ + pathToTsc, + "--rootDir", + cwd.toString(), + "--outDir", + resolve(cwd.toString(), "tmp"), + ], + (err) => { + throw err; + }, + ); + + const distFiles = fs + .readdirSync(distRoot, { + recursive: true, + withFileTypes: true, + }) + .map((val) => { + const srcPath = path.parse( + path.relative(distRoot, path.join(val.parentPath, val.name)), + ); + const scriptFileTestResult = scriptFileRegex.test(srcPath.base); + + printDebug( + `scriptFileTest '${path.join(srcPath.dir, srcPath.base)}':`, + scriptFileTestResult, + ); + + if (scriptFileTestResult && !val.isDirectory()) { + printDebug( + `Value '${path.join(srcPath.dir, srcPath.base)}' is a script file`, + ); + return path.join(srcPath.dir, srcPath.base); + } + }) + .filter((val) => { + return val !== undefined; + }); + // .filter((val) => { + // return val.toString().match(scriptFileRegex)?.[0]; + // }); + + const tmpSrcFiles = fs + .readdirSync(tmpSrc, { + recursive: true, + withFileTypes: true, + }) + .map((val) => { + const srcPath = path.parse( + path.relative(tmpSrc, path.join(val.parentPath, val.name)), + ); + if (!val.isDirectory()) { + return path.join(srcPath.dir, srcPath.base); + } + }) + .filter((val) => { + return val !== undefined; + }); - const distFiles = fs - .readdirSync(distRoot, { - recursive: true, - withFileTypes: true, - }) - .map((val) => { - const srcPath = path.parse( - path.relative(distRoot, path.join(val.parentPath, val.name)), - ); - const scriptFileTestResult = scriptFileRegex.test(srcPath.base); - - printDebug( - `scriptFileTest '${path.join(srcPath.dir, srcPath.base)}':`, - scriptFileTestResult, - ); - - if (scriptFileTestResult && !val.isDirectory()) { - printDebug( - `Value '${path.join(srcPath.dir, srcPath.base)}' is a script file`, - ); - return path.join(srcPath.dir, srcPath.base); - } - }) - .filter((val) => { - return val !== undefined; - }); - // .filter((val) => { - // return val.toString().match(scriptFileRegex)?.[0]; - // }); - - const tmpSrcFiles = fs - .readdirSync(tmpSrc, { - recursive: true, - withFileTypes: true, - }) - .map((val) => { - const srcPath = path.parse( - path.relative(tmpSrc, path.join(val.parentPath, val.name)), - ); - if (!val.isDirectory()) { - return path.join(srcPath.dir, srcPath.base); - } - }) - .filter((val) => { - return val !== undefined; - }); - - printDebug("distFiles:", distFiles); - printDebug("tmpSrcFiles:", tmpSrcFiles); - - for (const val of tmpSrcFiles) { - const tmpSrcFilePath = path.resolve(tmpSrc, val.toString()); - const distFilePath = path.resolve(distRoot, val.toString()); - - // await sleep(1000); - if (!fs.existsSync(distFilePath)) { - FSCopy(tmpSrcFilePath, distFilePath, cwd); - conditionalFSRemove(tmpSrcFilePath, cwd); - } else { - const tmpSrcFileContents = fs.readFileSync(tmpSrcFilePath); - const distFileContents = fs.readFileSync(distFilePath); - - let tmpSrcContents = tmpSrcFileContents.toString(); - while (exportRegex.test(tmpSrcContents)) { - tmpSrcContents = replaceExport(tmpSrcContents); - } - - if (tmpSrcContents !== distFileContents.toString()) { - FSCopy(tmpSrcFilePath, distFilePath, cwd); - conditionalFSRemove(tmpSrcFilePath, cwd); - } - } + printDebug("distFiles:", distFiles); + printDebug("tmpSrcFiles:", tmpSrcFiles); + + for (const val of tmpSrcFiles) { + const tmpSrcFilePath = path.resolve(tmpSrc, val.toString()); + const distFilePath = path.resolve(distRoot, val.toString()); + + // await sleep(1000); + if (!fs.existsSync(distFilePath)) { + FSCopy(tmpSrcFilePath, distFilePath, cwd); + conditionalFSRemove(tmpSrcFilePath, cwd); + } else { + const tmpSrcFileContents = fs.readFileSync(tmpSrcFilePath); + const distFileContents = fs.readFileSync(distFilePath); + + let tmpSrcContents = tmpSrcFileContents.toString(); + while (exportRegex.test(tmpSrcContents)) { + tmpSrcContents = replaceExport(tmpSrcContents); + } + + if (tmpSrcContents !== distFileContents.toString()) { + FSCopy(tmpSrcFilePath, distFilePath, cwd); + conditionalFSRemove(tmpSrcFilePath, cwd); + } } + } - conditionalFSRemove(tmpRoot, cwd); + conditionalFSRemove(tmpRoot, cwd); - fixExports(distRoot); + fixExports(distRoot); - printDebug("-----\n"); + printDebug("-----\n"); } export async function buildTSTPA( - cwd: fs.PathLike, - silent: boolean = false, - debugOutput: boolean = false, + cwd: fs.PathLike, + silent: boolean = false, + debugOutput: boolean = false, ) { - silentMode = silent; - printDebugMsgs = debugOutput; - - const srcRoot = resolve(cwd.toString(), "src"); - const distRoot = resolve(cwd.toString(), "dist"); - const tmpRoot = resolve(cwd.toString(), "tmp"); - const tmpSrc = resolve(tmpRoot, "src"); - - if (!fs.existsSync(distRoot)) { - fs.mkdirSync(distRoot, { - recursive: true, - }); - } + silentMode = silent; + printDebugMsgs = debugOutput; + + const srcRoot = resolve(cwd.toString(), "src"); + const distRoot = resolve(cwd.toString(), "dist"); + const tmpRoot = resolve(cwd.toString(), "tmp"); + const tmpSrc = resolve(tmpRoot, "src"); + + if (!fs.existsSync(distRoot)) { + fs.mkdirSync(distRoot, { + recursive: true, + }); + } - printDebug(); + printDebug(); - removeDeletedFiles(srcRoot, distRoot, cwd); + removeDeletedFiles(srcRoot, distRoot, cwd); - copyNewFiles(srcRoot, distRoot, cwd); + copyNewFiles(srcRoot, distRoot, cwd); - await compileAndCopySrc(distRoot, tmpRoot, tmpSrc, cwd); + await compileAndCopySrc(distRoot, tmpRoot, tmpSrc, cwd); } export default buildTSTPA; diff --git a/src/tpa/index.ts b/src/tpa/index.ts index 25778dd..efb1c5b 100644 --- a/src/tpa/index.ts +++ b/src/tpa/index.ts @@ -1,9 +1,9 @@ +import { outro, spinner } from "@clack/prompts"; +import axios from "axios"; import { writeFile } from "fs/promises"; +import { join } from "path"; import { Project } from "../project"; import { ScriptedApp } from "../types/app"; -import { join } from "path"; -import { outro, spinner } from "@clack/prompts"; -import axios from "axios"; export async function scaffoldProject(app: ScriptedApp, project: Project) { const spin = spinner(); diff --git a/src/tpa/wizard.ts b/src/tpa/wizard.ts index 14b5c59..4aff6df 100644 --- a/src/tpa/wizard.ts +++ b/src/tpa/wizard.ts @@ -1,7 +1,6 @@ import { intro, isCancel, multiselect, text } from "@clack/prompts"; import { abort } from "../commands/new"; import { ScriptedApp } from "../types/app"; -import { Project } from "../project"; import { PackageMetadata } from "../types/package"; export async function TpaWizard(pkg: PackageMetadata) { diff --git a/src/types/project.ts b/src/types/project.ts index 7849014..a200cfa 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -1,7 +1,7 @@ -import { PackageMetadata } from "./package"; import type { Request, Response } from "express"; -import { Method } from "./api"; import { Project } from "../project"; +import { Method } from "./api"; +import { PackageMetadata } from "./package"; export interface ProjectMetadata { metadata: PackageMetadata; @@ -21,7 +21,7 @@ export type RouteCallback = ( req: Request, res: Response, stop: (c?: number, json?: object) => string, - project: Project + project: Project, ) => void; export interface RouteType { diff --git a/tsconfig.json b/tsconfig.json index dac4d9d..554a80e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,13 @@ { - "include": ["src/**/*"], - "compilerOptions": { - "target": "ES2020", - "module": "CommonJS", - "lib": ["ES2020"], - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "resolveJsonModule": true - } + "include": ["src/**/*"], + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true + } } From f54075bad47bc5837c1e73517c4317ab9366716d Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:04:59 -0500 Subject: [PATCH 08/14] forgot to disable the OTHER debug text.. (+ comment cleanup) --- src/server/api/index.ts | 2 +- src/tools/build-ts-tpa.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 108f344..45ae811 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -61,7 +61,7 @@ export async function StartServer(project: Project) { }`, ); if (containsTS) { - await buildTSTPA(project.path, false, true); + await buildTSTPA(project.path); } project.websock?.client?.sock.emit("restart-tpa"); } diff --git a/src/tools/build-ts-tpa.ts b/src/tools/build-ts-tpa.ts index 7f44e70..3f46ecb 100644 --- a/src/tools/build-ts-tpa.ts +++ b/src/tools/build-ts-tpa.ts @@ -254,7 +254,7 @@ export function containsTypescript(searchPath: fs.PathLike) { function replaceExport(contents: string) { return contents.replace(exportRegex, (subStr: string, ...args: any[]) => { - // printDebug(`export replace subStr: '${subStr}'\nargs:`, args, "\n"); + printDebug(`export replace subStr: '${subStr}'\nargs:`, args, "\n"); const hasCurlyBrackets = subStr.includes("{"); @@ -348,9 +348,6 @@ async function compileAndCopySrc( .filter((val) => { return val !== undefined; }); - // .filter((val) => { - // return val.toString().match(scriptFileRegex)?.[0]; - // }); const tmpSrcFiles = fs .readdirSync(tmpSrc, { @@ -376,7 +373,6 @@ async function compileAndCopySrc( const tmpSrcFilePath = path.resolve(tmpSrc, val.toString()); const distFilePath = path.resolve(distRoot, val.toString()); - // await sleep(1000); if (!fs.existsSync(distFilePath)) { FSCopy(tmpSrcFilePath, distFilePath, cwd); conditionalFSRemove(tmpSrcFilePath, cwd); From 222332f1fb53a5bb13c932f6c1401bb4267d141c Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:21:43 -0500 Subject: [PATCH 09/14] dumb fix --- src/commands/dev/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/commands/dev/index.ts b/src/commands/dev/index.ts index cdd8ce6..efcaafa 100644 --- a/src/commands/dev/index.ts +++ b/src/commands/dev/index.ts @@ -30,10 +30,7 @@ export default async function DevCommand() { const buildHash = await getArcBuild(); - if ( - project.metadata?.buildHash == null || - project.metadata.buildHash != buildHash - ) { + if (project.metadata?.buildHash == null) { project.metadata!!.buildHash = buildHash; } From e9a21a879fd00e621c18fd3cd4f062422abd0766 Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:04:24 -0500 Subject: [PATCH 10/14] reformatting and fix folders in dist staying --- src/commands/build/index.ts | 19 +-- src/commands/dev/index.ts | 8 +- src/commands/new/index.ts | 19 +-- src/commands/update.ts | 2 +- src/index.ts | 20 +-- src/json.ts | 20 +-- src/project/index.ts | 64 ++------ src/server/api/index.ts | 38 ++--- src/server/api/routes/fs/cp.ts | 5 +- src/server/api/routes/fs/direct/get.ts | 6 +- src/server/api/routes/fs/mv.ts | 5 +- src/server/api/routes/fs/tree/get.ts | 4 +- src/server/fs/index.ts | 38 +---- src/server/websocket/index.ts | 10 +- src/tools/build-ts-tpa.ts | 198 +++++++++---------------- src/tpa/wizard.ts | 6 +- src/types/api.ts | 10 +- src/types/build-ts-tpa.ts | 4 + src/types/project.ts | 7 +- 19 files changed, 146 insertions(+), 337 deletions(-) create mode 100644 src/types/build-ts-tpa.ts diff --git a/src/commands/build/index.ts b/src/commands/build/index.ts index c8689a0..b29879a 100644 --- a/src/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -15,16 +15,14 @@ export default async function BuildCommand() { if (!project.metadata) return; if (await project.areTypeDefsOutdated()) { - signale.warn( - "Type definitions are outdated. Please run `npx v7cli update` to update them.", - ); + signale.warn("Type definitions are outdated. Please run `npx v7cli update` to update them."); } const appId = project.metadata?.metadata.appId; if (appId?.includes(".") || appId?.includes("-")) { signale.error( - "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa", + "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa" ); process.exit(1); } @@ -39,13 +37,11 @@ export default async function BuildCommand() { // DO THE BUILD HERE const containsTS = containsTypescript(project.metadata.payloadDir); - const payloadDir = containsTS - ? join(project.path, "dist") - : join(project.path, project.metadata.payloadDir); + const payloadDir = containsTS ? join(project.path, "dist") : join(project.path, project.metadata.payloadDir); if (containsTS) { spin.message("Compiling project"); - await buildTSTPA(project.path, true); + await buildTSTPA(project.path, { silent: true }); } spin.message("Copying payload"); @@ -57,14 +53,11 @@ export default async function BuildCommand() { await writeFile( join(project.path, ".arcdev-build", "_metadata.json"), JSON.stringify(project.metadata.metadata, null, 2), - "utf-8", + "utf-8" ); spin.message("Bundling package"); - await zip( - join(project.path, ".arcdev-build"), - join(project.path, project.metadata.outFile), - ); + await zip(join(project.path, ".arcdev-build"), join(project.path, project.metadata.outFile)); spin.message("Removing temp directory"); await rm(join(project.path, ".arcdev-build"), { diff --git a/src/commands/dev/index.ts b/src/commands/dev/index.ts index efcaafa..a63884e 100644 --- a/src/commands/dev/index.ts +++ b/src/commands/dev/index.ts @@ -13,16 +13,14 @@ export default async function DevCommand() { await project.readProjectFile(); if (await project.areTypeDefsOutdated()) { - signale.warn( - "Type definitions are outdated. Please run `npx v7cli update` to update them.", - ); + signale.warn("Type definitions are outdated. Please run `npx v7cli update` to update them."); } const appId = project.metadata?.metadata.appId; if (appId?.includes(".") || appId?.includes("-")) { signale.error( - "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa", + "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa" ); process.exit(1); @@ -60,6 +58,6 @@ export default async function DevCommand() { `${arrow}${run} ${command} in ArcTerm to connect`, "", " READY.".green.bold, - ].join("\n"), + ].join("\n") ); } diff --git a/src/commands/new/index.ts b/src/commands/new/index.ts index 67fe59d..20a86d8 100644 --- a/src/commands/new/index.ts +++ b/src/commands/new/index.ts @@ -1,12 +1,4 @@ -import { - cancel, - intro, - isCancel, - outro, - select, - spinner, - text, -} from "@clack/prompts"; +import { cancel, intro, isCancel, outro, select, spinner, text } from "@clack/prompts"; import { Command } from "commander"; import { userInfo } from "os"; import { join } from "path"; @@ -35,8 +27,7 @@ export default async function NewCommand(this: Command, destination: string) { validate(value) { if (!value) return `A description is required`; - if (value.length > 512) - return `Too long! Pick a description under 512 characters.`; + if (value.length > 512) return `Too long! Pick a description under 512 characters.`; }, }); @@ -47,8 +38,7 @@ export default async function NewCommand(this: Command, destination: string) { initialValue: userInfo().username, validate(value) { if (!value) return `An author is required`; - if (value.length > 32) - return `Too long! Pick a name under 32 characters.`; + if (value.length > 32) return `Too long! Pick a name under 32 characters.`; }, }); @@ -58,8 +48,7 @@ export default async function NewCommand(this: Command, destination: string) { message: "What version is your app?", initialValue: "1.0.0", validate(value) { - if (value.length !== 5 || value[1] !== "." || value[3] !== ".") - return "Need a version in an x.x.x format"; + if (value.length !== 5 || value[1] !== "." || value[3] !== ".") return "Need a version in an x.x.x format"; }, }); diff --git a/src/commands/update.ts b/src/commands/update.ts index 4396706..2400eca 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -17,7 +17,7 @@ export default async function UpdateCommand() { const appId = project.metadata?.metadata.appId; if (appId?.includes(".") || appId?.includes("-")) { signale.error( - "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa", + "Package ID is invalid: it may not contain dashes or periods. Please change it to CamelCase with the format Author_AppId. Be sure to:\n\n- Update any references in your CSS\n- Change the ID accordingly in _app.tpa" ); process.exit(1); diff --git a/src/index.ts b/src/index.ts index fbacba9..77abb44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,10 +9,7 @@ import UpdateCommand from "./commands/update"; const program = new Command(); -program - .name(packageJson.name) - .description("Develop applications for ArcOS v7") - .version(packageJson.version); +program.name(packageJson.name).description("Develop applications for ArcOS v7").version(packageJson.version); program .command("new") @@ -20,19 +17,10 @@ program .argument("", "What folder to save the app in") .action(NewCommand); -program - .command("dev") - .description("Start the development server") - .action(DevCommand); +program.command("dev").description("Start the development server").action(DevCommand); -program - .command("build") - .description("Compile the app into an ArcOS package") - .action(BuildCommand); +program.command("build").description("Compile the app into an ArcOS package").action(BuildCommand); -program - .command("update") - .description("Update your project's type definitions") - .action(UpdateCommand); +program.command("update").description("Update your project's type definitions").action(UpdateCommand); program.parse(); diff --git a/src/json.ts b/src/json.ts index 712514b..1b2e7c1 100644 --- a/src/json.ts +++ b/src/json.ts @@ -15,17 +15,14 @@ export function keysToLowerCase(obj: any): any { acc[key.toLowerCase()] = keysToLowerCase(value); return acc; }, - {} as Record, + {} as Record ); } return obj; } export type ValidationObject = { [key: string]: any }; -export function validateObject( - target: ValidationObject, - validation: ValidationObject, -): boolean { +export function validateObject(target: ValidationObject, validation: ValidationObject): boolean { if (typeof validation !== "object" || validation === null) return false; for (const key in validation) { @@ -36,18 +33,9 @@ export function validateObject( if (typeof validationValue === "object" && validationValue !== null) { if (Array.isArray(validationValue)) { - if ( - !Array.isArray(targetValue) || - validationValue.length > targetValue.length - ) - return false; + if (!Array.isArray(targetValue) || validationValue.length > targetValue.length) return false; - if ( - !validationValue.every((val, index) => - validateObject(targetValue[index], val), - ) - ) - return false; + if (!validationValue.every((val, index) => validateObject(targetValue[index], val))) return false; } else { if (!validateObject(targetValue, validationValue)) return false; } diff --git a/src/project/index.ts b/src/project/index.ts index 0226e28..4eefa2c 100644 --- a/src/project/index.ts +++ b/src/project/index.ts @@ -20,34 +20,17 @@ export class Project { this.path = path; } - async initialize( - metadata: PackageMetadata, - outFile: string, - payloadDir: string, - repository?: string, - devPort?: number, - ) { + async initialize(metadata: PackageMetadata, outFile: string, payloadDir: string, repository?: string, devPort?: number) { if (existsSync(this.path) && (await readdir(this.path)).length) { - signale.error( - "Cannot initialize project: directory exists and is not empty", - ); + signale.error("Cannot initialize project: directory exists and is not empty"); process.exit(1); } await mkdir(this.path); - await this.createProjectFile( - metadata, - outFile, - payloadDir, - repository, - devPort, - ); + await this.createProjectFile(metadata, outFile, payloadDir, repository, devPort); await mkdir(join(this.path, payloadDir)); await mkdir(join(this.path, ".vscode")); - await writeFile( - join(this.path, ".gitignore"), - `${this.metadata!.metadata.appId}.arc\n.arcdev-build/`, - ); + await writeFile(join(this.path, ".gitignore"), `${this.metadata!.metadata.appId}.arc\n.arcdev-build/`); await writeFile( join(this.path, ".vscode/settings.json"), JSON.stringify( @@ -57,8 +40,8 @@ export class Project { }, }, null, - 2, - ), + 2 + ) ); await writeFile( @@ -79,9 +62,9 @@ export class Project { include: ["./src/**/*"], }, null, - 2, + 2 ), - "utf-8", + "utf-8" ); await this.writeTypeDefs(); @@ -93,13 +76,7 @@ export class Project { } } - async createProjectFile( - metadata: PackageMetadata, - outFile: string, - payloadDir: string, - repository?: string, - devPort?: number, - ) { + async createProjectFile(metadata: PackageMetadata, outFile: string, payloadDir: string, repository?: string, devPort?: number) { const meta: ProjectMetadata = { metadata, outFile, @@ -110,36 +87,21 @@ export class Project { noHotRelaunch: false, }; - await writeFile( - join(this.path, "project.arc.json"), - JSON.stringify(meta, null, 2), - "utf-8", - ); + await writeFile(join(this.path, "project.arc.json"), JSON.stringify(meta, null, 2), "utf-8"); await this.readProjectFile(); } async writeProjectFile() { - await writeFile( - join(this.path, "project.arc.json"), - JSON.stringify(this.metadata!, null, 2), - "utf-8", - ); + await writeFile(join(this.path, "project.arc.json"), JSON.stringify(this.metadata!, null, 2), "utf-8"); } async readProjectFile() { try { - const contents = await readFile( - join(this.path, "project.arc.json"), - "utf-8", - ); + const contents = await readFile(join(this.path, "project.arc.json"), "utf-8"); this.metadata = JSON.parse(contents); - if ( - !this.metadata?.metadata || - !this.metadata.outFile || - !this.metadata.payloadDir - ) + if (!this.metadata?.metadata || !this.metadata.outFile || !this.metadata.payloadDir) throw `Your project file is missing the 'metadata', 'outFile' or 'payloadDir' properties.`; this.filesystem = new Filesystem(this.path, this.metadata!.payloadDir); diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 45ae811..e71e7d9 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -45,31 +45,23 @@ export async function StartServer(project: Project) { let watchTimeout: NodeJS.Timeout | undefined; let watchTimeoutTS = false; - watch( - join(project.path, project.metadata!.payloadDir), - { persistent: true, recursive: true }, - async (e, filename) => { - if (!watchTimeout && !watchTimeoutTS) { - if (containsTS) watchTimeoutTS = true; - if (filename?.endsWith(".css")) { - APILog.warn(`${filename || e}: Change detected, reloading CSS`); - project.websock?.client?.sock.emit("refresh-css", filename); - } else { - APILog.warn( - `${filename || e}: Change detected, restarting ${ - project.metadata?.metadata.appId - }`, - ); - if (containsTS) { - await buildTSTPA(project.path); - } - project.websock?.client?.sock.emit("restart-tpa"); + watch(join(project.path, project.metadata!.payloadDir), { persistent: true, recursive: true }, async (e, filename) => { + if (!watchTimeout && !watchTimeoutTS) { + if (containsTS) watchTimeoutTS = true; + if (filename?.endsWith(".css")) { + APILog.warn(`${filename || e}: Change detected, reloading CSS`); + project.websock?.client?.sock.emit("refresh-css", filename); + } else { + APILog.warn(`${filename || e}: Change detected, restarting ${project.metadata?.metadata.appId}`); + if (containsTS) { + await buildTSTPA(project.path); } - watchTimeoutTS = false; - watchTimeout = setTimeout(() => (watchTimeout = undefined), 200); + project.websock?.client?.sock.emit("restart-tpa"); } - }, - ); + watchTimeoutTS = false; + watchTimeout = setTimeout(() => (watchTimeout = undefined), 200); + } + }); r(); }); diff --git a/src/server/api/routes/fs/cp.ts b/src/server/api/routes/fs/cp.ts index 3ce3241..f6f7810 100644 --- a/src/server/api/routes/fs/cp.ts +++ b/src/server/api/routes/fs/cp.ts @@ -7,10 +7,7 @@ export const FsCp: RouteArrayed = [ if (!req.params.source || !req.body.destination) return stop(); try { - await project.filesystem?.copyItem( - req.params.source, - req.body.destination, - ); + await project.filesystem?.copyItem(req.params.source, req.body.destination); return stop(200); } catch { diff --git a/src/server/api/routes/fs/direct/get.ts b/src/server/api/routes/fs/direct/get.ts index dfc7834..5a1f2ea 100644 --- a/src/server/api/routes/fs/direct/get.ts +++ b/src/server/api/routes/fs/direct/get.ts @@ -34,11 +34,7 @@ export const FsDirectGet: RouteArrayed = [ "Content-Type": contentType, }); - const fileStream = await project.filesystem?.createReadStream( - path, - start, - finalEnd, - ); + const fileStream = await project.filesystem?.createReadStream(path, start, finalEnd); fileStream?.pipe(res); } else { diff --git a/src/server/api/routes/fs/mv.ts b/src/server/api/routes/fs/mv.ts index d25ccc1..45711ec 100644 --- a/src/server/api/routes/fs/mv.ts +++ b/src/server/api/routes/fs/mv.ts @@ -7,10 +7,7 @@ export const FsMv: RouteArrayed = [ if (!req.params.source || !req.body.destination) return stop(); try { - await project.filesystem?.moveItem( - req.params.source, - req.body.destination, - ); + await project.filesystem?.moveItem(req.params.source, req.body.destination); return stop(200); } catch { diff --git a/src/server/api/routes/fs/tree/get.ts b/src/server/api/routes/fs/tree/get.ts index eba10cd..cd94fef 100644 --- a/src/server/api/routes/fs/tree/get.ts +++ b/src/server/api/routes/fs/tree/get.ts @@ -7,9 +7,7 @@ export const FstreeGetPath: RouteArrayed = [ if (!req.params.path) return stop(); try { - const contents = await project.filesystem?.getDirectoryTree( - req.params.path, - ); + const contents = await project.filesystem?.getDirectoryTree(req.params.path); return res.json(contents); } catch { diff --git a/src/server/fs/index.ts b/src/server/fs/index.ts index 7f46c3e..7cb699d 100644 --- a/src/server/fs/index.ts +++ b/src/server/fs/index.ts @@ -6,13 +6,7 @@ import { platform } from "os"; import path, { join } from "path"; import { tryJsonParse } from "../../json"; import { containsTypescript } from "../../tools/build-ts-tpa"; -import { - DirectoryReadReturn, - FileEntry, - FolderEntry, - RecursiveDirectoryReadReturn, - UserQuota, -} from "../../types/fs"; +import { DirectoryReadReturn, FileEntry, FolderEntry, RecursiveDirectoryReadReturn, UserQuota } from "../../types/fs"; export class Filesystem { private path: string; @@ -34,12 +28,9 @@ export class Filesystem { } private resolvePath(relativePath?: string): string { - const resolvedPath = relativePath - ? path.resolve(this.path, relativePath) - : this.path; + const resolvedPath = relativePath ? path.resolve(this.path, relativePath) : this.path; - if (!resolvedPath.startsWith(this.path)) - throw new Error("Invalid path; breaks out of project payload"); + if (!resolvedPath.startsWith(this.path)) throw new Error("Invalid path; breaks out of project payload"); return resolvedPath; } @@ -141,10 +132,7 @@ export class Filesystem { await fs.mkdir(resolvedPath, { recursive: true }); } - public async readDirectory( - dirPath?: string, - populateShortcuts = true, - ): Promise { + public async readDirectory(dirPath?: string, populateShortcuts = true): Promise { const resolvedPath = this.resolvePath(dirPath); const dirEntries = await fs.readdir(resolvedPath, { withFileTypes: true, @@ -152,9 +140,7 @@ export class Filesystem { const size = await this.calculateFolderSize(dirPath); const fileCount = await this.countFiles(dirPath); const folderCount = await this.countFolders(dirPath); - const shortcuts = populateShortcuts - ? await this.bulk(".arclnk", dirPath) - : {}; + const shortcuts = populateShortcuts ? await this.bulk(".arclnk", dirPath) : {}; const directoryReadReturn: DirectoryReadReturn = { dirs: [], files: [], @@ -192,14 +178,10 @@ export class Filesystem { return directoryReadReturn; } - public async getDirectoryTree( - dirPath?: string, - ): Promise { + public async getDirectoryTree(dirPath?: string): Promise { const resolvedPath = this.resolvePath(dirPath); - const getTree = async ( - currentPath: string, - ): Promise => { + const getTree = async (currentPath: string): Promise => { const dirEntries = await fs.readdir(currentPath, { withFileTypes: true, }); @@ -296,11 +278,7 @@ export class Filesystem { } } - public async createReadStream( - filePath: string, - start?: number, - end?: number, - ) { + public async createReadStream(filePath: string, start?: number, end?: number) { const resolvedPath = this.resolvePath(filePath); return createReadStream(resolvedPath, { start, end }); diff --git a/src/server/websocket/index.ts b/src/server/websocket/index.ts index a60b4fc..cbd986f 100644 --- a/src/server/websocket/index.ts +++ b/src/server/websocket/index.ts @@ -30,9 +30,7 @@ export class WebSock { onConnection(sock: Socket) { if (this.client) { - SockLog.warn( - `Only one client is allowed at a time. Disconnecting ${sock.id.blue}`, - ); + SockLog.warn(`Only one client is allowed at a time. Disconnecting ${sock.id.blue}`); this.client.sock.disconnect(); } @@ -93,11 +91,7 @@ export class SockClient { break; case "process": for (const pid of this.pids) { - if ( - item.source.includes(`[${pid}]`) || - item.message.includes(`PID ${pid}`) || - item.message.includes(`${pid} PID`) - ) { + if (item.source.includes(`[${pid}]`) || item.message.includes(`PID ${pid}`) || item.message.includes(`${pid} PID`)) { log(); } } diff --git a/src/tools/build-ts-tpa.ts b/src/tools/build-ts-tpa.ts index 3f46ecb..08a47a4 100644 --- a/src/tools/build-ts-tpa.ts +++ b/src/tools/build-ts-tpa.ts @@ -3,10 +3,10 @@ import { once } from "events"; import fs from "fs"; import path, { resolve } from "path"; import process from "process"; +import { buildTSTPAOptions } from "../types/build-ts-tpa"; // this shit is annoying to deal with -const exportRegex = - /export\b\s*(?:(?:default\s*)?{\s*((?:[^,{}]+,?)+\b)\s*}|(?:default\s*)?([^,;\s]+));?/m; +const exportRegex = /export\b\s*(?:(?:default\s*)?{\s*((?:[^,{}]+,?)+\b)\s*}|(?:default\s*)?([^,;\s]+));?/m; const resourceFileRegex = /(?!\w+\.[em]?ts$|\w+\.[em]?js$)(^\w+\.?\w*)/m; const scriptFileRegex = /(\w+\.[me]?ts$|\w+\.[me]?js$)/m; @@ -17,14 +17,11 @@ export const tsFileRegex = /(\w+\.[me]?ts$)/m; let silentMode = false; let printDebugMsgs = false; -async function sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function printDebug(message?: any, ...optionalParams: any[]) { - if (printDebugMsgs && !silentMode) console.debug(message, ...optionalParams); + if (printDebugMsgs && !silentMode) { + if (!message) console.debug(); + else console.debug(message, ...optionalParams); + } } function conditionalFSRemove(source: fs.PathLike, cwd?: fs.PathLike) { @@ -32,23 +29,13 @@ function conditionalFSRemove(source: fs.PathLike, cwd?: fs.PathLike) { const fileStat = fs.statSync(source); if (!silentMode) { - console.log( - `Removing './${ - cwd - ? path.relative(cwd.toString(), source.toString()) - : path.basename(source.toString()) - }'..`, - ); + console.log(`Removing './${cwd ? path.relative(cwd.toString(), source.toString()) : path.basename(source.toString())}'..`); } fs.rmSync(source, fileStat.isDirectory() ? { recursive: true } : {}); } } -function FSCopy( - source: fs.PathLike, - destination: fs.PathLike, - cwd?: fs.PathLike, -) { +function FSCopy(source: fs.PathLike, destination: fs.PathLike, cwd?: fs.PathLike) { if (fs.existsSync(source)) { const sourceFileStat = fs.statSync(source); @@ -56,40 +43,20 @@ function FSCopy( if (!silentMode) { console.log( "Copying", - `'./${ - cwd - ? path.relative(cwd.toString(), source.toString()) - : path.basename(source.toString()) - }'`, + `'./${cwd ? path.relative(cwd.toString(), source.toString()) : path.basename(source.toString())}'`, "->", - `'./${ - cwd - ? path.relative(cwd.toString(), destination.toString()) - : path.basename(destination.toString()) - }'..`, + `'./${cwd ? path.relative(cwd.toString(), destination.toString()) : path.basename(destination.toString())}'..` ); } - fs.cpSync( - source.toString(), - destination.toString(), - sourceFileStat.isDirectory() ? { recursive: true } : {}, - ); + fs.cpSync(source.toString(), destination.toString(), sourceFileStat.isDirectory() ? { recursive: true } : {}); } } -function FSWriteFile( - destination: fs.PathLike, - data: string | NodeJS.ArrayBufferView, - cwd?: fs.PathLike, -) { +function FSWriteFile(destination: fs.PathLike, data: string | NodeJS.ArrayBufferView, cwd?: fs.PathLike) { if (!silentMode) { console.log( "Writing data to ", - `'./${ - cwd - ? path.relative(cwd.toString(), destination.toString()) - : path.basename(destination.toString()) - }'..`, + `'./${cwd ? path.relative(cwd.toString(), destination.toString()) : path.basename(destination.toString())}'..` ); } @@ -101,7 +68,7 @@ async function runCommand( args: string[] | undefined, onError: (err: Error) => void, commandOpts?: SpawnOptionsWithoutStdio, - afterRun?: () => void, + afterRun?: () => void ) { const cmd = spawn(command, args, commandOpts); @@ -122,11 +89,7 @@ async function runCommand( return code; } -function removeDeletedFiles( - srcRoot: fs.PathLike, - distRoot: fs.PathLike, - cwd: fs.PathLike, -) { +function removeDeletedFiles(srcRoot: fs.PathLike, distRoot: fs.PathLike, cwd: fs.PathLike) { printDebug("Checking for deleted files to remove in dist..\n-----"); const srcFiles = fs .readdirSync(srcRoot, { @@ -154,14 +117,34 @@ function removeDeletedFiles( printDebug("srcFiles:", srcFiles); printDebug("distFiles:", distFiles); - distFiles.forEach((val) => { + function recursiveParentRemove(filePath: fs.PathLike) { + const distFolderContents = fs.readdirSync(filePath); + printDebug("distFolderContents:", distFolderContents); + if (distFolderContents.length === 0) { + printDebug("Folder contents empty in dist, removing folder."); + conditionalFSRemove(filePath, cwd); + recursiveParentRemove(resolve(filePath.toString(), "..")); + } + } + + for (const val of distFiles) { + const distFilePath = path.resolve(distRoot.toString(), val.toString()); + const parentFolderPath = resolve(distFilePath, ".."); + const distFileStat = fs.statSync(distFilePath); + + printDebug("current val:", val); + printDebug("parent folder:", parentFolderPath); + + if (distFileStat.isDirectory()) { + printDebug("Current item is a folder."); + recursiveParentRemove(distFilePath); + } + if (!srcFiles.includes(val)) { - conditionalFSRemove( - path.resolve(distRoot.toString(), val.toString()), - cwd, - ); + conditionalFSRemove(distFilePath, cwd); + recursiveParentRemove(parentFolderPath); } - }); + } printDebug("-----\n"); } @@ -173,15 +156,11 @@ function copyNewFiles(srcRoot: string, distRoot: string, cwd: fs.PathLike) { withFileTypes: true, }) .map((val) => { - const srcPath = path.parse( - path.relative(srcRoot, path.join(val.parentPath, val.name)), - ); + const srcPath = path.parse(path.relative(srcRoot, path.join(val.parentPath, val.name))); const resFileTestResult = resourceFileRegex.test(srcPath.base); if (resFileTestResult && !val.isDirectory()) { - printDebug( - `Value '${path.join(srcPath.dir, srcPath.base)}' is a resource file`, - ); + printDebug(`Value '${path.join(srcPath.dir, srcPath.base)}' is a resource file`); return path.join(srcPath.dir, srcPath.base); } }) @@ -195,15 +174,11 @@ function copyNewFiles(srcRoot: string, distRoot: string, cwd: fs.PathLike) { withFileTypes: true, }) .map((val) => { - const srcPath = path.parse( - path.relative(distRoot, path.join(val.parentPath, val.name)), - ); + const srcPath = path.parse(path.relative(distRoot, path.join(val.parentPath, val.name))); const resFileTestResult = resourceFileRegex.test(srcPath.base); if (resFileTestResult && !val.isDirectory()) { - printDebug( - `Value '${path.join(srcPath.dir, srcPath.base)}' is a resource file`, - ); + printDebug(`Value '${path.join(srcPath.dir, srcPath.base)}' is a resource file`); return path.join(srcPath.dir, srcPath.base); } }) @@ -214,23 +189,28 @@ function copyNewFiles(srcRoot: string, distRoot: string, cwd: fs.PathLike) { printDebug("srcFiles:", srcFiles); printDebug("distFiles:", distFiles); - srcFiles.forEach((val) => { + for (const val of srcFiles) { const srcFilePath = path.resolve(srcRoot, val.toString()); const distFilePath = path.resolve(distRoot, val.toString()); - if (!distFiles.includes(val)) { - printDebug(`'${val}' was missing from dist. Copying over.`); - FSCopy(srcFilePath, distFilePath, cwd); - } else { - printDebug(`'${val}' is present in dist. Updating the file..`); - const srcFileContents = fs.readFileSync(srcFilePath); - const distFileContents = fs.readFileSync(distFilePath); + if (val.endsWith(".d.ts")) continue; + + const srcFileStat = fs.statSync(srcFilePath); + if (!srcFileStat.isDirectory()) { + if (!distFiles.includes(val)) { + printDebug(`'${val}' was missing from dist. Copying over.`); + FSCopy(srcFilePath, distFilePath, cwd); + } else { + printDebug(`'${val}' is present in dist. Updating the file..`); + const srcFileContents = fs.readFileSync(srcFilePath); + const distFileContents = fs.readFileSync(distFilePath); - if (srcFileContents.compare(distFileContents) !== 0) { - FSWriteFile(distFilePath, srcFileContents, cwd); + if (srcFileContents.compare(distFileContents) !== 0) { + FSWriteFile(distFilePath, srcFileContents, cwd); + } } } - }); + } printDebug("-----\n"); } @@ -254,7 +234,7 @@ export function containsTypescript(searchPath: fs.PathLike) { function replaceExport(contents: string) { return contents.replace(exportRegex, (subStr: string, ...args: any[]) => { - printDebug(`export replace subStr: '${subStr}'\nargs:`, args, "\n"); + printDebug(`export replace subStr: '${subStr}'\n`); const hasCurlyBrackets = subStr.includes("{"); @@ -291,36 +271,19 @@ function fixExports(distRoot: string) { }); } -async function compileAndCopySrc( - distRoot: string, - tmpRoot: string, - tmpSrc: string, - cwd: fs.PathLike, -) { +async function compileAndCopySrc(distRoot: string, tmpRoot: string, tmpSrc: string, cwd: fs.PathLike) { printDebug("Attempting to compile source..\n-----"); const pathToTsc = require.resolve("typescript/bin/tsc"); if (!fs.existsSync(resolve(cwd.toString(), "tsconfig.json"))) { - console.error( - "The project directory must have a properly configured 'tsconfig.json' file.", - ); + console.error("The project directory must have a properly configured 'tsconfig.json' file."); process.exit(-1); } - await runCommand( - "node", - [ - pathToTsc, - "--rootDir", - cwd.toString(), - "--outDir", - resolve(cwd.toString(), "tmp"), - ], - (err) => { - throw err; - }, - ); + await runCommand("node", [pathToTsc, "--rootDir", cwd.toString(), "--outDir", resolve(cwd.toString(), "tmp")], (err) => { + throw err; + }); const distFiles = fs .readdirSync(distRoot, { @@ -328,20 +291,13 @@ async function compileAndCopySrc( withFileTypes: true, }) .map((val) => { - const srcPath = path.parse( - path.relative(distRoot, path.join(val.parentPath, val.name)), - ); + const srcPath = path.parse(path.relative(distRoot, path.join(val.parentPath, val.name))); const scriptFileTestResult = scriptFileRegex.test(srcPath.base); - printDebug( - `scriptFileTest '${path.join(srcPath.dir, srcPath.base)}':`, - scriptFileTestResult, - ); + printDebug(`scriptFileTest '${path.join(srcPath.dir, srcPath.base)}':`, scriptFileTestResult); if (scriptFileTestResult && !val.isDirectory()) { - printDebug( - `Value '${path.join(srcPath.dir, srcPath.base)}' is a script file`, - ); + printDebug(`Value '${path.join(srcPath.dir, srcPath.base)}' is a script file`); return path.join(srcPath.dir, srcPath.base); } }) @@ -355,9 +311,7 @@ async function compileAndCopySrc( withFileTypes: true, }) .map((val) => { - const srcPath = path.parse( - path.relative(tmpSrc, path.join(val.parentPath, val.name)), - ); + const srcPath = path.parse(path.relative(tmpSrc, path.join(val.parentPath, val.name))); if (!val.isDirectory()) { return path.join(srcPath.dir, srcPath.base); } @@ -399,13 +353,9 @@ async function compileAndCopySrc( printDebug("-----\n"); } -export async function buildTSTPA( - cwd: fs.PathLike, - silent: boolean = false, - debugOutput: boolean = false, -) { - silentMode = silent; - printDebugMsgs = debugOutput; +export async function buildTSTPA(cwd: fs.PathLike, options?: buildTSTPAOptions) { + silentMode = options?.silent ?? false; + printDebugMsgs = options?.debugOutput ?? false; const srcRoot = resolve(cwd.toString(), "src"); const distRoot = resolve(cwd.toString(), "dist"); diff --git a/src/tpa/wizard.ts b/src/tpa/wizard.ts index 4aff6df..1ce8187 100644 --- a/src/tpa/wizard.ts +++ b/src/tpa/wizard.ts @@ -11,8 +11,7 @@ export async function TpaWizard(pkg: PackageMetadata) { initialValue: "main.js", validate(value) { if (!value) return "Please specify a filename"; - if (value.includes("/") || value.includes("\\") || value.includes("..")) - return "Please specify just a filename."; + if (value.includes("/") || value.includes("\\") || value.includes("..")) return "Please specify just a filename."; if (!value.endsWith(".js")) return "The filename needs to end in .js"; }, }); @@ -24,8 +23,7 @@ export async function TpaWizard(pkg: PackageMetadata) { initialValue: "icon.png", validate(value) { if (!value) return "Please specify a filename"; - if (value.includes("/") || value.includes("\\") || value.includes("..")) - return "Please specify just a filename."; + if (value.includes("/") || value.includes("\\") || value.includes("..")) return "Please specify just a filename."; }, }); diff --git a/src/types/api.ts b/src/types/api.ts index ea64e4e..62cfcc4 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -1,9 +1 @@ -export type Method = - | "get" - | "all" - | "post" - | "put" - | "delete" - | "patch" - | "options" - | "head"; +export type Method = "get" | "all" | "post" | "put" | "delete" | "patch" | "options" | "head"; diff --git a/src/types/build-ts-tpa.ts b/src/types/build-ts-tpa.ts new file mode 100644 index 0000000..9e611f7 --- /dev/null +++ b/src/types/build-ts-tpa.ts @@ -0,0 +1,4 @@ +export interface buildTSTPAOptions { + silent?: boolean; + debugOutput?: boolean; +} diff --git a/src/types/project.ts b/src/types/project.ts index a200cfa..8c4cd0b 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -17,12 +17,7 @@ export interface ProjectMetadata { export type RouteArrayed = [Method, string, RouteCallback, number]; export type RouteStore = RouteArrayed[]; -export type RouteCallback = ( - req: Request, - res: Response, - stop: (c?: number, json?: object) => string, - project: Project, -) => void; +export type RouteCallback = (req: Request, res: Response, stop: (c?: number, json?: object) => string, project: Project) => void; export interface RouteType { method: Method; From 42aeb55e5a856d9df6b8110d73477d22f90a149e Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:32:09 -0500 Subject: [PATCH 11/14] add impl for getting new base project files --- package.json | 1 + src/commands/new/index.ts | 6 ++--- src/project/index.ts | 25 +----------------- src/tpa/index.ts | 54 +++++++++++++++++++++++++-------------- src/tpa/wizard.ts | 2 +- 5 files changed, 41 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index 064334d..260cf95 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "socket.io": "^4.8.1", "socket.io-server": "^1.0.0-b", "tslib": "^2.8.1", + "unzipit": "^2.0.3", "zip-a-folder": "^3.1.9" } } diff --git a/src/commands/new/index.ts b/src/commands/new/index.ts index 20a86d8..b15326e 100644 --- a/src/commands/new/index.ts +++ b/src/commands/new/index.ts @@ -79,8 +79,8 @@ export default async function NewCommand(this: Command, destination: string) { const isTypeScriptProject = await select({ message: "Do you want to enable experimental TS support?", options: [ - { value: "true", label: "Sure!" }, - { value: "false", label: "No thanks." }, + { value: false, label: "No thanks." }, + { value: true, label: "Sure!" }, ], }); @@ -107,7 +107,7 @@ export default async function NewCommand(this: Command, destination: string) { const app = await TpaWizard(metadata); - scaffoldProject(app, project); + scaffoldProject(app, project, isTypeScriptProject as boolean); } export function abort(): any { diff --git a/src/project/index.ts b/src/project/index.ts index 4eefa2c..a2ce00f 100644 --- a/src/project/index.ts +++ b/src/project/index.ts @@ -30,7 +30,7 @@ export class Project { await this.createProjectFile(metadata, outFile, payloadDir, repository, devPort); await mkdir(join(this.path, payloadDir)); await mkdir(join(this.path, ".vscode")); - await writeFile(join(this.path, ".gitignore"), `${this.metadata!.metadata.appId}.arc\n.arcdev-build/`); + await writeFile(join(this.path, ".gitignore"), `${this.metadata!.metadata.appId}.arc\n.arcdev-build/\ndist/`); await writeFile( join(this.path, ".vscode/settings.json"), JSON.stringify( @@ -44,29 +44,6 @@ export class Project { ) ); - await writeFile( - join(this.path, "tsconfig.json"), - JSON.stringify( - { - compilerOptions: { - target: "ESNext", - module: "ESNext", - moduleResolution: "Node", - esModuleInterop: true, - allowJs: true, - allowSyntheticDefaultImports: true, - typeRoots: ["./"], - outDir: "./dist", - types: ["./arcos.d.ts"], - }, - include: ["./src/**/*"], - }, - null, - 2 - ), - "utf-8" - ); - await this.writeTypeDefs(); try { diff --git a/src/tpa/index.ts b/src/tpa/index.ts index efb1c5b..eb90770 100644 --- a/src/tpa/index.ts +++ b/src/tpa/index.ts @@ -1,33 +1,49 @@ import { outro, spinner } from "@clack/prompts"; import axios from "axios"; -import { writeFile } from "fs/promises"; -import { join } from "path"; +import { writeFile, mkdir } from "fs/promises"; +import { join, parse } from "path"; import { Project } from "../project"; import { ScriptedApp } from "../types/app"; +import { unzip } from "unzipit"; -export async function scaffoldProject(app: ScriptedApp, project: Project) { +export async function scaffoldProject(app: ScriptedApp, project: Project, isTypeScriptProject: boolean) { const spin = spinner(); spin.start("Scaffolding project..."); - const Path = (f: string) => - join(project.path, project.metadata!.payloadDir, f); + const Path = (f: string) => join(project.path, project.metadata!.payloadDir, f); await writeFile(Path("_app.tpa"), JSON.stringify(app, null, 2), "utf-8"); - const body = (await axios.get("https://cdn.arcapi.nl/v7cli/body.txt")) - .data as string; - const entrypoint = ( - await axios.get("https://cdn.arcapi.nl/v7cli/entrypoint.txt") - ).data as string; - const process = (await axios.get("https://cdn.arcapi.nl/v7cli/process.txt")) - .data as string; - const style = (await axios.get("https://cdn.arcapi.nl/v7cli/style.txt")) - .data as string; - - await writeFile(Path("body.html"), body, "utf-8"); - await writeFile(Path(app.entrypoint!), entrypoint, "utf-8"); - await writeFile(Path("process.js"), process, "utf-8"); - await writeFile(Path("style.css"), style.replace("{{id}}", app.id), "utf-8"); + const repoName = "v7cli-templates"; + const branchName = "main"; + + const templatesZip = ( + await axios.get(`https://github.com/ArcOS-Project/${repoName}/archive/${branchName}.zip`, { responseType: "arraybuffer" }) + ).data as ArrayBuffer; + + const { entries } = await unzip(templatesZip); + + const zipName = `${repoName}-${branchName}`; + const projectFileFolderName = isTypeScriptProject ? "typescript" : "javascript"; + const projectFilesFolder = `${zipName}/${projectFileFolderName}`; + const isFileRegex = /(\w+\.?\w*$)/m; + + await writeFile(join(project.path, "tsconfig.json"), await entries[`${zipName}/tsconfig.json`].text()); + + for (const [name, _entry] of Object.entries(entries)) { + if (name.includes(projectFileFolderName) && isFileRegex.test(name)) { + const entryPath = parse(name); + const splitPath = entryPath.dir.split("/"); + const localPath = splitPath.splice(2, splitPath.length).join("/"); + + const srcCode = (await entries[name].text()).replace("{{id}}", app.id); + + await mkdir(Path(localPath), { + recursive: true, + }); + await writeFile(Path(`${localPath}/${entryPath.base}`), srcCode, "utf-8"); + } + } spin.stop("Project scaffolded."); outro(); diff --git a/src/tpa/wizard.ts b/src/tpa/wizard.ts index 1ce8187..322c16c 100644 --- a/src/tpa/wizard.ts +++ b/src/tpa/wizard.ts @@ -8,7 +8,7 @@ export async function TpaWizard(pkg: PackageMetadata) { const entrypoint = await text({ message: "What do you want to call your entrypoint file?", - initialValue: "main.js", + initialValue: "process.js", validate(value) { if (!value) return "Please specify a filename"; if (value.includes("/") || value.includes("\\") || value.includes("..")) return "Please specify just a filename."; From babfdb7120a27153dbc4ac7c3e166e77ee0f7e08 Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:05:12 -0500 Subject: [PATCH 12/14] add support for new process type --- src/commands/new/index.ts | 20 ++++++++++++++------ src/tpa/index.ts | 13 ++++++------- yarn.lock | 5 +++++ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/commands/new/index.ts b/src/commands/new/index.ts index b15326e..07b1b0e 100644 --- a/src/commands/new/index.ts +++ b/src/commands/new/index.ts @@ -74,17 +74,25 @@ export default async function NewCommand(this: Command, destination: string) { }, }); - if (isCancel(installLocation)) abort(); + const processType = await select({ + message: "What kind of app is this?", + options: [ + { value: "AppProcess", label: "AppProcess", hint: "A window is included for the user to interact with." }, + { value: "Process", label: "Process", hint: "No included window, yet has access to all ArcOS offers." }, + ], + }); + + if (isCancel(processType)) abort(); - const isTypeScriptProject = await select({ + const projectType = await select({ message: "Do you want to enable experimental TS support?", options: [ - { value: false, label: "No thanks." }, - { value: true, label: "Sure!" }, + { value: "javascript", label: "No thanks." }, + { value: "typescript", label: "Sure!" }, ], }); - if (isCancel(isTypeScriptProject)) abort(); + if (isCancel(projectType)) abort(); const metadata: PackageMetadata = { name: name.toString(), @@ -107,7 +115,7 @@ export default async function NewCommand(this: Command, destination: string) { const app = await TpaWizard(metadata); - scaffoldProject(app, project, isTypeScriptProject as boolean); + scaffoldProject(app, project, processType.toString(), projectType.toString()); } export function abort(): any { diff --git a/src/tpa/index.ts b/src/tpa/index.ts index eb90770..1e34587 100644 --- a/src/tpa/index.ts +++ b/src/tpa/index.ts @@ -6,7 +6,7 @@ import { Project } from "../project"; import { ScriptedApp } from "../types/app"; import { unzip } from "unzipit"; -export async function scaffoldProject(app: ScriptedApp, project: Project, isTypeScriptProject: boolean) { +export async function scaffoldProject(app: ScriptedApp, project: Project, processType: string, projectType: string) { const spin = spinner(); spin.start("Scaffolding project..."); @@ -24,17 +24,16 @@ export async function scaffoldProject(app: ScriptedApp, project: Project, isType const { entries } = await unzip(templatesZip); const zipName = `${repoName}-${branchName}`; - const projectFileFolderName = isTypeScriptProject ? "typescript" : "javascript"; - const projectFilesFolder = `${zipName}/${projectFileFolderName}`; const isFileRegex = /(\w+\.?\w*$)/m; await writeFile(join(project.path, "tsconfig.json"), await entries[`${zipName}/tsconfig.json`].text()); for (const [name, _entry] of Object.entries(entries)) { - if (name.includes(projectFileFolderName) && isFileRegex.test(name)) { - const entryPath = parse(name); - const splitPath = entryPath.dir.split("/"); - const localPath = splitPath.splice(2, splitPath.length).join("/"); + const entryPath = parse(name); + const [_zipName, processTypeFolder, projectTypeFolder, ...localPathSplit] = entryPath.dir.split("/"); + + if (processTypeFolder === processType && projectTypeFolder === projectType && isFileRegex.test(name)) { + const localPath = localPathSplit.join("/"); const srcCode = (await entries[name].text()).replace("{{id}}", app.id); diff --git a/yarn.lock b/yarn.lock index 6c7b058..0a6b26b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1922,6 +1922,11 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +unzipit@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/unzipit/-/unzipit-2.0.3.tgz#93fe755d3352d1767aef0b02cd634e7554226a01" + integrity sha512-e6+ftgSQRj9Hwf2mIWFwYIGqBoKV7AziG/d7pep2Uv3NCzHbhLO5d9dGRwpahVlnpQ7aQC+5kWk72dnWocdYyA== + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" From d1c70588d327cc75d2c528d7ef01fe8347f8941b Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:08:36 -0500 Subject: [PATCH 13/14] clarify TS support wording --- src/commands/new/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/new/index.ts b/src/commands/new/index.ts index 07b1b0e..425689f 100644 --- a/src/commands/new/index.ts +++ b/src/commands/new/index.ts @@ -85,7 +85,7 @@ export default async function NewCommand(this: Command, destination: string) { if (isCancel(processType)) abort(); const projectType = await select({ - message: "Do you want to enable experimental TS support?", + message: "Do you want to enable experimental TypeScript support?", options: [ { value: "javascript", label: "No thanks." }, { value: "typescript", label: "Sure!" }, From 3900bfc336ee55012372ef93529cc9a837166ba0 Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:44:09 -0500 Subject: [PATCH 14/14] i guess these never updated? --- package-lock.json | 14 ++++++++++++-- yarn.lock | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index debecc8..055d959 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "v7cli", - "version": "1.0.14", + "version": "1.0.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "v7cli", - "version": "1.0.14", + "version": "1.0.19", "license": "MIT", "dependencies": { "@clack/prompts": "^0.10.1", @@ -33,6 +33,7 @@ "socket.io": "^4.8.1", "socket.io-server": "^1.0.0-b", "tslib": "^2.8.1", + "unzipit": "^2.0.3", "zip-a-folder": "^3.1.9" }, "bin": { @@ -3306,6 +3307,15 @@ "node": ">= 0.8" } }, + "node_modules/unzipit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unzipit/-/unzipit-2.0.3.tgz", + "integrity": "sha512-e6+ftgSQRj9Hwf2mIWFwYIGqBoKV7AziG/d7pep2Uv3NCzHbhLO5d9dGRwpahVlnpQ7aQC+5kWk72dnWocdYyA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/yarn.lock b/yarn.lock index 0a6b26b..44d1935 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1924,7 +1924,7 @@ unpipe@1.0.0, unpipe@~1.0.0: unzipit@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/unzipit/-/unzipit-2.0.3.tgz#93fe755d3352d1767aef0b02cd634e7554226a01" + resolved "https://registry.npmjs.org/unzipit/-/unzipit-2.0.3.tgz" integrity sha512-e6+ftgSQRj9Hwf2mIWFwYIGqBoKV7AziG/d7pep2Uv3NCzHbhLO5d9dGRwpahVlnpQ7aQC+5kWk72dnWocdYyA== util-deprecate@^1.0.1, util-deprecate@~1.0.1: