|
| 1 | +#!/usr/bin/env node |
| 2 | +// Scaffold a new app from template/. |
| 3 | +// |
| 4 | +// Usage: |
| 5 | +// npm run new-app -- <slug> <Type> <icon> <description> |
| 6 | +// npm run new-app -- contacts AddressBook 📒 "An address book" |
| 7 | +// |
| 8 | +// Creates <slug>/{index.html, app.json, <slug>-view-pane.js} from |
| 9 | +// template/, substituting placeholders. Then prints a 6-repo checklist. |
| 10 | + |
| 11 | +import fs from "node:fs"; |
| 12 | +import path from "node:path"; |
| 13 | +import url from "node:url"; |
| 14 | + |
| 15 | +const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), ".."); |
| 16 | +const TEMPLATE = path.join(ROOT, "template"); |
| 17 | + |
| 18 | +const args = process.argv.slice(2); |
| 19 | +if (args.length < 4) { |
| 20 | + console.error("Usage: npm run new-app -- <slug> <Type> <icon> <description>"); |
| 21 | + console.error('Example: npm run new-app -- contacts AddressBook 📒 "An address book"'); |
| 22 | + process.exit(1); |
| 23 | +} |
| 24 | + |
| 25 | +const [slug, type, icon, description] = args; |
| 26 | + |
| 27 | +if (!/^[a-z][a-z0-9-]*$/.test(slug)) { |
| 28 | + console.error("slug must be lowercase, alphanumeric + dashes (e.g. 'address-book')"); |
| 29 | + process.exit(1); |
| 30 | +} |
| 31 | +if (!/^[A-Z][A-Za-z0-9]*$/.test(type)) { |
| 32 | + console.error("type must be PascalCase (e.g. 'AddressBook')"); |
| 33 | + process.exit(1); |
| 34 | +} |
| 35 | + |
| 36 | +const dest = path.join(ROOT, slug); |
| 37 | +if (fs.existsSync(dest)) { |
| 38 | + console.error(`${slug}/ already exists — refusing to overwrite`); |
| 39 | + process.exit(1); |
| 40 | +} |
| 41 | + |
| 42 | +const today = new Date().toISOString().slice(0, 10); |
| 43 | +const label = type.replace(/([A-Z])/g, ' $1').trim(); |
| 44 | + |
| 45 | +const subst = (s) => s |
| 46 | + .replaceAll("{{SLUG}}", slug) |
| 47 | + .replaceAll("{{TYPE}}", type) |
| 48 | + .replaceAll("{{LABEL}}", label) |
| 49 | + .replaceAll("{{ICON}}", icon) |
| 50 | + .replaceAll("{{DESCRIPTION}}", description) |
| 51 | + .replaceAll("{{TODAY}}", today); |
| 52 | + |
| 53 | +fs.mkdirSync(dest); |
| 54 | + |
| 55 | +const writes = [ |
| 56 | + ["index.html", "index.html"], |
| 57 | + ["app.json", "app.json"], |
| 58 | + ["view-pane.js", `${slug}-view-pane.js`], |
| 59 | +]; |
| 60 | + |
| 61 | +for (const [src, out] of writes) { |
| 62 | + const content = subst(fs.readFileSync(path.join(TEMPLATE, src), "utf8")); |
| 63 | + fs.writeFileSync(path.join(dest, out), content); |
| 64 | + console.log(` wrote ${slug}/${out}`); |
| 65 | +} |
| 66 | + |
| 67 | +console.log(`\n ${slug}/ scaffolded for urn:solid:${type}\n`); |
| 68 | +console.log("Next — make sure these are in place across the 6 repos:"); |
| 69 | +console.log(` 1. urn-solid: Class term ${type}/index.json (+ any new properties)`); |
| 70 | +console.log(` 2. solid-schema: ${type}/index.json (JSON Schema)`); |
| 71 | +console.log(` 3. solid-shapes: npm run build (auto-derives from solid-schema)`); |
| 72 | +console.log(` 4. solid-ui: npm run build (auto-derives from solid-schema)`); |
| 73 | +console.log(` 5. solid-panes: ${type}/index.json manifest`); |
| 74 | +console.log(` 6. solid-apps: edit ${slug}/index.html — replace the sample data`); |
| 75 | +console.log(` then npm run build && commit + push`); |
| 76 | +console.log("\nIf any of 1-5 are missing the Edit/View tabs will not appear.\n"); |
0 commit comments