Skip to content

Commit 0193835

Browse files
add: solid-app-template scaffold + new-app script
Cuts new-app boilerplate from ~30 min to ~2 min. npm run new-app -- contacts AddressBook 📒 "An address book" Generates <slug>/{index.html, app.json, <slug>-view-pane.js} from template/, then prints the 6-repo checklist. Existing apps still hand- written; this is opt-in for the next one. build.js: skip template/ from the apps catalog.
1 parent 1e03630 commit 0193835

6 files changed

Lines changed: 200 additions & 2 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"type": "module",
66
"scripts": {
77
"validate": "node scripts/validate.js",
8-
"build": "node scripts/build.js"
8+
"build": "node scripts/build.js",
9+
"new-app": "node scripts/new-app.js"
910
},
1011
"devDependencies": {
1112
"ajv": "^8.17.0",

scripts/build.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), ".."
77

88
const RESERVED = new Set([
99
"schema", "scripts", "node_modules", ".github", ".git", ".claude",
10-
"assets", "vendor", "spec", "demo"
10+
"assets", "vendor", "spec", "demo", "template"
1111
]);
1212

1313
const isAppDir = (name) => {

scripts/new-app.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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");

template/app.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "{{LABEL}}",
3+
"description": "{{DESCRIPTION}}",
4+
"entry": "./index.html",
5+
"types": ["urn:solid:{{TYPE}}"],
6+
"icon": "{{ICON}}",
7+
"author": "Melvin Carvalho",
8+
"status": "stable",
9+
"added": "{{TODAY}}"
10+
}

template/index.html

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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>{{LABEL}} — solid-apps</title>
7+
<style>
8+
* { margin: 0; box-sizing: border-box; }
9+
body { font-family: 'Inter', -apple-system, sans-serif; background: #fafaf8; }
10+
#losos { max-width: 100% !important; }
11+
#losos > div { max-width: 100% !important; width: 100% !important; }
12+
</style>
13+
</head>
14+
<body>
15+
16+
<!-- Sample data: edit this to fit your type. See the JSON Schema at
17+
https://solid-schema.github.io/{{TYPE}}/index.json for the field list. -->
18+
<script type="application/ld+json">
19+
{
20+
"@context": { "@vocab": "urn:solid:" },
21+
"@id": "#this",
22+
"@type": "{{TYPE}}",
23+
"title": "Edit me"
24+
}
25+
</script>
26+
27+
<!-- Login: Nostr + Solid in one tag. -->
28+
<script src="https://unpkg.com/xlogin"></script>
29+
30+
<!-- Panes — bespoke {{LABEL}} view first, then schema-driven. -->
31+
<script type="module" data-pane src="./{{SLUG}}-view-pane.js"></script>
32+
<script type="module" data-pane src="https://solid-ui.github.io/ui-pane.js"></script>
33+
<script type="module" data-pane src="https://losos.org/panes/schema-pane.js"></script>
34+
<script type="module" data-pane src="https://solid-panes.github.io/schema-view.js"></script>
35+
<script type="module" data-pane src="https://losos.org/panes/source-pane.js"></script>
36+
37+
<div id="losos"></div>
38+
39+
<!-- Boot: autoSchema patches $schema, then shell -->
40+
<script type="module">
41+
import { autoSchema } from 'https://solid-panes.github.io/auto-schema.js'
42+
const result = await autoSchema()
43+
console.log('[{{SLUG}}] autoSchema patched', result.patched, 'island(s),', result.skipped, 'skipped')
44+
await import('https://losos.org/losos/shell.js')
45+
</script>
46+
47+
</body>
48+
</html>

template/view-pane.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* {{LABEL}} view pane — bespoke renderer for urn:solid:{{TYPE}}.
3+
*
4+
* Reads the JSON-LD data island and renders a custom view. Drop in
5+
* alongside ui-pane (Inline) and schema-pane (Edit) — LOSOS shows them
6+
* all as tabs.
7+
*
8+
* AGPL-3.0 — part of solid-apps
9+
*/
10+
11+
import { html, render } from 'https://losos.org/losos/html.js'
12+
13+
export default {
14+
label: '{{LABEL}}',
15+
icon: '{{ICON}}',
16+
17+
canHandle(subject, store) {
18+
const node = store.get(subject.value)
19+
if (!node) return false
20+
const t = store.type(node)
21+
if (!t) return false
22+
return Array.isArray(t)
23+
? t.some(x => /{{TYPE}}/i.test(x))
24+
: /{{TYPE}}/i.test(t)
25+
},
26+
27+
render(subject, lionStore, container, rawData) {
28+
let data = rawData
29+
if (!data) {
30+
const dataEl = document.querySelector('script[type="application/ld+json"]')
31+
try { data = JSON.parse(dataEl.textContent) } catch { return }
32+
}
33+
34+
const title = data.title || data.name || data['@id'] || '{{LABEL}}'
35+
36+
render(container, html`
37+
<style>
38+
.v-bg {
39+
min-height: 100vh;
40+
background: linear-gradient(180deg, #fafaf8 0%, #f0efeb 100%);
41+
padding: 56px 24px 96px;
42+
}
43+
.v-card {
44+
max-width: 640px; margin: 0 auto;
45+
background: #fff;
46+
border: 1px solid #e5e3de;
47+
border-radius: 16px;
48+
padding: 40px 36px;
49+
}
50+
.v-title {
51+
font: 500 28px/1.2 Georgia, serif;
52+
color: #1a1a1a; margin: 0;
53+
}
54+
</style>
55+
<div class="v-bg">
56+
<div class="v-card">
57+
<h1 class="v-title">${title}</h1>
58+
<!-- TODO: add fields from your data here -->
59+
</div>
60+
</div>
61+
`)
62+
}
63+
}

0 commit comments

Comments
 (0)