@celsoft/create-platform-app 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +49 -0
- package/dist/cli.js.map +1 -0
- package/dist/scaffold.d.ts +13 -0
- package/dist/scaffold.js +35 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +16 -4
- package/template/README.md +17 -0
- package/template/gitignore +3 -0
- package/template/migrations/001_init.sql +5 -0
- package/template/package.json +13 -0
- package/template/platform.jsonc +13 -0
- package/template/public/index.html +12 -0
- package/template/scripts/migrate.js +23 -0
- package/template/src/worker.ts +11 -0
- package/template/tsconfig.json +12 -0
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sean O'Dell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { invalid, rules, scaffold } from "./scaffold.js";
|
|
7
|
+
const usage = `usage: npm create @celsoft/platform-app <name> -- [--hostname <host>] [--neon-project <id>]`;
|
|
8
|
+
async function main() {
|
|
9
|
+
const { values, positionals } = parseArgs({
|
|
10
|
+
allowPositionals: true,
|
|
11
|
+
options: { hostname: { type: "string" }, "neon-project": { type: "string" }, help: { type: "boolean", short: "h" } },
|
|
12
|
+
});
|
|
13
|
+
if (values.help) {
|
|
14
|
+
console.log(usage);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const answers = { name: positionals[0], hostname: values.hostname, neonProject: values["neon-project"] };
|
|
18
|
+
if (process.stdin.isTTY)
|
|
19
|
+
await ask(answers);
|
|
20
|
+
const bad = invalid(answers);
|
|
21
|
+
if (bad.length > 0) {
|
|
22
|
+
throw new Error(`${bad.map((key) => `${key}: ${rules[key].hint}`).join("\n")}\n\n${usage}`);
|
|
23
|
+
}
|
|
24
|
+
const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
25
|
+
const target = resolve(answers.name);
|
|
26
|
+
await scaffold(target, answers, version);
|
|
27
|
+
console.log(`\nCreated ${answers.name}. Next:\n\n cd ${answers.name}\n npm install\n\nthen follow README.md for the first deploy.`);
|
|
28
|
+
}
|
|
29
|
+
async function ask(answers) {
|
|
30
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
31
|
+
const labels = { name: "App name", hostname: "Prod hostname", neonProject: "Neon project id" };
|
|
32
|
+
try {
|
|
33
|
+
for (const key of Object.keys(labels)) {
|
|
34
|
+
while (!rules[key].pattern.test(answers[key] ?? "")) {
|
|
35
|
+
if (answers[key])
|
|
36
|
+
console.log(` must be ${rules[key].hint}`);
|
|
37
|
+
answers[key] = (await rl.question(`${labels[key]}: `)).trim();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
rl.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
main().catch((error) => {
|
|
46
|
+
console.error(`✗ ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
49
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const TEMPLATE_DIR: string;
|
|
2
|
+
export declare const COMPATIBILITY_DATE = "2026-10-01";
|
|
3
|
+
export interface Answers {
|
|
4
|
+
name: string;
|
|
5
|
+
hostname: string;
|
|
6
|
+
neonProject: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const rules: Record<keyof Answers, {
|
|
9
|
+
pattern: RegExp;
|
|
10
|
+
hint: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function invalid(answers: Partial<Answers>): (keyof Answers)[];
|
|
13
|
+
export declare function scaffold(target: string, answers: Answers, platformVersion: string): Promise<void>;
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export const TEMPLATE_DIR = fileURLToPath(new URL("../template/", import.meta.url));
|
|
5
|
+
export const COMPATIBILITY_DATE = "2026-10-01";
|
|
6
|
+
export const rules = {
|
|
7
|
+
name: { pattern: /^[a-z][a-z0-9-]{0,49}$/, hint: "lowercase letters, digits and -, starting with a letter, at most 50 characters" },
|
|
8
|
+
hostname: { pattern: /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/, hint: "a lowercase hostname in your Cloudflare zone, such as app.example.com" },
|
|
9
|
+
neonProject: { pattern: /^[a-z]+-[a-z]+-\d+$/, hint: "a Neon project id such as quiet-meadow-12345678, from the project's Settings" },
|
|
10
|
+
};
|
|
11
|
+
export function invalid(answers) {
|
|
12
|
+
return Object.keys(rules).filter((key) => !rules[key].pattern.test(answers[key] ?? ""));
|
|
13
|
+
}
|
|
14
|
+
export async function scaffold(target, answers, platformVersion) {
|
|
15
|
+
if ((await stat(target).catch(() => undefined)) && (await readdir(target)).length > 0) {
|
|
16
|
+
throw new Error(`${target} already exists and is not empty`);
|
|
17
|
+
}
|
|
18
|
+
const vars = { ...answers, platformVersion, compatibilityDate: COMPATIBILITY_DATE };
|
|
19
|
+
await copy(TEMPLATE_DIR, target, vars);
|
|
20
|
+
}
|
|
21
|
+
async function copy(from, to, vars) {
|
|
22
|
+
await mkdir(to, { recursive: true });
|
|
23
|
+
for (const entry of await readdir(from, { withFileTypes: true })) {
|
|
24
|
+
// npm drops .gitignore from published packages, so the template stores it without the dot.
|
|
25
|
+
const name = entry.name === "gitignore" ? ".gitignore" : entry.name;
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
await copy(join(from, entry.name), join(to, name), vars);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const text = await readFile(join(from, entry.name), "utf8");
|
|
31
|
+
await writeFile(join(to, name), text.replace(/\{\{(\w+)\}\}/g, (match, key) => vars[key] ?? match));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,cAAc,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACpF,MAAM,CAAC,MAAM,kBAAkB,GAAG,YAAY,CAAC;AAQ/C,MAAM,CAAC,MAAM,KAAK,GAA6D;IAC7E,IAAI,EAAE,EAAE,OAAO,EAAE,wBAAwB,EAAE,IAAI,EAAE,gFAAgF,EAAE;IACnI,QAAQ,EAAE,EAAE,OAAO,EAAE,mEAAmE,EAAE,IAAI,EAAE,uEAAuE,EAAE;IACzK,WAAW,EAAE,EAAE,OAAO,EAAE,qBAAqB,EAAE,IAAI,EAAE,8EAA8E,EAAE;CACtI,CAAC;AAEF,MAAM,UAAU,OAAO,CAAC,OAAyB;IAC/C,OAAQ,MAAM,CAAC,IAAI,CAAC,KAAK,CAAuB,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AACjH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,MAAc,EAAE,OAAgB,EAAE,eAAuB;IACtF,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtF,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,kCAAkC,CAAC,CAAC;IAC/D,CAAC;IACD,MAAM,IAAI,GAA2B,EAAE,GAAG,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,CAAC;IAC5G,MAAM,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AACzC,CAAC;AAED,KAAK,UAAU,IAAI,CAAC,IAAY,EAAE,EAAU,EAAE,IAA4B;IACxE,MAAM,KAAK,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QACjE,2FAA2F;QAC3F,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;QACpE,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;QAC3D,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;YAC5D,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC,KAAK,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC;QAC9G,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celsoft/create-platform-app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Create an app deployed by @celsoft/platform: npm create @celsoft/platform-app",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": { "type": "git", "url": "git+https://github.com/celsoft-com/platform.git", "directory": "packages/create-platform-app" },
|
|
7
|
+
"homepage": "https://github.com/celsoft-com/platform#readme",
|
|
8
|
+
"publishConfig": { "access": "public" },
|
|
9
|
+
"type": "module",
|
|
10
|
+
"bin": { "create-platform-app": "./dist/cli.js" },
|
|
11
|
+
"files": ["dist", "template"],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"prepare": "tsc -b"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "7.0.2"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
Deployed to Cloudflare Workers with a Neon database by [@celsoft/platform](https://github.com/celsoft-com/platform).
|
|
4
|
+
|
|
5
|
+
## First deploy
|
|
6
|
+
|
|
7
|
+
1. **Repo:** create a GitHub repo for this directory and push it.
|
|
8
|
+
2. **Tokens:** run `npx platform init`; it adds blank token entries to `.env.secrets` and stops.
|
|
9
|
+
3. **Fill:** create the tokens described in the platform's [SETUP.md](https://github.com/celsoft-com/platform/blob/main/SETUP.md) and paste them into `.env.secrets`.
|
|
10
|
+
4. **Init:** run `npx platform init` again; it stores the tokens in GitHub and writes the deploy workflow.
|
|
11
|
+
5. **Ship:** commit and push; main deploys to https://{{hostname}}, and every PR gets a preview.
|
|
12
|
+
|
|
13
|
+
## Your code
|
|
14
|
+
|
|
15
|
+
1. **API:** `src/worker.ts`, where `defineApp` hands each request a ready `sql` client.
|
|
16
|
+
2. **Pages:** `public/`, served as static files.
|
|
17
|
+
3. **Schema:** add numbered SQL files to `migrations/`; each deploy applies the new ones.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@celsoft/platform": "^{{platformVersion}}",
|
|
7
|
+
"postgres": "^3.4.9"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@cloudflare/workers-types": "^5.20261023.1",
|
|
11
|
+
"typescript": "^7.0.2"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@celsoft/platform/schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"name": "{{name}}",
|
|
5
|
+
"hostname": "{{hostname}}",
|
|
6
|
+
"worker": "src/worker.ts",
|
|
7
|
+
"frontend": "public",
|
|
8
|
+
"compatibilityDate": "{{compatibilityDate}}",
|
|
9
|
+
"database": {
|
|
10
|
+
"neonProject": "{{neonProject}}",
|
|
11
|
+
"migrate": "node scripts/migrate.js"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{{name}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<h1>{{name}}</h1>
|
|
10
|
+
<p>Deployed with @celsoft/platform. Edit <code>public/index.html</code> and <code>src/worker.ts</code>.</p>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import postgres from "postgres";
|
|
3
|
+
|
|
4
|
+
const dir = new URL("../migrations/", import.meta.url);
|
|
5
|
+
const sql = postgres(process.env.DATABASE_URL, { max: 1, onnotice: () => {} });
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
await sql`create table if not exists schema_migrations (name text primary key, applied_at timestamptz not null default now())`;
|
|
9
|
+
const applied = new Set((await sql`select name from schema_migrations`).map((row) => row.name));
|
|
10
|
+
const pending = (await readdir(dir)).filter((name) => name.endsWith(".sql") && !applied.has(name)).sort();
|
|
11
|
+
|
|
12
|
+
for (const name of pending) {
|
|
13
|
+
const text = await readFile(new URL(name, dir), "utf8");
|
|
14
|
+
await sql.begin(async (tx) => {
|
|
15
|
+
await tx.unsafe(text);
|
|
16
|
+
await tx`insert into schema_migrations (name) values (${name})`;
|
|
17
|
+
});
|
|
18
|
+
console.log(`applied ${name}`);
|
|
19
|
+
}
|
|
20
|
+
if (pending.length === 0) console.log("migrations up to date");
|
|
21
|
+
} finally {
|
|
22
|
+
await sql.end();
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineApp } from "@celsoft/platform";
|
|
2
|
+
|
|
3
|
+
export default defineApp(async (request, { sql }) => {
|
|
4
|
+
const url = new URL(request.url);
|
|
5
|
+
|
|
6
|
+
if (url.pathname === "/api/items" && request.method === "GET") {
|
|
7
|
+
return Response.json(await sql`select id, name, created_at from items order by created_at desc limit 100`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return new Response("not found", { status: 404 });
|
|
11
|
+
});
|
package/README.md
DELETED