rokay
Advanced tools
Comparing version 0.0.10 to 0.0.11-alpha.0
import { Command } from "commander"; | ||
import { lintProject } from "../tslint.js"; | ||
import { Log } from "../utils/log.js"; | ||
import { getRokayCtx } from "../utils/rokay-ctx.js"; | ||
import { getClientTsconfig } from "./tsconfig.js"; | ||
import { getRokayCtx } from "../utils/rokay-ctx.js"; | ||
import { Log } from "../utils/log.js"; | ||
export const clientLint = () => new Command("client:lint") | ||
@@ -7,0 +7,0 @@ .description("Run tslint --fix on the client project") |
@@ -5,4 +5,4 @@ import { Command } from "commander"; | ||
import ts from "typescript"; | ||
import { Log } from "../utils/log.js"; | ||
import { getRokayCtx } from "../utils/rokay-ctx.js"; | ||
import { Log } from "../utils/log.js"; | ||
export const clientTsconfig = () => new Command("client:tsconfig") | ||
@@ -9,0 +9,0 @@ .description("Write out the tsconfig file for the client to src/client for editor integration") |
@@ -36,3 +36,2 @@ import { Command } from "commander"; | ||
"src/client/tsconfig.json", | ||
"src/server/main.ts", | ||
"src/server/tsconfig.json", | ||
@@ -39,0 +38,0 @@ "src/shared/tsconfig.json", |
import { Command } from "commander"; | ||
import { mkdir, rename, rm } from "fs/promises"; | ||
import { resolve } from "path"; | ||
import { getDirname } from "../lib/server/node.js"; | ||
import { eject } from "./eject.js"; | ||
@@ -22,13 +23,20 @@ import { gitignore } from "./gitignore.js"; | ||
import { CURRENT_ROKAY_VERSION } from "./utils/rokay-ctx.js"; | ||
const __dirname = getDirname(import.meta); | ||
export const comInit = () => new Command("init") | ||
.description("Create a new rokay repo") | ||
.action(() => init() | ||
.then(() => { process.exit(0); })), init = () => prompt("Enter your project's name (i.e. the directory name): ") | ||
.option("--description <description>", "an optional description for the project") | ||
.option("--display-name <displayName>", "the display name for the project (defaults to capitalized name)") | ||
.option("--name <name>", "the project name/directory used for the project") | ||
.option("--port <port>", "the server port") | ||
.option("--theme <theme>", "the background/theme color (defaults to #00f)") | ||
.action((cliArgs) => init(cliArgs) | ||
.then(() => { process.exit(0); })), init = (cliArgs) => argOr(cliArgs.name, "Enter your project's name (i.e. the directory name): ") | ||
.then(name => { | ||
console.log("name:", name); | ||
if (!name) { | ||
throw new Error("You must enter a project name!"); | ||
} | ||
return promptDefault("Enter the display name", capitalize(name)) | ||
.then(title => promptDefault("Enter a description", "") | ||
.then(description => prompt("Enter the server port: ") | ||
return argOrDefault(cliArgs.displayName, "Enter the display name", capitalize(name)) | ||
.then(title => argOrDefault(cliArgs.description, "Enter a description", "") | ||
.then(description => argOr(cliArgs.port, "Enter the server port: ") | ||
.then(p => { | ||
@@ -41,6 +49,6 @@ const port = parseInt(p, 10); | ||
}) | ||
.then(port => promptDefault("Enter the background/theme color", "#00f") | ||
.then(port => argOrDefault(cliArgs.theme, "Enter the background/theme color", "#00f") | ||
.then(theme => initWithArgs({ name, description, port, theme, title }))))); | ||
}); | ||
const capitalize = (str) => str.replace(/(?:\s[a-z]|^[a-z])/g, c => c.toUpperCase()), initWithArgs = ({ name, description, port, theme, title }) => { | ||
const argOr = (val, promptMsg) => (val ? Promise.resolve(val) : prompt(promptMsg)), argOrDefault = (val, promptMsg, def) => (val ? Promise.resolve(val) : promptDefault(promptMsg, def)), capitalize = (str) => str.replace(/(?:\s[a-z]|^[a-z])/g, c => c.toUpperCase()), initWithArgs = ({ name, description, port, theme, title }) => { | ||
const cwd = process.cwd(), dir = resolve(__dirname, "..", "..", "..", ".staging"), ctx = { | ||
@@ -108,20 +116,9 @@ description, | ||
"npm i", | ||
"@types/bcrypt", | ||
"@types/cookie-session", | ||
"@types/express", | ||
"@types/morgan", | ||
"@types/pg", | ||
"@types/uuid", | ||
"bcrypt", | ||
"busboy", | ||
"cookie-session", | ||
"express", | ||
"helmet", | ||
"morgan", | ||
"okay-site-login", | ||
"pg", | ||
"rokay", | ||
"tslib", | ||
"tslint", | ||
"typescript", | ||
"uuid", | ||
].join(" "))) | ||
.then(() => exec("npm link login rokay")); | ||
].join(" "))); |
@@ -16,2 +16,3 @@ #!/usr/bin/env -S node --experimental-vm-modules | ||
import { packageScripts } from "./package-scripts.js"; | ||
import { comPackage } from "./package.js"; | ||
import { comRoutes, comRoutesWatch } from "./routes/index.js"; | ||
@@ -47,5 +48,4 @@ import { comScaffoldApp } from "./scaffold/app.js"; | ||
import { watch } from "./watch.js"; | ||
import { comPackage } from "./package.js"; | ||
const main = (argv) => { | ||
const program = new Command().version("0.0.7-alpha.0") | ||
const program = new Command() | ||
.description("Rokay CLI") | ||
@@ -52,0 +52,0 @@ .addCommand(comBuild()) |
@@ -14,3 +14,3 @@ import { Command } from "commander"; | ||
scaffold.write(resolve(ctx.dir, "src", "shared", "app"), "ts", `\ | ||
import { User } from "login/shared/users/types.gen" | ||
import { User } from "okay-site-login/shared/users/types.gen" | ||
import { Asink } from "rokay/prop/async" | ||
@@ -17,0 +17,0 @@ import { Router } from "rokay/route/router" |
@@ -20,3 +20,3 @@ import { Command } from "commander"; | ||
scaffold.write(resolve(ctx.dir, "src", "client", ctx.name), "ts", `\ | ||
import { getUserAsink } from "login/client/users/service" | ||
import { getUserAsink } from "okay-site-login/client/users/service" | ||
import { apd } from "rokay/browser/core" | ||
@@ -23,0 +23,0 @@ import { code, div, h2 } from "rokay/browser/elt" |
@@ -31,4 +31,5 @@ import { Command } from "commander"; | ||
}, | ||
type: "module", | ||
version: "1.0.0", | ||
}, null, 2) + "\n"); | ||
}; |
import { Command } from "commander"; | ||
import { resolve } from "path"; | ||
import { capitalize } from "../../lib/data/string.js"; | ||
import { routesFromCtx } from "../routes/index.js"; | ||
@@ -215,3 +214,3 @@ import { typesFromCtx } from "../types/index.js"; | ||
}) | ||
`), ctx = getRokayCtx(), name = rawName.toLowerCase(), names = name + "s", Name = capitalize(name), NameAPI = `${Name}API`, NameDao = `${Name}Dao`, NameDaoReadArgs = `${NameDao}ReadArgs`, NameRequest = `${Name}Request`, NameRow = `${Name}Row`, NameService = `${Name}Service`, Names = capitalize(names), NamesRouter = `${Names}Router`, clientDir = resolve(ctx.dir, "src", "client", names), sharedDir = resolve(ctx.dir, "src", "shared", names), serverDir = resolve(ctx.dir, "src", "server", names), scaffold = Scaffolder(ctx, dry, Log("scaffold:resource")); | ||
`), ctx = getRokayCtx(), name = rawName.toLowerCase(), names = name + "s", clientDir = resolve(ctx.dir, "src", "client", names), sharedDir = resolve(ctx.dir, "src", "shared", names), serverDir = resolve(ctx.dir, "src", "server", names), scaffold = Scaffolder(ctx, dry, Log("scaffold:resource")), { Name } = scaffold, NameAPI = `${Name}API`, NameDao = `${Name}Dao`, NameDaoReadArgs = `${NameDao}ReadArgs`, NameRequest = `${Name}Request`, NameRow = `${Name}Row`, NameService = `${Name}Service`, Names = Name + "s", NamesRouter = `${Names}Router`; | ||
return Promise.all([ | ||
@@ -218,0 +217,0 @@ scaffold.mkdir(clientDir), |
import { Command } from "commander"; | ||
import { resolve } from "path"; | ||
import { capitalize } from "../../lib/data/string.js"; | ||
import { Log } from "../utils/log.js"; | ||
@@ -12,10 +11,6 @@ import { getRokayCtx } from "../utils/rokay-ctx.js"; | ||
.then(() => { process.exit(0); })), scaffoldServer = (ctx, dry) => { | ||
const Name = capitalize(ctx.name), main = `\ | ||
import { ${Name} } from "./${ctx.name}.js" | ||
${Name}({}) | ||
`, scaffold = Scaffolder(ctx, dry, Log("scaffold:server")); | ||
const scaffold = Scaffolder(ctx, dry, Log("scaffold:server")), { Name, nameSanitized } = scaffold; | ||
return Promise.all([ | ||
scaffold.write(resolve(ctx.dir, "src", "server", "app"), "ts", `\ | ||
import { User } from "login/shared/users/types.gen" | ||
import { User } from "okay-site-login/shared/users/types.gen" | ||
import { Asink } from "rokay/prop/async" | ||
@@ -41,16 +36,43 @@ import { ServerRouter } from "rokay/server/router" | ||
scaffold.write(resolve(ctx.dir, "src", "server", "config"), "ts", `\ | ||
import { LoginConfig } from "okay-site-login/server/config" | ||
export type ${Name}Config = { | ||
login: LoginConfig | ||
} | ||
`), | ||
scaffold.write(resolve(ctx.dir, "src", "server", "main"), "ts", main), | ||
scaffold.write(resolve(ctx.dir, "src", "server", "db"), "ts", `\ | ||
import { DB } from "rokay/server/db/db" | ||
export type ${Name}DB = ReturnType<typeof ${Name}DB> | ||
export const | ||
${Name}DB = (db: DB) => ({ | ||
single: db.single, | ||
transact: db.transact, | ||
}) | ||
`), | ||
scaffold.write(resolve(ctx.dir, "src", "server", "main"), "ts", `\ | ||
import { getLoginConfig } from "okay-site-login/server/config" | ||
import { ${Name} } from "./${ctx.name}.js" | ||
${Name}({ | ||
login: getLoginConfig(), | ||
}) | ||
`), | ||
scaffold.write(resolve(ctx.dir, "src", "server", ctx.name), "ts", `\ | ||
import express from "express" | ||
import { UserService } from "login/server/users/service" | ||
import { userExpress } from "login/server/utils" | ||
import { LoginDB } from "okay-site-login/server/login-db" | ||
import { UserService } from "okay-site-login/server/users/service" | ||
import { UserServer } from "okay-site-login/server/utils" | ||
import { resolve } from "path" | ||
import { Pool } from "pg" | ||
import { migrate } from "rokay/server/db/pg" | ||
import { NotFound } from "rokay/server/errors" | ||
import { renderPage } from "rokay/server/render" | ||
import pg from "pg" | ||
import { DBPG } from "rokay/server/db/db-pg" | ||
import { Eq } from "rokay/server/db/filter" | ||
import { migrate } from "rokay/server/db/migrate" | ||
import { getDirname } from "rokay/server/node" | ||
@@ -60,62 +82,60 @@ import { AppServer } from "./app.js" | ||
import { ${Name}Config } from "./config.js" | ||
import { ${Name}DB } from "./db.js" | ||
import { HTML } from "./html.js" | ||
const { Pool } = pg | ||
export const | ||
${Name} = (_config: ${Name}Config) => { | ||
const | ||
pool = new Pool(), | ||
const __dirname = getDirname(import.meta) | ||
PORT = process.env.PORT || ${ctx.port}, | ||
STATIC_DIR = resolve(__dirname, "..", "..", "static"), | ||
TITLE = ${JSON.stringify(ctx.title)}, | ||
users = UserService(pool), | ||
export const | ||
${Name} = (config: ${Name}Config) => { | ||
getAndMigrate${Name}DB().then(({ ${nameSanitized}DB: _${nameSanitized}DB, loginDB }) => { | ||
const | ||
users = UserService(config.login, loginDB) | ||
app = userExpress(TITLE, users) | ||
// APIs GO HERE | ||
.get("*", (req, res, next) => { | ||
res.setHeader("Vary", "Content-Type") | ||
if (req.headers.accept && req.headers.accept.includes("text/html")) { | ||
users.current(req, res) | ||
.then(user => { | ||
const app = AppServer(req, user) | ||
res.send(renderPage(HTML(app, Base(app)))) | ||
}, next) | ||
} else { | ||
next() | ||
} | ||
UserServer(${JSON.stringify(ctx.title)}, ${ctx.port}, { | ||
staticDir: resolve(__dirname, "..", "..", "static"), | ||
users, | ||
}, [ | ||
// APIs go here | ||
], (req, res) => | ||
users.current(req, res).then(user => { | ||
const app = AppServer(req, user) | ||
return HTML(app, Base(app)) | ||
}) | ||
) | ||
}, error => { | ||
console.error("Error Running Migrations:", error) | ||
}) | ||
} | ||
.use(express.static(STATIC_DIR)) | ||
.use((req, _, next) => { | ||
next(NotFound(\`Cannot \${req.method} \${req.originalUrl}\`)) | ||
}) | ||
const | ||
getAndMigrate${Name}DB = (): Promise<{ ${nameSanitized}DB: ${Name}DB, loginDB: LoginDB }> => { | ||
const | ||
project = "${ctx.name}", | ||
pool = new Pool(), | ||
db = DBPG(pool), | ||
Migrations = db.table<{ | ||
created: Date | ||
file: string | ||
project: string | ||
}>("migrations") | ||
.use(((error, req, res, _) => { | ||
const { message, status } = error | ||
if (status == null || status >= 500) { | ||
console.error("Internal Server Error:", error) | ||
} | ||
if (!res.headersSent) { | ||
res.status(status != null ? status : 500).send({ message }) | ||
} else { | ||
console.error("Headers sent. Not sending anything. URL:", req.originalUrl) | ||
} | ||
}) as express.ErrorRequestHandler) | ||
migrate(pool, ${JSON.stringify(ctx.name)}, resolve(__dirname, "..", "..", "schema"), [ | ||
//{ name: <some string>, cb: <some callback> }, | ||
]) | ||
.then(() => { | ||
app.listen(PORT, () => { | ||
console.log(\`\${TITLE} Listening on Port \${PORT}...\`) | ||
}) | ||
}, error => { | ||
console.error("Error Running Migrations:", error) | ||
}) | ||
return migrate( | ||
resolve(__dirname, "..", "..", "schema"), | ||
db.transact, | ||
conn => | ||
conn.q(Migrations.select(["file"], { | ||
where: Eq("project", project) | ||
})) | ||
.then(rows => rows.map(r => r.file)), | ||
(conn, file) => | ||
conn.e(Migrations.insert([{ file, project }])), | ||
) | ||
.then(() => ({ | ||
${nameSanitized}DB: ${Name}DB(db), | ||
loginDB: LoginDB(db), | ||
})) | ||
} | ||
@@ -122,0 +142,0 @@ `), |
@@ -16,6 +16,9 @@ import { Command } from "commander"; | ||
margin: 0; | ||
padding: 0 } | ||
padding: 0; | ||
} | ||
body { background-color: ${ctx.theme} } | ||
body { | ||
background-color: ${ctx.theme}; | ||
} | ||
`); | ||
}; |
@@ -1,2 +0,7 @@ | ||
export const prompt = (question) => new Promise((res, rej) => { | ||
export const | ||
/** | ||
* asks the user the question exactly as given and resolves to the user's trimmed input from stdin. | ||
* this is locked to only allow asking the user one question at a time. | ||
**/ | ||
prompt = (question) => withLock(() => new Promise((res, rej) => { | ||
const onerror = (err) => { | ||
@@ -14,3 +19,41 @@ process.stdin.off("data", onsuccess); | ||
process.stdin.on("error", onerror); | ||
}), promptDefault = (question, def) => prompt(`${question} (${def}): `) | ||
})), | ||
/** | ||
* asks the user the given question with the default in parentheses and a colon and space afterward. | ||
* this resolves to def if a blank response is given. | ||
**/ | ||
promptDefault = (question, def) => prompt(`${question} (${def}): `) | ||
.then(res => res ? res : def); | ||
const | ||
/** | ||
* a queue of work to do in terms of prompting the user. This prevents multiple calls to prompt | ||
* from stepping on each other's toes and getting back the same answer. | ||
**/ | ||
queue = []; | ||
const | ||
/** | ||
* Adds the given cb to the work queue, ensure that only one cb is asking for user input at a time. | ||
* @returns the value that the promise returned by cb resolves to | ||
**/ | ||
withLock = (cb) => new Promise(res => { | ||
queue.push(() => new Promise(innerRes => { | ||
res(cb().finally(() => { | ||
innerRes(); | ||
})); | ||
})); | ||
if (queue.length === 1) { | ||
runNext(); | ||
} | ||
}), | ||
/** | ||
* recursively runs through the items in the work queue until it is empty | ||
**/ | ||
runNext = () => { | ||
const next = queue.shift(); | ||
if (next == null) { | ||
return; | ||
} | ||
else { | ||
next().finally(runNext); | ||
} | ||
}; |
import { existsSync } from "fs"; | ||
import { mkdir, readFile, writeFile } from "fs/promises"; | ||
import { relative } from "path"; | ||
import { capitalize } from "../../lib/data/string.js"; | ||
import { promptDefault } from "./prompt.js"; | ||
export const Scaffolder = (ctx, dry, log) => ({ | ||
mkdir(path) { | ||
log("Ensuring directory %s exists...", relative(ctx.dir, path)); | ||
return mkdir(path, { recursive: true }); | ||
}, | ||
write(pathBase, extension, contents) { | ||
const ext = extension != null ? `.${extension}` : "", path = `${pathBase}${ext}`; | ||
if (!dry) { | ||
const rel = relative(ctx.dir, path); | ||
log("Creating %s...", rel); | ||
return readFile(path, { encoding: "utf-8" }) | ||
.then(old => { | ||
if (old === contents) { | ||
log("%s already exists, but the content is the same. Skipping.", rel); | ||
return undefined; | ||
} | ||
log("%s already exists and has been modified.", rel); | ||
return promptDefault("Q to Quit, R to Replace, S to Skip, K to Keep Both", "Q") | ||
.then(raw => { | ||
const key = raw.toUpperCase(); | ||
if (key === "K") { | ||
let i = 0; | ||
while (existsSync(`${pathBase}.${++i}${ext}`)) { } | ||
const path = `${pathBase}.${i}${ext}`; | ||
log("Writing to", relative(ctx.dir, path)); | ||
return writeFile(path, contents); | ||
export const scaffoldName = (name) => capitalize(name.replace(/[^\w]+(\w)/g, (_, c) => c.toUpperCase())), Scaffolder = (ctx, dry, log) => { | ||
const nameSanitized = ctx.name.replace(/[^\w]+(\w)/g, (_, c) => c.toUpperCase()), Name = capitalize(nameSanitized); | ||
return { | ||
mkdir(path) { | ||
log("Ensuring directory %s exists...", relative(ctx.dir, path)); | ||
return mkdir(path, { recursive: true }); | ||
}, | ||
Name, | ||
nameSanitized, | ||
write(pathBase, extension, contents) { | ||
const ext = extension != null ? `.${extension}` : "", path = `${pathBase}${ext}`; | ||
if (!dry) { | ||
const rel = relative(ctx.dir, path); | ||
log("Creating %s...", rel); | ||
return readFile(path, { encoding: "utf-8" }) | ||
.then(old => { | ||
if (old === contents) { | ||
log("%s already exists, but the content is the same. Skipping.", rel); | ||
return undefined; | ||
} | ||
else if (key === "S") { | ||
log("Skipping."); | ||
return; | ||
} | ||
else if (key === "Q") { | ||
log("Quitting."); | ||
throw new Error("I Quit!"); | ||
} | ||
else if (key === "R") { | ||
log("Replacing."); | ||
log("%s already exists and has been modified.", rel); | ||
return promptDefault("Q to Quit, R to Replace, S to Skip, K to Keep Both", "Q") | ||
.then(raw => { | ||
const key = raw.toUpperCase(); | ||
if (key === "K") { | ||
let i = 0; | ||
while (existsSync(`${pathBase}.${++i}${ext}`)) { } | ||
const path = `${pathBase}.${i}${ext}`; | ||
log("Writing to", relative(ctx.dir, path)); | ||
return writeFile(path, contents); | ||
} | ||
else if (key === "S") { | ||
log("Skipping."); | ||
return; | ||
} | ||
else if (key === "Q") { | ||
log("Quitting."); | ||
throw new Error("I Quit!"); | ||
} | ||
else if (key === "R") { | ||
log("Replacing."); | ||
return writeFile(path, contents); | ||
} | ||
log("Unknown Input. Bailing."); | ||
throw new Error("Unknown Input"); | ||
}); | ||
}, error => { | ||
if (error.code === "ENOENT") { | ||
log("Writing %s...", rel); | ||
return writeFile(path, contents); | ||
} | ||
log("Unknown Input. Bailing."); | ||
throw new Error("Unknown Input"); | ||
throw error; | ||
}); | ||
}, error => { | ||
if (error.code === "ENOENT") { | ||
log("Writing %s...", rel); | ||
return writeFile(path, contents); | ||
} | ||
throw error; | ||
}); | ||
} | ||
log(`${relative(ctx.dir, path)}:`); | ||
log(contents); | ||
return Promise.resolve(); | ||
} | ||
log(`${relative(ctx.dir, path)}:`); | ||
log(contents); | ||
return Promise.resolve(); | ||
} | ||
}); | ||
}; | ||
}; |
@@ -45,3 +45,3 @@ import { onDestroy } from "./capture.js"; | ||
onDestroy(function () { | ||
listeners[key] = undefined; | ||
ctrlListeners[key] = undefined; | ||
}); | ||
@@ -48,0 +48,0 @@ }, |
import { createPool } from "mysql"; | ||
import { whereStr } from "./filter.js"; | ||
import { getToStmt } from "./stmt.js"; | ||
import { Table } from "./table.js"; | ||
import { getToStmt } from "./stmt.js"; | ||
import { whereStr } from "./filter.js"; | ||
export const DBMysql = (config) => { | ||
@@ -6,0 +6,0 @@ const doTransaction = (conn, cb) => new Promise((res, rej) => { |
@@ -0,4 +1,4 @@ | ||
import { whereStr } from "./filter.js"; | ||
import { getToStmt, returningStr } from "./stmt.js"; | ||
import { Table } from "./table.js"; | ||
import { getToStmt, returningStr } from "./stmt.js"; | ||
import { whereStr } from "./filter.js"; | ||
export const DBPG = (pool) => { | ||
@@ -5,0 +5,0 @@ const doTransaction = (cb) => pool.connect() |
{ | ||
"name": "rokay", | ||
"version": "0.0.10", | ||
"version": "0.0.11-alpha.0", | ||
"description": "A full-stack framework for making CRUD apps in TypeScript.", | ||
@@ -5,0 +5,0 @@ "scripts": { |
368917
7699