diff --git a/package.json b/package.json new file mode 100644 index 0000000..32cfe1f --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "@lumeweb/publish-webapp", + "version": "0.1.0", + "type": "module", + "main": "lib/index.js", + "bin": "./lib/index.js", + "devDependencies": { + "@lumeweb/node-library-preset": "^0.2.7", + "@types/mime": "^3.0.1", + "@types/prompts": "^2.4.4", + "presetter": "*" + }, + "readme": "ERROR: No README data found!", + "_id": "@lumeweb/publish-webapp@0.1.0", + "scripts": { + "prepare": "presetter bootstrap", + "build": "run build" + }, + "dependencies": { + "@lumeweb/libweb": "0.2.0-develop.31", + "array-from-async": "^3.0.0", + "chalk": "^5.3.0", + "mime": "^3.0.0", + "msgpackr": "^1.9.7", + "p-queue": "^7.3.4", + "prompts": "^2.4.2" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..bb0ee0f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,122 @@ +import fs from "fs/promises"; +import path from "path"; +import * as process from "process"; +import fromAsync from "array-from-async"; + +import * as util from "util"; + +import { + hexToBytes, + loginActivePortals, + maybeInitDefaultPortals, + setActivePortalMasterKey, + uploadObject, +} from "@lumeweb/libweb"; + +import chalk from "chalk"; + +import mime from "mime"; +import { pack } from "msgpackr"; +import PQueue from "p-queue"; +import prompts from "prompts"; + +import type { WebAppMetadata } from "#types.js"; + +let key = process.env.PORTAL_PRIVATE_KEY; +let dir = process.env.DIR; +const parallelUploads = parseInt(process.env.PARALLEL_UPLOADS ?? "0", 10) || 10; + +if (!key) { + key = await prompts.prompts.password({ + name: "private_key", + message: "Enter your private key", + validate: (prev: string) => prev && prev.length === 64, + type: undefined, + }); +} + +if (!dir) { + dir = (await prompts.prompts.text({ + name: "dir", + message: "Enter the directory of the webapp", + validate: (prev: string) => prev && prev.length > 0, + type: undefined, + })) as unknown as string; +} + +dir = path.resolve(dir) + "/"; + +setActivePortalMasterKey(hexToBytes(key as string)); +maybeInitDefaultPortals(); + +const processedFiles: Array<{ cid: string; file: string; size: number }> = []; +const queue = new PQueue({ concurrency: parallelUploads }); + +void (await loginActivePortals()); + +const files: string[] = await fromAsync(walkSync(dir)); + +files.forEach((item) => { + void queue.add(async () => processFile(item)); +}); + +await queue.onIdle(); + +const metadata: WebAppMetadata = { + type: "web_app", + paths: {}, + tryFiles: ["index.html"], +}; + +processedFiles + .sort((a, b) => { + if (a.file < b.file) { + return -1; + } + if (a.file > b.file) { + return 1; + } + + return 0; + }) + .forEach((item) => { + metadata.paths[item.file] = { + cid: item.cid, + contentType: mime.getType(item.file) ?? "application/octet-stream", + size: item.size, + }; + }); + +const serializedMetadata = pack(metadata); + +const [cid, err] = await uploadObject(serializedMetadata); +if (err) { + console.error("Failed to publish: ", err); + process.exit(1); +} + +console.log( + util.format("%s: %s", chalk.green("Web App successfully published"), cid), +); + +async function processFile(filePath: string) { + const fd = await fs.open(filePath); + const size = (await fd.stat()).size; + const [cid, err] = await uploadObject(fd.createReadStream(), BigInt(size)); + if (err) { + console.error("Failed to publish: ", err); + process.exit(1); + } + processedFiles.push({ cid, file: filePath.replace(dir as string, ""), size }); +} + +async function* walkSync(dir: string): AsyncGenerator { + const files = await fs.readdir(dir, { withFileTypes: true }); + for (const file of files) { + if (file.isDirectory()) { + yield* walkSync(path.join(dir, file.name)); + } else { + yield path.join(dir, file.name); + } + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..162385d --- /dev/null +++ b/src/types.ts @@ -0,0 +1,23 @@ +export interface WebAppMetadata { + type: "web_app"; + name?: string; + tryFiles?: string[]; + errorPages?: { + [key: string]: string; // key should match the pattern ^\d{3}$ + }; + paths: { + [path: string]: PathContent; // path has maxLength 255 + }; + extraMetadata?: ExtraMetadata; // I'm assuming this as any since the actual structure isn't provided +} + +export interface PathContent { + cid: CID; // Assuming CID is another interface or type + contentType?: string; // Should match the provided pattern + size: number; +} + +// Placeholder definitions based on the $ref in the schema. +// You should replace these with the actual structures if you have them. +export type CID = any; +export type ExtraMetadata = any;