@loadbare/app 0.8.0 → 0.8.1
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/dist/build/skills-cli.d.ts +12 -0
- package/dist/build/skills-cli.d.ts.map +1 -0
- package/dist/build/skills-cli.js +81 -0
- package/dist/build/skills-cli.js.map +1 -0
- package/dist/build/skills.d.ts +47 -0
- package/dist/build/skills.d.ts.map +1 -0
- package/dist/build/skills.js +124 -0
- package/dist/build/skills.js.map +1 -0
- package/docs/reference/widgets.md +2 -2
- package/package.json +8 -4
- package/skills/loadbare-app/SKILL.md +258 -0
- package/skills/loadbare-app/references/TECHREF-1.0.md +1189 -0
- package/skills/loadbare-app/references/builder.md +134 -0
- package/skills/loadbare-app/references/chrome.md +158 -0
- package/skills/loadbare-app/references/css.md +44 -0
- package/skills/loadbare-app/references/custom-elements.md +397 -0
- package/skills/loadbare-app/references/data-binding.md +457 -0
- package/skills/loadbare-app/references/overview.md +38 -0
- package/skills/loadbare-app/references/page-files.md +194 -0
- package/skills/loadbare-app/references/server.md +142 -0
- package/skills/loadbare-app/references/widgets.md +174 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* loadbare-app — commands that are not the build.
|
|
4
|
+
*
|
|
5
|
+
* loadbare-app skills install [--copy] [--dir <path>]
|
|
6
|
+
*
|
|
7
|
+
* Installs the agent skills that ship in this package into `.agents/skills`
|
|
8
|
+
* and `.claude/skills`, or into `--dir` alone — see build/skills.ts. Nothing
|
|
9
|
+
* goes to stdout; every line is progress, on stderr.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=skills-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills-cli.d.ts","sourceRoot":"","sources":["../../build/skills-cli.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* loadbare-app — commands that are not the build.
|
|
4
|
+
*
|
|
5
|
+
* loadbare-app skills install [--copy] [--dir <path>]
|
|
6
|
+
*
|
|
7
|
+
* Installs the agent skills that ship in this package into `.agents/skills`
|
|
8
|
+
* and `.claude/skills`, or into `--dir` alone — see build/skills.ts. Nothing
|
|
9
|
+
* goes to stdout; every line is progress, on stderr.
|
|
10
|
+
*/
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { findPackageRoot } from "./package-root.js";
|
|
15
|
+
import { installSkills, SKILL_DESTINATIONS } from "./skills.js";
|
|
16
|
+
const USAGE = "usage: loadbare-app skills install [--copy] [--dir <path>]";
|
|
17
|
+
async function main() {
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = parseArgs({
|
|
21
|
+
allowPositionals: true,
|
|
22
|
+
options: {
|
|
23
|
+
copy: { type: "boolean", default: false },
|
|
24
|
+
dir: { type: "string" },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.error(`loadbare-app: ${err.message}`);
|
|
30
|
+
console.error(USAGE);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const { values, positionals } = parsed;
|
|
34
|
+
if (positionals.join(" ") !== "skills install") {
|
|
35
|
+
console.error(USAGE);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
// Source and compiled output sit at different depths, so the package is
|
|
39
|
+
// found by walking up rather than by counting.
|
|
40
|
+
const packageRoot = await findPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
41
|
+
const source = path.join(packageRoot, "skills");
|
|
42
|
+
const destinations = values.dir ? [values.dir] : SKILL_DESTINATIONS;
|
|
43
|
+
const result = installSkills({
|
|
44
|
+
source,
|
|
45
|
+
destinations,
|
|
46
|
+
copy: values.copy,
|
|
47
|
+
cwd: process.cwd(),
|
|
48
|
+
log: (line) => console.error(line),
|
|
49
|
+
});
|
|
50
|
+
// An install with no skills in it is a packaging fault, not a user error,
|
|
51
|
+
// and saying where it looked is what makes that diagnosable.
|
|
52
|
+
if (result.names.length === 0) {
|
|
53
|
+
console.error(`loadbare-app: this install of @loadbare/app carries no skills (looked in ${source})`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
console.error("");
|
|
57
|
+
if (result.copiedInstead) {
|
|
58
|
+
console.error("Some links could not be created and those skills were copied instead.");
|
|
59
|
+
console.error("A copy does not follow the package when it is upgraded — rerun this");
|
|
60
|
+
console.error("command after an upgrade, or use --copy everywhere and commit the result.");
|
|
61
|
+
console.error("");
|
|
62
|
+
}
|
|
63
|
+
if (values.copy) {
|
|
64
|
+
console.error("These are copies. Commit them, and rerun this command after upgrading");
|
|
65
|
+
console.error("@loadbare/app so the skill matches the framework.");
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.error("These are links into node_modules. To keep them out of git:");
|
|
69
|
+
console.error("");
|
|
70
|
+
for (const destination of destinations) {
|
|
71
|
+
for (const name of result.names) {
|
|
72
|
+
console.error(` ${path.join(destination, name)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
console.error("");
|
|
77
|
+
console.error("Start your agent from this directory and it will find them.");
|
|
78
|
+
return result.refused > 0 ? 1 : 0;
|
|
79
|
+
}
|
|
80
|
+
process.exitCode = await main();
|
|
81
|
+
//# sourceMappingURL=skills-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills-cli.js","sourceRoot":"","sources":["../../build/skills-cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * loadbare-app — commands that are not the build.\n *\n * loadbare-app skills install [--copy] [--dir <path>]\n *\n * Installs the agent skills that ship in this package into `.agents/skills`\n * and `.claude/skills`, or into `--dir` alone — see build/skills.ts. Nothing\n * goes to stdout; every line is progress, on stderr.\n */\n\nimport { parseArgs } from \"node:util\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { findPackageRoot } from \"./package-root.js\";\nimport { installSkills, SKILL_DESTINATIONS } from \"./skills.js\";\n\nconst USAGE = \"usage: loadbare-app skills install [--copy] [--dir <path>]\";\n\nasync function main(): Promise<number> {\n let parsed;\n try {\n parsed = parseArgs({\n allowPositionals: true,\n options: {\n copy: { type: \"boolean\", default: false },\n dir: { type: \"string\" },\n },\n });\n } catch (err) {\n console.error(`loadbare-app: ${(err as Error).message}`);\n console.error(USAGE);\n return 1;\n }\n\n const { values, positionals } = parsed;\n if (positionals.join(\" \") !== \"skills install\") {\n console.error(USAGE);\n return 1;\n }\n\n // Source and compiled output sit at different depths, so the package is\n // found by walking up rather than by counting.\n const packageRoot = await findPackageRoot(\n path.dirname(fileURLToPath(import.meta.url)),\n );\n const source = path.join(packageRoot, \"skills\");\n const destinations = values.dir ? [values.dir] : SKILL_DESTINATIONS;\n\n const result = installSkills({\n source,\n destinations,\n copy: values.copy!,\n cwd: process.cwd(),\n log: (line) => console.error(line),\n });\n\n // An install with no skills in it is a packaging fault, not a user error,\n // and saying where it looked is what makes that diagnosable.\n if (result.names.length === 0) {\n console.error(\n `loadbare-app: this install of @loadbare/app carries no skills (looked in ${source})`,\n );\n return 1;\n }\n\n console.error(\"\");\n if (result.copiedInstead) {\n console.error(\n \"Some links could not be created and those skills were copied instead.\",\n );\n console.error(\n \"A copy does not follow the package when it is upgraded — rerun this\",\n );\n console.error(\n \"command after an upgrade, or use --copy everywhere and commit the result.\",\n );\n console.error(\"\");\n }\n\n if (values.copy) {\n console.error(\n \"These are copies. Commit them, and rerun this command after upgrading\",\n );\n console.error(\"@loadbare/app so the skill matches the framework.\");\n } else {\n console.error(\n \"These are links into node_modules. To keep them out of git:\",\n );\n console.error(\"\");\n for (const destination of destinations) {\n for (const name of result.names) {\n console.error(` ${path.join(destination, name)}`);\n }\n }\n }\n\n console.error(\"\");\n console.error(\"Start your agent from this directory and it will find them.\");\n\n return result.refused > 0 ? 1 : 0;\n}\n\nprocess.exitCode = await main();\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Put the agent skills that ship in this package where a coding agent will
|
|
3
|
+
* find them.
|
|
4
|
+
*
|
|
5
|
+
* The same command as `loadbare-db skills install`, and deliberately a copy
|
|
6
|
+
* of it rather than a shared module: `@loadbare/db` shares no code with this
|
|
7
|
+
* package, and this package imports nothing from the repository.
|
|
8
|
+
*
|
|
9
|
+
* A symlink by default, because the point is that the skill tracks the
|
|
10
|
+
* dependency: `npm install @loadbare/app@next` and the skill moves with it.
|
|
11
|
+
* `copy` is for a team that would rather commit the text than have every
|
|
12
|
+
* clone depend on `node_modules` being populated.
|
|
13
|
+
*
|
|
14
|
+
* Nothing is done to `.gitignore`. Whether these paths are committed is a
|
|
15
|
+
* decision about the project, and the command that installs a file is not the
|
|
16
|
+
* one to make it.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Where a coding agent looks for skills.
|
|
20
|
+
*
|
|
21
|
+
* `.agents/skills` is the vendor-neutral location, read by Codex, Cursor and
|
|
22
|
+
* Copilot; `.claude/skills` is Claude Code's, and Cursor reads it too. Both
|
|
23
|
+
* are written rather than guessing from what happens to be configured.
|
|
24
|
+
*/
|
|
25
|
+
export declare const SKILL_DESTINATIONS: string[];
|
|
26
|
+
export interface InstallOptions {
|
|
27
|
+
/** The directory holding one subdirectory per skill. */
|
|
28
|
+
source: string;
|
|
29
|
+
/** The directories to install into. */
|
|
30
|
+
destinations: string[];
|
|
31
|
+
/** Copy rather than link. */
|
|
32
|
+
copy: boolean;
|
|
33
|
+
/** The project root; a link to a package inside it is relative. */
|
|
34
|
+
cwd: string;
|
|
35
|
+
/** Where progress goes, one line at a time. */
|
|
36
|
+
log: (line: string) => void;
|
|
37
|
+
}
|
|
38
|
+
export interface InstallResult {
|
|
39
|
+
names: string[];
|
|
40
|
+
refused: number;
|
|
41
|
+
/** A link was refused by the platform and a copy made in its place. */
|
|
42
|
+
copiedInstead: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** The skills under `source`: each directory holding a `SKILL.md`. */
|
|
45
|
+
export declare function shippedSkills(source: string): string[];
|
|
46
|
+
export declare function installSkills(options: InstallOptions): InstallResult;
|
|
47
|
+
//# sourceMappingURL=skills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../build/skills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAcH;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,UAAuC,CAAC;AAEvE,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6BAA6B;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,aAAa,EAAE,OAAO,CAAC;CACxB;AAwCD,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAUtD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,aAAa,CAsDpE"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Put the agent skills that ship in this package where a coding agent will
|
|
3
|
+
* find them.
|
|
4
|
+
*
|
|
5
|
+
* The same command as `loadbare-db skills install`, and deliberately a copy
|
|
6
|
+
* of it rather than a shared module: `@loadbare/db` shares no code with this
|
|
7
|
+
* package, and this package imports nothing from the repository.
|
|
8
|
+
*
|
|
9
|
+
* A symlink by default, because the point is that the skill tracks the
|
|
10
|
+
* dependency: `npm install @loadbare/app@next` and the skill moves with it.
|
|
11
|
+
* `copy` is for a team that would rather commit the text than have every
|
|
12
|
+
* clone depend on `node_modules` being populated.
|
|
13
|
+
*
|
|
14
|
+
* Nothing is done to `.gitignore`. Whether these paths are committed is a
|
|
15
|
+
* decision about the project, and the command that installs a file is not the
|
|
16
|
+
* one to make it.
|
|
17
|
+
*/
|
|
18
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, symlinkSync, } from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
/**
|
|
21
|
+
* Where a coding agent looks for skills.
|
|
22
|
+
*
|
|
23
|
+
* `.agents/skills` is the vendor-neutral location, read by Codex, Cursor and
|
|
24
|
+
* Copilot; `.claude/skills` is Claude Code's, and Cursor reads it too. Both
|
|
25
|
+
* are written rather than guessing from what happens to be configured.
|
|
26
|
+
*/
|
|
27
|
+
export const SKILL_DESTINATIONS = [".agents/skills", ".claude/skills"];
|
|
28
|
+
/** `absent` covers a dangling symlink, which `existsSync` reports as missing. */
|
|
29
|
+
function entryKind(p) {
|
|
30
|
+
let stat;
|
|
31
|
+
try {
|
|
32
|
+
stat = lstatSync(p);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return "absent";
|
|
36
|
+
}
|
|
37
|
+
if (stat.isSymbolicLink())
|
|
38
|
+
return "symlink";
|
|
39
|
+
if (stat.isDirectory())
|
|
40
|
+
return "directory";
|
|
41
|
+
return "other";
|
|
42
|
+
}
|
|
43
|
+
/** The `name:` a skill directory declares, or null if there is no skill there. */
|
|
44
|
+
function declaredSkillName(dir) {
|
|
45
|
+
try {
|
|
46
|
+
const match = /^name:\s*(\S+)\s*$/m.exec(readFileSync(path.join(dir, "SKILL.md"), "utf-8"));
|
|
47
|
+
return match?.[1] ?? null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A link into `node_modules` is relative, so the project stays movable and the
|
|
55
|
+
* link means the same thing to everyone who checks it out. A globally
|
|
56
|
+
* installed package gets an absolute path, since a relative one breaks the
|
|
57
|
+
* moment the project moves.
|
|
58
|
+
*/
|
|
59
|
+
function linkTarget(from, to, cwd) {
|
|
60
|
+
const inside = path.resolve(from).startsWith(path.resolve(cwd) + path.sep);
|
|
61
|
+
return inside
|
|
62
|
+
? path.relative(path.dirname(path.resolve(cwd, to)), path.resolve(from))
|
|
63
|
+
: path.resolve(from);
|
|
64
|
+
}
|
|
65
|
+
/** The skills under `source`: each directory holding a `SKILL.md`. */
|
|
66
|
+
export function shippedSkills(source) {
|
|
67
|
+
if (!existsSync(source))
|
|
68
|
+
return [];
|
|
69
|
+
return readdirSync(source, { withFileTypes: true })
|
|
70
|
+
.filter((entry) => entry.isDirectory() &&
|
|
71
|
+
existsSync(path.join(source, entry.name, "SKILL.md")))
|
|
72
|
+
.map((entry) => entry.name)
|
|
73
|
+
.sort();
|
|
74
|
+
}
|
|
75
|
+
export function installSkills(options) {
|
|
76
|
+
const { source, destinations, copy, cwd, log } = options;
|
|
77
|
+
const names = shippedSkills(source);
|
|
78
|
+
let refused = 0;
|
|
79
|
+
let copiedInstead = false;
|
|
80
|
+
for (const destination of destinations) {
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
const from = path.join(source, name);
|
|
83
|
+
const to = path.join(destination, name);
|
|
84
|
+
const at = path.resolve(cwd, to);
|
|
85
|
+
const kind = entryKind(at);
|
|
86
|
+
// A directory this command wrote says so on its own front matter, and
|
|
87
|
+
// is replaced. Anything else at that path belongs to someone, and a
|
|
88
|
+
// skill install is not permitted to be the thing that lost it.
|
|
89
|
+
if (kind === "directory" && declaredSkillName(at) !== name) {
|
|
90
|
+
log(` refused ${to} — a directory is already there and is not the ${name} skill`);
|
|
91
|
+
refused++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (kind === "other") {
|
|
95
|
+
log(` refused ${to} — a file is already there`);
|
|
96
|
+
refused++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
mkdirSync(path.resolve(cwd, destination), { recursive: true });
|
|
100
|
+
rmSync(at, { recursive: true, force: true });
|
|
101
|
+
if (copy) {
|
|
102
|
+
cpSync(from, at, { recursive: true });
|
|
103
|
+
log(` copied ${to}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
symlinkSync(linkTarget(from, to, cwd), at, "dir");
|
|
108
|
+
log(` linked ${to}`);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
// Windows refuses a symlink to an unprivileged process unless
|
|
112
|
+
// developer mode is on. A copy is the same skill, so the install
|
|
113
|
+
// succeeds and the difference is reported rather than raised.
|
|
114
|
+
if (error.code !== "EPERM")
|
|
115
|
+
throw error;
|
|
116
|
+
cpSync(from, at, { recursive: true });
|
|
117
|
+
copiedInstead = true;
|
|
118
|
+
log(` copied ${to}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { names, refused, copiedInstead };
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.js","sourceRoot":"","sources":["../../build/skills.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Put the agent skills that ship in this package where a coding agent will\n * find them.\n *\n * The same command as `loadbare-db skills install`, and deliberately a copy\n * of it rather than a shared module: `@loadbare/db` shares no code with this\n * package, and this package imports nothing from the repository.\n *\n * A symlink by default, because the point is that the skill tracks the\n * dependency: `npm install @loadbare/app@next` and the skill moves with it.\n * `copy` is for a team that would rather commit the text than have every\n * clone depend on `node_modules` being populated.\n *\n * Nothing is done to `.gitignore`. Whether these paths are committed is a\n * decision about the project, and the command that installs a file is not the\n * one to make it.\n */\n\nimport {\n cpSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n rmSync,\n symlinkSync,\n} from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * Where a coding agent looks for skills.\n *\n * `.agents/skills` is the vendor-neutral location, read by Codex, Cursor and\n * Copilot; `.claude/skills` is Claude Code's, and Cursor reads it too. Both\n * are written rather than guessing from what happens to be configured.\n */\nexport const SKILL_DESTINATIONS = [\".agents/skills\", \".claude/skills\"];\n\nexport interface InstallOptions {\n /** The directory holding one subdirectory per skill. */\n source: string;\n /** The directories to install into. */\n destinations: string[];\n /** Copy rather than link. */\n copy: boolean;\n /** The project root; a link to a package inside it is relative. */\n cwd: string;\n /** Where progress goes, one line at a time. */\n log: (line: string) => void;\n}\n\nexport interface InstallResult {\n names: string[];\n refused: number;\n /** A link was refused by the platform and a copy made in its place. */\n copiedInstead: boolean;\n}\n\n/** `absent` covers a dangling symlink, which `existsSync` reports as missing. */\nfunction entryKind(p: string): \"absent\" | \"symlink\" | \"directory\" | \"other\" {\n let stat;\n try {\n stat = lstatSync(p);\n } catch {\n return \"absent\";\n }\n if (stat.isSymbolicLink()) return \"symlink\";\n if (stat.isDirectory()) return \"directory\";\n return \"other\";\n}\n\n/** The `name:` a skill directory declares, or null if there is no skill there. */\nfunction declaredSkillName(dir: string): string | null {\n try {\n const match = /^name:\\s*(\\S+)\\s*$/m.exec(\n readFileSync(path.join(dir, \"SKILL.md\"), \"utf-8\"),\n );\n return match?.[1] ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * A link into `node_modules` is relative, so the project stays movable and the\n * link means the same thing to everyone who checks it out. A globally\n * installed package gets an absolute path, since a relative one breaks the\n * moment the project moves.\n */\nfunction linkTarget(from: string, to: string, cwd: string): string {\n const inside = path.resolve(from).startsWith(path.resolve(cwd) + path.sep);\n return inside\n ? path.relative(path.dirname(path.resolve(cwd, to)), path.resolve(from))\n : path.resolve(from);\n}\n\n/** The skills under `source`: each directory holding a `SKILL.md`. */\nexport function shippedSkills(source: string): string[] {\n if (!existsSync(source)) return [];\n return readdirSync(source, { withFileTypes: true })\n .filter(\n (entry) =>\n entry.isDirectory() &&\n existsSync(path.join(source, entry.name, \"SKILL.md\")),\n )\n .map((entry) => entry.name)\n .sort();\n}\n\nexport function installSkills(options: InstallOptions): InstallResult {\n const { source, destinations, copy, cwd, log } = options;\n const names = shippedSkills(source);\n let refused = 0;\n let copiedInstead = false;\n\n for (const destination of destinations) {\n for (const name of names) {\n const from = path.join(source, name);\n const to = path.join(destination, name);\n const at = path.resolve(cwd, to);\n const kind = entryKind(at);\n\n // A directory this command wrote says so on its own front matter, and\n // is replaced. Anything else at that path belongs to someone, and a\n // skill install is not permitted to be the thing that lost it.\n if (kind === \"directory\" && declaredSkillName(at) !== name) {\n log(\n ` refused ${to} — a directory is already there and is not the ${name} skill`,\n );\n refused++;\n continue;\n }\n if (kind === \"other\") {\n log(` refused ${to} — a file is already there`);\n refused++;\n continue;\n }\n\n mkdirSync(path.resolve(cwd, destination), { recursive: true });\n rmSync(at, { recursive: true, force: true });\n\n if (copy) {\n cpSync(from, at, { recursive: true });\n log(` copied ${to}`);\n continue;\n }\n\n try {\n symlinkSync(linkTarget(from, to, cwd), at, \"dir\");\n log(` linked ${to}`);\n } catch (error) {\n // Windows refuses a symlink to an unprivileged process unless\n // developer mode is on. A copy is the same skill, so the install\n // succeeds and the difference is reported rather than raised.\n if ((error as NodeJS.ErrnoException).code !== \"EPERM\") throw error;\n cpSync(from, at, { recursive: true });\n copiedInstead = true;\n log(` copied ${to}`);\n }\n }\n }\n\n return { names, refused, copiedInstead };\n}\n"]}
|
|
@@ -19,8 +19,8 @@ npm install @loadbare/widgets
|
|
|
19
19
|
export default ["@loadbare/widgets"];
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
See [
|
|
23
|
-
|
|
22
|
+
See [Widgets from packages](./builder.md#widgets-from-packages) for what
|
|
23
|
+
listing a package does, and [The Builder](./builder.md#where-the-builder-looks)
|
|
24
24
|
for where a listed package sits in the cascade.
|
|
25
25
|
|
|
26
26
|
## `lb-input`
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loadbare/app",
|
|
3
3
|
"description": "High performance web app framework for server-bound applications",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
8
8
|
"docs",
|
|
9
|
+
"skills/loadbare-app/SKILL.md",
|
|
10
|
+
"skills/loadbare-app/references",
|
|
9
11
|
"README.md"
|
|
10
12
|
],
|
|
11
13
|
"publishConfig": {
|
|
12
14
|
"access": "public"
|
|
13
15
|
},
|
|
14
16
|
"bin": {
|
|
15
|
-
"loadbare-app-build": "dist/build/cli.js"
|
|
17
|
+
"loadbare-app-build": "dist/build/cli.js",
|
|
18
|
+
"loadbare-app": "dist/build/skills-cli.js"
|
|
16
19
|
},
|
|
17
20
|
"exports": {
|
|
18
21
|
".": "./dist/hub/lb-hub.browser.js",
|
|
@@ -31,8 +34,9 @@
|
|
|
31
34
|
"release:major": "node ../../scripts/bump-release.mjs major",
|
|
32
35
|
"prebuild": "node --eval \"fs.rmSync('dist',{recursive:true,force:true})\" --input-type=module",
|
|
33
36
|
"build:server": "tsc --project tsconfig.build.json",
|
|
34
|
-
"build": "
|
|
35
|
-
"
|
|
37
|
+
"build:skills": "node scripts/build-skills.mjs",
|
|
38
|
+
"build": "npm run build:server && npm run build:skills",
|
|
39
|
+
"postbuild": "node --eval \"for (const f of ['dist/build/cli.js','dist/build/skills-cli.js']) fs.chmodSync(f, 0o755)\" --input-type=module && node ../../scripts/check-dist-imports.mjs",
|
|
36
40
|
"test": "tsx --test \"tests/**/*.test.ts\"",
|
|
37
41
|
"typecheck": "tsc --noEmit",
|
|
38
42
|
"format": "prettier --write .",
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loadbare-app
|
|
3
|
+
description: Build server-bound web applications with Loadbare/app. Covers the file conventions the builder finds by name (`chrome.html`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `imports.ts`, `<tag>.html`, `<tag>.browser.ts`); the `lb-*` attribute vocabulary that binds HTML to server data and sends requests (`lb-list`, `lb-row`, `lb-cell`, `lb-key`, `lb-show`, `lb-action`); build-time widget expansion with `exp-*` parameters, `lb-slot` and `lb-template`; custom element code; the Express wiring with `hubRoutes`; and the `@loadbare/widgets` library. Use whenever a task involves `@loadbare/app`, `@loadbare/widgets`, `loadbare-app-build`, an `lb-` attribute, or a page, query, request or widget file in a Loadbare application. The model is not React, htmx or REST, and cannot be inferred from them.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
package: "@loadbare/app"
|
|
7
|
+
homepage: https://gitlab.com/kendowns/loadbare
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Working with Loadbare/app
|
|
11
|
+
|
|
12
|
+
Loadbare/app builds a whole application into one HTML document, one client
|
|
13
|
+
script and one stylesheet. The server answers named queries with rows and
|
|
14
|
+
performs named requests; the browser lands those rows on elements that name
|
|
15
|
+
them. No component renders anything, and no page fetches anything.
|
|
16
|
+
|
|
17
|
+
The wire is relational. A name answers with one row or a set of rows, a
|
|
18
|
+
row holds cells, and a cell holds one value. Hold on to that: most wrong
|
|
19
|
+
designs come from sending a shape a relational answer cannot have.
|
|
20
|
+
|
|
21
|
+
## Build every change before handing it over
|
|
22
|
+
|
|
23
|
+
`loadbare-app-build` is the checker. It expands every page and widget and
|
|
24
|
+
refuses what it cannot ship, naming the tag and the file:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
loadbare-app-build --src src --out dist
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
It exits non-zero on the first failure and prints
|
|
31
|
+
`loadbare-app-build: <message>`. A clean run prints the files it wrote and
|
|
32
|
+
the number of custom elements it bundled.
|
|
33
|
+
|
|
34
|
+
Type-check the server side as well:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
tsc --noEmit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`dist/pages.ts` imports page files with their `.ts` extensions, so the
|
|
41
|
+
application's `tsconfig.json` needs `allowImportingTsExtensions`.
|
|
42
|
+
|
|
43
|
+
**Write, build, fix, repeat until it is clean.** Then start the server:
|
|
44
|
+
`createHub` refuses a query that answers with the wrong shape and a page
|
|
45
|
+
that declares a reserved name, and those refusals only appear at run time.
|
|
46
|
+
Restart the server after adding or changing a `.queries.ts` or
|
|
47
|
+
`.requests.ts` file.
|
|
48
|
+
|
|
49
|
+
## The shape of an application
|
|
50
|
+
|
|
51
|
+
The builder classifies files by name, never by directory:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
src/
|
|
55
|
+
chrome.html exactly one; the document every page lands in
|
|
56
|
+
imports.ts at most one; the widget packages to scan
|
|
57
|
+
00-reset.css any .css anywhere, concatenated
|
|
58
|
+
pages/
|
|
59
|
+
index.page.html the page for /
|
|
60
|
+
members.page.html the page for /members
|
|
61
|
+
members.queries.ts what members displays
|
|
62
|
+
members.requests.ts what members may be asked to do
|
|
63
|
+
widgets/
|
|
64
|
+
note-card.html the markup <note-card> expands into
|
|
65
|
+
visit-count.browser.ts the class <visit-count> registers
|
|
66
|
+
server.ts
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The chrome carries `<lb-hub>` inside `<body>`, an empty `<main>` inside the
|
|
70
|
+
hub, and `<script src="/client.js" defer>`. Everything a user touches sits
|
|
71
|
+
inside `<lb-hub>`.
|
|
72
|
+
|
|
73
|
+
A page is an HTML fragment that lands in `<main>`:
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<!-- src/pages/members.page.html -->
|
|
77
|
+
<p lb-row="dues">Collected this year: <span lb-cell="total"></span></p>
|
|
78
|
+
|
|
79
|
+
<section lb-list="roster">
|
|
80
|
+
<form lb-action="lb-row-insert">
|
|
81
|
+
<input lb-cell="name" />
|
|
82
|
+
<button type="submit">Add</button>
|
|
83
|
+
</form>
|
|
84
|
+
<ul>
|
|
85
|
+
<template lb-key="id">
|
|
86
|
+
<li>
|
|
87
|
+
<span lb-cell="name"></span>
|
|
88
|
+
<button lb-action="lb-row-delete">Remove</button>
|
|
89
|
+
</li>
|
|
90
|
+
</template>
|
|
91
|
+
</ul>
|
|
92
|
+
</section>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// src/pages/members.queries.ts
|
|
97
|
+
import { list, row, type Queries } from "@loadbare/app/server";
|
|
98
|
+
|
|
99
|
+
export const queries: Queries = {
|
|
100
|
+
dues: row((ctx) => ctx.db.duesTotal()),
|
|
101
|
+
roster: list((ctx) => ctx.db.members()),
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// src/pages/members.requests.ts
|
|
107
|
+
import { patch, type Requests } from "@loadbare/app/server";
|
|
108
|
+
|
|
109
|
+
export const requests: Requests = {
|
|
110
|
+
crud: {
|
|
111
|
+
roster: {
|
|
112
|
+
rowInsert: {
|
|
113
|
+
run: async (ctx, { values }) => ({
|
|
114
|
+
roster: patch({ rows: [await ctx.db.addMember(values)] }),
|
|
115
|
+
}),
|
|
116
|
+
refresh: ["dues"],
|
|
117
|
+
},
|
|
118
|
+
rowDelete: {
|
|
119
|
+
run: async (ctx, { key }) => {
|
|
120
|
+
await ctx.db.removeMember(key);
|
|
121
|
+
return { roster: patch({ drop: [key] }) };
|
|
122
|
+
},
|
|
123
|
+
refresh: ["dues"],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Where front-end knowledge misleads
|
|
131
|
+
|
|
132
|
+
Re-read this list when a build fails or a design will not fit. Each item is
|
|
133
|
+
a place a reasonable instinct from React, Vue, htmx or REST produces an
|
|
134
|
+
error or a dead end.
|
|
135
|
+
|
|
136
|
+
**There is no template language.** No `{#if}`, no `v-for`, no `map()`. A
|
|
137
|
+
list is an element carrying `lb-list` with a `<template lb-key="...">`
|
|
138
|
+
inside it, and the hub clones the template once per row. A condition is
|
|
139
|
+
`lb-show="<column>"`: write every possibility into the page and let a column
|
|
140
|
+
decide which is present. The column must be a boolean or null; the string
|
|
141
|
+
`"false"` counts as present.
|
|
142
|
+
|
|
143
|
+
**`{{placeholder}}` is build time only.** It reads an `exp-` attribute on
|
|
144
|
+
the tag, never data. Write it as an entire attribute value or an entire
|
|
145
|
+
text node. Name parameters in lowercase with hyphens: HTML lowercases
|
|
146
|
+
`exp-inputClass` before expansion sees it.
|
|
147
|
+
|
|
148
|
+
**Scope comes from ancestry, not props.** An `lb-cell` binds to the row on
|
|
149
|
+
its nearest ancestor carrying `lb-row` or a live list row. An element
|
|
150
|
+
outside every scope displays nothing. A value of `lb-cell` is a column name.
|
|
151
|
+
|
|
152
|
+
**One name, one cardinality.** Declare every query with `row()` or
|
|
153
|
+
`list()`. A page that needs the same data as a row and as a set declares
|
|
154
|
+
two queries.
|
|
155
|
+
|
|
156
|
+
**A cell never holds rows.** Master-detail is a row and a list under two
|
|
157
|
+
names. Many masters with their details is one list of joined rows, grouped
|
|
158
|
+
for display by a widget's `lbPlaceRow`. A list nested inside another list's
|
|
159
|
+
rows receives the same rows in every outer row; use it for a picker, never
|
|
160
|
+
for per-row detail.
|
|
161
|
+
|
|
162
|
+
**A URL names a page, never a resource.** Do not design `/accounts/42`. A
|
|
163
|
+
row is addressed by `list` and `key`, taken from where the element sits.
|
|
164
|
+
Queries take no arguments from the browser; which record a page shows is
|
|
165
|
+
server state reached through `ctx`.
|
|
166
|
+
|
|
167
|
+
**There are no endpoints to write.** `hubRoutes(hub, contextFor)` is the
|
|
168
|
+
whole data channel. Declare every action under `actions` and every
|
|
169
|
+
permitted operation under `crud`; anything undeclared is refused.
|
|
170
|
+
|
|
171
|
+
**All three CRUD operations are list operations.** Each needs a key, and a
|
|
172
|
+
key exists only on a live row inside `lb-list`. An `lb-row` scope is
|
|
173
|
+
read-only; give a writable single row a list that answers with one row.
|
|
174
|
+
|
|
175
|
+
**Write `rowUpdate` as a partial update.** A form sends every cell it
|
|
176
|
+
holds; a widget carrying `lb-cell` and `lb-action="lb-row-update"` sends its
|
|
177
|
+
one cell. Set the columns `values` names, leave the rest alone, and check
|
|
178
|
+
the names against the columns the page may edit.
|
|
179
|
+
|
|
180
|
+
**Put `lb-row-insert` and `lb-row-update` on a `<form>` or a button.** The
|
|
181
|
+
hub gathers the nearest `<form>`, `<tr>` or live row. Cells in a bare
|
|
182
|
+
`<div>` belong to no row and the request is refused. A native `<input>`
|
|
183
|
+
carrying `lb-row-update` is refused; commit one cell with a widget such as
|
|
184
|
+
`<lb-input>`. Inside a form, leave `lb-action` off the widgets.
|
|
185
|
+
|
|
186
|
+
**Return a patch when the change has a known extent.** `patch({ rows })`
|
|
187
|
+
for rows added or edited, `patch({ drop })` for keys removed, with
|
|
188
|
+
`refresh: []`. List a query in `refresh` only when its membership or order
|
|
189
|
+
changed in a way the operation cannot name.
|
|
190
|
+
|
|
191
|
+
**Format values in the query.** The hub does no type conversion. What a
|
|
192
|
+
number, date or null looks like is decided on the server.
|
|
193
|
+
|
|
194
|
+
**Checkboxes, radio buttons and file inputs are not bound.** They receive
|
|
195
|
+
no value and are not gathered. This is an open item, not a mistake in the
|
|
196
|
+
page.
|
|
197
|
+
|
|
198
|
+
**`lb-` belongs to Loadbare.** Invent no `lb-` attribute, and name no
|
|
199
|
+
query or action with the prefix. Import attribute names in widget code from
|
|
200
|
+
`@loadbare/app/constants`; never write them as string literals.
|
|
201
|
+
|
|
202
|
+
**Name a widget script `<tag>.browser.ts`.** A plain `<tag>.ts` stays on
|
|
203
|
+
the server and the tag goes unregistered.
|
|
204
|
+
|
|
205
|
+
**Light DOM, global CSS.** No shadow root, no scoping. Style empty lists
|
|
206
|
+
with `[lb-row-count="0"]`, pending requests with `[lb-pending]`, and failed
|
|
207
|
+
ones with `[lb-error]`.
|
|
208
|
+
|
|
209
|
+
**Host at the origin root.** The hub reaches its route by absolute path, so
|
|
210
|
+
a subpath such as `example.com/myapp/` does not work.
|
|
211
|
+
|
|
212
|
+
## Widgets
|
|
213
|
+
|
|
214
|
+
A widget is `<tag>.html`, `<tag>.browser.ts`, or both, and a tag with
|
|
215
|
+
neither is a build error. The HTML is expanded at build time into the tag's
|
|
216
|
+
children; the class is an ordinary custom element with no base class.
|
|
217
|
+
|
|
218
|
+
Reach for a widget only when plain HTML cannot do the job. A list, a form
|
|
219
|
+
and a condition need none. A widget exists to wrap a control that decides
|
|
220
|
+
its own moment to send (`<lb-input>` on `change`), or to place and scaffold
|
|
221
|
+
rows (`lbPlaceRow`, `lbRowsLanded`).
|
|
222
|
+
|
|
223
|
+
Before writing one, check `@loadbare/widgets`: `lb-input`, `lb-select`,
|
|
224
|
+
`lb-options`, `lb-table`, `lb-picker`, `lb-unknown-page`. Install it and
|
|
225
|
+
list it in `src/imports.ts`:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
export default ["@loadbare/widgets"];
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The application's own definition of a tag overrides a package's.
|
|
232
|
+
|
|
233
|
+
## Reference material
|
|
234
|
+
|
|
235
|
+
Read these when the summary above does not settle the question. They ship
|
|
236
|
+
with the package, so no network is needed.
|
|
237
|
+
|
|
238
|
+
- [`references/overview.md`](references/overview.md) — the map of the
|
|
239
|
+
reference, by part of the application.
|
|
240
|
+
- [`references/data-binding.md`](references/data-binding.md) — every `lb-`
|
|
241
|
+
attribute, requests, forms, conditions and request state. Start here for
|
|
242
|
+
anything in a page.
|
|
243
|
+
- [`references/page-files.md`](references/page-files.md) — queries,
|
|
244
|
+
`onPageEnter`, actions, CRUD, refresh and patch.
|
|
245
|
+
- [`references/custom-elements.md`](references/custom-elements.md) —
|
|
246
|
+
expansion, parameters, slots, destinations, and widget code.
|
|
247
|
+
- [`references/chrome.md`](references/chrome.md) — the chrome,
|
|
248
|
+
navigation and `lb-navigation`.
|
|
249
|
+
- [`references/server.md`](references/server.md) — the Express server and
|
|
250
|
+
the request context.
|
|
251
|
+
- [`references/builder.md`](references/builder.md) — the builder's options,
|
|
252
|
+
what it reads, and where it looks.
|
|
253
|
+
- [`references/css.md`](references/css.md) — how stylesheets are ordered.
|
|
254
|
+
- [`references/widgets.md`](references/widgets.md) — the basic widget
|
|
255
|
+
library.
|
|
256
|
+
- [`references/TECHREF-1.0.md`](references/TECHREF-1.0.md) — the reserved
|
|
257
|
+
names, and the open items that block 1.0. Read it before designing around
|
|
258
|
+
something the other references do not mention.
|