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/package.json b/package.json index 44c0ec2..260cf95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "v7cli", - "version": "1.0.15", + "version": "1.0.19", "main": "index.js", "scripts": { "cli:publish": "npx tsc && npm publish" @@ -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/build/index.ts b/src/commands/build/index.ts index 80bfb47..b29879a 100644 --- a/src/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -2,9 +2,10 @@ import { intro, spinner } from "@clack/prompts"; 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 { @@ -14,9 +15,7 @@ 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; @@ -35,14 +34,20 @@ export default async function BuildCommand() { spin.message("Creating temp directory"); await mkdir(join(project.path, ".arcdev-build"), { recursive: true }); + // DO THE BUILD HERE + const containsTS = containsTypescript(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, { silent: true }); + } + spin.message("Copying payload"); - await cp( - join(project.path, project.metadata.payloadDir), - ".arcdev-build/payload", - { - recursive: true, - } - ); + await cp(payloadDir, ".arcdev-build/payload", { + recursive: true, + }); spin.message("Writing _metadata.json"); await writeFile( @@ -52,10 +57,7 @@ export default async function BuildCommand() { ); 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 f013066..a63884e 100644 --- a/src/commands/dev/index.ts +++ b/src/commands/dev/index.ts @@ -1,10 +1,11 @@ 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 buildTSTPA, { containsTypescript } from "../../tools/build-ts-tpa"; export default async function DevCommand() { const project = new Project(cwd()); @@ -12,9 +13,7 @@ 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; @@ -27,12 +26,18 @@ export default async function DevCommand() { process.exit(1); } - const buildHash = await getArcBuild() + const buildHash = await getArcBuild(); - if (project.metadata?.buildHash == null || project.metadata.buildHash != buildHash) { + if (project.metadata?.buildHash == null) { project.metadata!!.buildHash = buildHash; } + const containsTS = containsTypescript(project.metadata!.payloadDir); + + if (containsTS) { + await buildTSTPA(project.path); + } + await StartServer(project); const name = "ArcOS v7 CLI".green.bold; diff --git a/src/commands/new/index.ts b/src/commands/new/index.ts index 38939e9..425689f 100644 --- a/src/commands/new/index.ts +++ b/src/commands/new/index.ts @@ -1,4 +1,4 @@ -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"; @@ -27,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.`; }, }); @@ -39,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.`; }, }); @@ -50,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"; }, }); @@ -77,7 +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 projectType = await select({ + message: "Do you want to enable experimental TypeScript support?", + options: [ + { value: "javascript", label: "No thanks." }, + { value: "typescript", label: "Sure!" }, + ], + }); + + if (isCancel(projectType)) abort(); const metadata: PackageMetadata = { name: name.toString(), @@ -100,7 +115,7 @@ export default async function NewCommand(this: Command, destination: string) { const app = await TpaWizard(metadata); - scaffoldProject(app, project); + scaffoldProject(app, project, processType.toString(), projectType.toString()); } export function abort(): any { diff --git a/src/commands/update.ts b/src/commands/update.ts index ede0cf4..2400eca 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()); diff --git a/src/index.ts b/src/index.ts index e068d41..77abb44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,17 +2,14 @@ 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(); -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 668ecaa..1b2e7c1 100644 --- a/src/json.ts +++ b/src/json.ts @@ -10,19 +10,19 @@ 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; } 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) { @@ -33,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 28d84f4..a2ce00f 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/\ndist/`); await writeFile( join(this.path, ".vscode/settings.json"), JSON.stringify( @@ -61,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 { @@ -93,13 +53,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 +64,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 3e214d0..e71e7d9 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -1,18 +1,19 @@ -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 { watch } from "fs"; -import { join } from "path"; -import "colors"; -import signale, { Signale } from "signale"; export const App = express(); export const APILog = new Signale({ @@ -26,6 +27,8 @@ export async function StartServer(project: Project) { 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()); @@ -40,27 +43,25 @@ export async function StartServer(project: Project) { } 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"); + 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); } - 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 5ccc75c..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 b758b9d..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 820d440..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 209a122..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 473b2de..7cb699d 100644 --- a/src/server/fs/index.ts +++ b/src/server/fs/index.ts @@ -1,34 +1,36 @@ +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 { - 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"; +import { DirectoryReadReturn, FileEntry, FolderEntry, RecursiveDirectoryReadReturn, UserQuota } from "../../types/fs"; export class Filesystem { private path: string; accessors: Record = {}; // R constructor(projectPath: string, payloadDir: string) { - this.path = join(projectPath, payloadDir); + 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); + } } 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; } @@ -37,7 +39,9 @@ export class Filesystem { const resolvedPath = this.resolvePath(folderPath); const calculateSize = async (directory: string): Promise => { - const entries = await fs.readdir(directory, { withFileTypes: true }); + const entries = await fs.readdir(directory, { + withFileTypes: true, + }); let totalSize = 0; @@ -61,7 +65,9 @@ export class Filesystem { const calculate = async (directory: string): Promise => { let count = 0; - const entries = await fs.readdir(directory, { withFileTypes: true }); + const entries = await fs.readdir(directory, { + withFileTypes: true, + }); for (const entry of entries) { const entryPath = path.join(directory, entry.name); @@ -82,7 +88,9 @@ export class Filesystem { const calculate = async (directory: string): Promise => { let count = 0; - const entries = await fs.readdir(directory, { withFileTypes: true }); + const entries = await fs.readdir(directory, { + withFileTypes: true, + }); for (const entry of entries) { const entryPath = path.join(directory, entry.name); @@ -124,18 +132,15 @@ 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 }); + 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 shortcuts = populateShortcuts ? await this.bulk(".arclnk", dirPath) : {}; const directoryReadReturn: DirectoryReadReturn = { dirs: [], files: [], @@ -173,19 +178,18 @@ 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 dirEntries = await fs.readdir(currentPath, { withFileTypes: true }); + 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 dirs: (FolderEntry & { + children: RecursiveDirectoryReadReturn; + })[] = []; const files: FileEntry[] = []; for (const entry of dirEntries) { @@ -274,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 f60af88..cbd986f 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", @@ -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 new file mode 100644 index 0000000..08a47a4 --- /dev/null +++ b/src/tools/build-ts-tpa.ts @@ -0,0 +1,380 @@ +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 { 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 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; + +function printDebug(message?: any, ...optionalParams: any[]) { + if (printDebugMsgs && !silentMode) { + if (!message) console.debug(); + else 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 } : {}); + } +} + +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 😭 + 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) { + if (!silentMode) { + 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, + 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) { + 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("srcFiles:", srcFiles); + printDebug("distFiles:", distFiles); + + 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(distFilePath, cwd); + recursiveParentRemove(parentFolderPath); + } + } + 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); + + for (const val of srcFiles) { + const srcFilePath = path.resolve(srcRoot, val.toString()); + const distFilePath = path.resolve(distRoot, val.toString()); + + 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); + } + } + } + } + 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}'\n`); + + const hasCurlyBrackets = subStr.includes("{"); + + 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]; + }); + + sortedJsFiles.forEach((val) => { + const jsPath = resolve(distRoot, val.toString()); + + printDebug("jsPath:", jsPath); + + try { + const data = fs.readFileSync(jsPath); + + let contents = data.toString(); + + while (exportRegex.test(contents)) { + contents = replaceExport(contents); + + fs.writeFileSync(jsPath, contents); + } + } catch (err) { + throw err; + } + }); +} + +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."); + 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; + }); + + 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()); + + 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); + + fixExports(distRoot); + + printDebug("-----\n"); +} + +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"); + const tmpRoot = resolve(cwd.toString(), "tmp"); + const tmpSrc = resolve(tmpRoot, "src"); + + if (!fs.existsSync(distRoot)) { + fs.mkdirSync(distRoot, { + recursive: true, + }); + } + + printDebug(); + + removeDeletedFiles(srcRoot, distRoot, cwd); + + copyNewFiles(srcRoot, distRoot, cwd); + + await compileAndCopySrc(distRoot, tmpRoot, tmpSrc, cwd); +} + +export default buildTSTPA; diff --git a/src/tpa/index.ts b/src/tpa/index.ts index 25778dd..1e34587 100644 --- a/src/tpa/index.ts +++ b/src/tpa/index.ts @@ -1,33 +1,48 @@ -import { writeFile } from "fs/promises"; -import { Project } from "../project"; -import { ScriptedApp } from "../types/app"; -import { join } from "path"; import { outro, spinner } from "@clack/prompts"; import axios from "axios"; +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, processType: string, projectType: string) { 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 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)) { + 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); + + 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 14b5c59..322c16c 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) { @@ -9,11 +8,10 @@ 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."; + 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"; }, }); @@ -25,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 7849014..8c4cd0b 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; @@ -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; diff --git a/tsconfig.json b/tsconfig.json index 893e405..554a80e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,5 @@ { + "include": ["src/**/*"], "compilerOptions": { "target": "ES2020", "module": "CommonJS", diff --git a/yarn.lock b/yarn.lock index 6c7b058..44d1935 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.npmjs.org/unzipit/-/unzipit-2.0.3.tgz" + 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"