@monospaced/create-set 0.0.0-stage → 0.20.2
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/README.md +36 -2
- package/bin/create-set.mjs +4 -0
- package/package.json +29 -3
- package/src/answers.mjs +34 -0
- package/src/cli.mjs +167 -0
- package/src/constants.mjs +20 -0
- package/src/execute.mjs +128 -0
- package/src/page.mjs +107 -0
- package/src/plan.mjs +47 -0
- package/src/templates.mjs +257 -0
- package/src/toolchain.mjs +7 -0
package/README.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @monospaced/create-set
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Project scaffolding for Set. Bootstraps a site or app with Set's components, tooling and defaults.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm create @monospaced/set my-site
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Prompts for a package name, site title and description, each showing its default, which Enter accepts. Each has a flag (`--name`, `--title`, `--description`), and `--defaults` takes every default without prompting, so it runs non-interactively too; without a terminal, anything unanswered is refused rather than guessed. Refuses a directory that isn't empty.
|
|
10
|
+
|
|
11
|
+
## What you get
|
|
12
|
+
|
|
13
|
+
A Vite and TypeScript project with a starter page built from Set components, a `src/styles.css` for your own styles, Set's ESLint and Stylelint presets, Prettier, Husky and lint-staged, VS Code settings, and Lightning CSS targets from Set's Baseline query.
|
|
14
|
+
|
|
15
|
+
- Dependencies install live, so nothing in the template goes stale. Tools the config package declares as peers install at its declared ranges, so preset and tool agree.
|
|
16
|
+
- `scripts/sync-set.mjs` copies the token catalog, agent skills and favicons into place on every install.
|
|
17
|
+
- Git is initialised first, with an initial commit at the end.
|
|
18
|
+
|
|
19
|
+
The Node and pnpm versions written to `engines`, `packageManager` and `.node-version` are generated into `src/toolchain.mjs` from the monorepo's toolchain by `pnpm create:sync`; `pnpm create:verify` fails CI when they drift.
|
|
20
|
+
|
|
21
|
+
## Developing
|
|
22
|
+
|
|
23
|
+
From the repository root:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pnpm create:test
|
|
27
|
+
pnpm create:dev ../scratch/my-site
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`create:dev` builds core first and runs the CLI from source. The generated project installs the published Set packages, which is what a user gets. To scaffold against the workspace instead, pack the Set packages and point the CLI at them:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pnpm --filter "@monospaced/set-{assets,config,core,skills,tokens}" pack --pack-destination ../scratch/pack
|
|
34
|
+
CREATE_SET_PACK_DIR=../scratch/pack pnpm create:dev ../scratch/my-site
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`plan()` in `src/plan.mjs` is pure and returns every file for a set of answers; the tests snapshot it. `execute()` does the I/O: git, files, `pnpm add`, the sync script, the commit.
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monospaced/create-set",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.20.2",
|
|
4
|
+
"description": "Project scaffolding for Set. Bootstraps a site or app with Set's components, tooling and defaults.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/monospaced/set.git",
|
|
8
|
+
"directory": "packages/create"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"create-set": "./bin/create-set.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@clack/prompts": "^1.8.1",
|
|
21
|
+
"@monospaced/set-core": "0.20.2"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"vitest": "^4.1.6"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"sync": "node scripts/sync.mjs",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"verify": "node scripts/verify.mjs"
|
|
31
|
+
}
|
|
6
32
|
}
|
package/src/answers.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
const PACKAGE_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
4
|
+
|
|
5
|
+
/** A package name derived from a directory: lowercase, separators as hyphens. */
|
|
6
|
+
export function toPackageName(dir) {
|
|
7
|
+
const name = basename(resolve(dir))
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9._~-]+/g, "-")
|
|
10
|
+
.replace(/^[-._]+|[-._]+$/g, "");
|
|
11
|
+
|
|
12
|
+
return name || "set-app";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A site title derived from a package name: words capitalised, scope dropped. */
|
|
16
|
+
export function toTitle(name) {
|
|
17
|
+
return name
|
|
18
|
+
.replace(/^@[^/]+\//, "")
|
|
19
|
+
.split(/[-._]+/)
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
22
|
+
.join(" ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function toDescription(title) {
|
|
26
|
+
return `${title}, built with Set.`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Returns a message when `value` is not a valid npm package name. */
|
|
30
|
+
export function validatePackageName(value) {
|
|
31
|
+
if (!PACKAGE_NAME.test(value)) {
|
|
32
|
+
return "Use lowercase letters, digits, hyphens, dots or underscores, optionally scoped.";
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
toDescription,
|
|
8
|
+
toPackageName,
|
|
9
|
+
toTitle,
|
|
10
|
+
validatePackageName,
|
|
11
|
+
} from "./answers.mjs";
|
|
12
|
+
import { execute, isEmptyDirectory } from "./execute.mjs";
|
|
13
|
+
|
|
14
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
15
|
+
|
|
16
|
+
const USAGE = `Usage: pnpm create @monospaced/set [directory] [options]
|
|
17
|
+
|
|
18
|
+
Scaffolds a Set site or app into an empty directory. Prompts for anything
|
|
19
|
+
not given as a flag; Enter accepts a prompt's default.
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--name <name> Package name (default: the directory name)
|
|
23
|
+
--title <title> Site title (default: from the package name)
|
|
24
|
+
--description <text> Site description (default: "<title>, built with Set.")
|
|
25
|
+
--defaults Take every default without prompting
|
|
26
|
+
-h, --help Show this help
|
|
27
|
+
-v, --version Show the version
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const OPTIONS = {
|
|
31
|
+
defaults: { type: "boolean" },
|
|
32
|
+
description: { type: "string" },
|
|
33
|
+
help: { short: "h", type: "boolean" },
|
|
34
|
+
name: { type: "string" },
|
|
35
|
+
title: { type: "string" },
|
|
36
|
+
version: { short: "v", type: "boolean" },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Prompts, or returns `undefined` when the user cancels. */
|
|
40
|
+
async function prompt(promise) {
|
|
41
|
+
const value = await promise;
|
|
42
|
+
return p.isCancel(value) ? undefined : value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Answers a question from its flag, its default (with `--defaults`), or a
|
|
47
|
+
* prompt whose hint is the default.
|
|
48
|
+
*/
|
|
49
|
+
async function answer(values, key, fallback, { missing, ...options }) {
|
|
50
|
+
if (values[key] !== undefined) return values[key];
|
|
51
|
+
if (values.defaults) return fallback;
|
|
52
|
+
if (!process.stdin.isTTY) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`No terminal to prompt for the ${key}. ${missing ?? `Pass --${key}`}, or pass --defaults.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return prompt(
|
|
59
|
+
p.text({ defaultValue: fallback, placeholder: fallback, ...options }),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Resolves the answers, or returns `undefined` when cancelled or refused. */
|
|
64
|
+
async function ask(values, positional) {
|
|
65
|
+
const dir =
|
|
66
|
+
positional ??
|
|
67
|
+
(await answer(values, "directory", "set-app", {
|
|
68
|
+
message: "Where should the project be created?",
|
|
69
|
+
missing: "Pass it as the first argument",
|
|
70
|
+
}));
|
|
71
|
+
if (dir === undefined) return;
|
|
72
|
+
|
|
73
|
+
if (!isEmptyDirectory(dir)) {
|
|
74
|
+
p.cancel(`${dir} is not empty. Choose an empty or new directory.`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const name = await answer(values, "name", toPackageName(dir), {
|
|
79
|
+
message: "Package name",
|
|
80
|
+
validate: validatePackageName,
|
|
81
|
+
});
|
|
82
|
+
if (name === undefined) return;
|
|
83
|
+
|
|
84
|
+
const invalid = validatePackageName(name);
|
|
85
|
+
if (invalid) {
|
|
86
|
+
p.cancel(`${name} is not a valid package name. ${invalid}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const title = await answer(values, "title", toTitle(name), {
|
|
91
|
+
message: "Site title",
|
|
92
|
+
});
|
|
93
|
+
if (title === undefined) return;
|
|
94
|
+
|
|
95
|
+
const description = await answer(
|
|
96
|
+
values,
|
|
97
|
+
"description",
|
|
98
|
+
toDescription(title),
|
|
99
|
+
{ message: "Site description" },
|
|
100
|
+
);
|
|
101
|
+
if (description === undefined) return;
|
|
102
|
+
|
|
103
|
+
return { description, dir, name, title };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function run(argv) {
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = parseArgs({
|
|
110
|
+
allowPositionals: true,
|
|
111
|
+
args: argv,
|
|
112
|
+
options: OPTIONS,
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(`${error.message}\n\n${USAGE}`);
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { positionals, values } = parsed;
|
|
121
|
+
|
|
122
|
+
if (values.help) {
|
|
123
|
+
console.log(USAGE);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (values.version) {
|
|
128
|
+
console.log(version);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
p.intro(`create-set ${version}`);
|
|
133
|
+
|
|
134
|
+
let answers;
|
|
135
|
+
try {
|
|
136
|
+
answers = await ask(values, positionals[0]);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
p.cancel(error.message);
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!answers) {
|
|
144
|
+
p.cancel("Nothing created.");
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const spinner = p.spinner();
|
|
150
|
+
spinner.start("Creating project");
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const root = execute(answers, {
|
|
154
|
+
step: (message) => spinner.message(message),
|
|
155
|
+
warn: (message) => p.log.warn(message),
|
|
156
|
+
});
|
|
157
|
+
spinner.stop(`Created ${root}`);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
spinner.stop("Failed", 1);
|
|
160
|
+
p.log.error(error.message);
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
p.note(`cd ${answers.dir}\npnpm dev`, "Next steps");
|
|
166
|
+
p.outro("Happy building.");
|
|
167
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** One social-share card for every page, shared with the Set docs. */
|
|
2
|
+
export const OG_IMAGE =
|
|
3
|
+
"https://res.cloudinary.com/monospaced/image/upload/v1789307953/2018-04-20_15.28.26--cyan--og--mid.png";
|
|
4
|
+
|
|
5
|
+
/** Installed live at generation time, so nothing in the template goes stale. */
|
|
6
|
+
export const RUNTIME_DEPENDENCIES = [
|
|
7
|
+
"@monospaced/set-assets",
|
|
8
|
+
"@monospaced/set-core",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export const DEV_DEPENDENCIES = [
|
|
12
|
+
"@monospaced/set-config",
|
|
13
|
+
"@monospaced/set-skills",
|
|
14
|
+
"@monospaced/set-tokens",
|
|
15
|
+
"husky",
|
|
16
|
+
"lint-staged",
|
|
17
|
+
"prettier",
|
|
18
|
+
"vite",
|
|
19
|
+
"vitest",
|
|
20
|
+
];
|
package/src/execute.mjs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { DEV_DEPENDENCIES, RUNTIME_DEPENDENCIES } from "./constants.mjs";
|
|
6
|
+
import { plan } from "./plan.mjs";
|
|
7
|
+
|
|
8
|
+
/** True when `dir` does not exist or holds nothing. */
|
|
9
|
+
export function isEmptyDirectory(dir) {
|
|
10
|
+
try {
|
|
11
|
+
return readdirSync(dir).length === 0;
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error.code === "ENOENT") return true;
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function run(command, args, cwd) {
|
|
19
|
+
const result = spawnSync(command, args, {
|
|
20
|
+
cwd,
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
shell: process.platform === "win32",
|
|
23
|
+
stdio: "pipe",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (result.error) throw result.error;
|
|
27
|
+
if (result.status !== 0) {
|
|
28
|
+
const output = `${result.stdout}\n${result.stderr}`.trim();
|
|
29
|
+
throw new Error(`\`${command} ${args.join(" ")}\` failed:\n${output}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result.stdout;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pnpm = (args, cwd) => run("pnpm", args, cwd);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Dev harness: with CREATE_SET_PACK_DIR set, Set packages install from the
|
|
39
|
+
* `pnpm pack` tarballs in that directory instead of the registry, so the CLI
|
|
40
|
+
* can be exercised against unreleased packages. Users always get the registry.
|
|
41
|
+
*/
|
|
42
|
+
function setSpec(name) {
|
|
43
|
+
const packDir = process.env.CREATE_SET_PACK_DIR;
|
|
44
|
+
if (!packDir || !name.startsWith("@monospaced/")) return name;
|
|
45
|
+
|
|
46
|
+
const stem = `${name.slice(1).replace("/", "-")}-`;
|
|
47
|
+
const tarball = readdirSync(packDir).find(
|
|
48
|
+
(file) => file.startsWith(stem) && file.endsWith(".tgz"),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return tarball ? resolve(packDir, tarball) : name;
|
|
52
|
+
}
|
|
53
|
+
const git = (args, cwd) => run("git", args, cwd);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The tools the config package drives, as `name@range` specs. Its presets
|
|
57
|
+
* declare them as peers, so they install at the range it supports rather
|
|
58
|
+
* than at latest, and the preset and the tool always agree.
|
|
59
|
+
*/
|
|
60
|
+
function configPeerSpecs(root) {
|
|
61
|
+
const manifest = JSON.parse(
|
|
62
|
+
readFileSync(
|
|
63
|
+
join(root, "node_modules/@monospaced/set-config/package.json"),
|
|
64
|
+
"utf8",
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return Object.entries(manifest.peerDependencies).map(
|
|
69
|
+
([name, range]) => `${name}@${range}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Writes the project, installs its dependencies live, syncs the Set
|
|
75
|
+
* artifacts and makes the initial commit.
|
|
76
|
+
*
|
|
77
|
+
* @param {{ dir: string } & import("./plan.mjs").Answers} answers
|
|
78
|
+
* @param {{ step(message: string): void, warn(message: string): void }} report
|
|
79
|
+
*/
|
|
80
|
+
export function execute({ dir, ...answers }, report) {
|
|
81
|
+
const root = resolve(dir);
|
|
82
|
+
const files = plan(answers);
|
|
83
|
+
|
|
84
|
+
report.step("Initialising git");
|
|
85
|
+
mkdirSync(root, { recursive: true });
|
|
86
|
+
git(["init", "--initial-branch=main"], root);
|
|
87
|
+
|
|
88
|
+
report.step("Writing files");
|
|
89
|
+
for (const [path, content] of Object.entries(files)) {
|
|
90
|
+
const target = join(root, path);
|
|
91
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
92
|
+
writeFileSync(target, content);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Lifecycle scripts wait until everything `prepare` needs is installed.
|
|
96
|
+
report.step("Installing Set");
|
|
97
|
+
pnpm(["add", "--ignore-scripts", ...RUNTIME_DEPENDENCIES.map(setSpec)], root);
|
|
98
|
+
|
|
99
|
+
report.step("Installing tooling");
|
|
100
|
+
pnpm(
|
|
101
|
+
["add", "--ignore-scripts", "--save-dev", ...DEV_DEPENDENCIES.map(setSpec)],
|
|
102
|
+
root,
|
|
103
|
+
);
|
|
104
|
+
pnpm(
|
|
105
|
+
["add", "--ignore-scripts", "--save-dev", ...configPeerSpecs(root)],
|
|
106
|
+
root,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Explicit: `pnpm install` skips lifecycle scripts when nothing changed.
|
|
110
|
+
report.step("Syncing Set artifacts and git hooks");
|
|
111
|
+
pnpm(["run", "prepare"], root);
|
|
112
|
+
|
|
113
|
+
report.step("Formatting");
|
|
114
|
+
pnpm(["exec", "prettier", "--write", "."], root);
|
|
115
|
+
|
|
116
|
+
report.step("Committing");
|
|
117
|
+
git(["add", "--all"], root);
|
|
118
|
+
try {
|
|
119
|
+
git(
|
|
120
|
+
["commit", "--message", "chore: bootstrap with @monospaced/create-set"],
|
|
121
|
+
root,
|
|
122
|
+
);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
report.warn(`Initial commit skipped. ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return root;
|
|
128
|
+
}
|
package/src/page.mjs
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderSetBox,
|
|
3
|
+
renderSetContainer,
|
|
4
|
+
renderSetDivider,
|
|
5
|
+
renderSetHeading,
|
|
6
|
+
renderSetInline,
|
|
7
|
+
renderSetLightswitch,
|
|
8
|
+
renderSetPage,
|
|
9
|
+
renderSetRoot,
|
|
10
|
+
renderSetStack,
|
|
11
|
+
renderSetText,
|
|
12
|
+
SET_LIGHTSWITCH_STORAGE_KEY,
|
|
13
|
+
} from "@monospaced/set-core";
|
|
14
|
+
|
|
15
|
+
/** Escapes a value for an HTML attribute or text node. */
|
|
16
|
+
export const escapeHtml = (value) =>
|
|
17
|
+
value
|
|
18
|
+
.replaceAll("&", "&")
|
|
19
|
+
.replaceAll("<", "<")
|
|
20
|
+
.replaceAll(">", ">")
|
|
21
|
+
.replaceAll('"', """);
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* Stamps a stored lightswitch override onto the root before its content
|
|
25
|
+
* parses, so an overridden theme paints correctly on first load. Must be the
|
|
26
|
+
* root's first child: earlier and the root doesn't exist yet, later and
|
|
27
|
+
* content above it may already have painted in the wrong theme.
|
|
28
|
+
*/
|
|
29
|
+
const themeBootstrap = `<script>try{var t=localStorage.getItem(${JSON.stringify(
|
|
30
|
+
SET_LIGHTSWITCH_STORAGE_KEY,
|
|
31
|
+
)});if(t==="light"||t==="dark")document.currentScript.closest(".set").setAttribute("data-set-theme",t)}catch(e){}</script>`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The hello-world page: Root → Page with a header, a heading, a paragraph and
|
|
35
|
+
* a Lightswitch, built from Set components only. Rendered with core at
|
|
36
|
+
* generation time, so the markup is always what the installed core expects.
|
|
37
|
+
*/
|
|
38
|
+
export function renderHelloPage({ title }) {
|
|
39
|
+
const safeTitle = escapeHtml(title);
|
|
40
|
+
|
|
41
|
+
const header = renderSetContainer({
|
|
42
|
+
children: renderSetBox({
|
|
43
|
+
background: "transparent",
|
|
44
|
+
paddingBlock: "2xs",
|
|
45
|
+
paddingInline: "none",
|
|
46
|
+
children: renderSetInline({
|
|
47
|
+
gap: "sm",
|
|
48
|
+
justify: "between",
|
|
49
|
+
children:
|
|
50
|
+
renderSetHeading({ children: safeTitle, size: "md" }) +
|
|
51
|
+
renderSetLightswitch({ size: "sm" }),
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const main = renderSetContainer({
|
|
57
|
+
children: renderSetBox({
|
|
58
|
+
paddingBlock: "lg",
|
|
59
|
+
paddingInline: "none",
|
|
60
|
+
responsive: true,
|
|
61
|
+
children: renderSetStack({
|
|
62
|
+
gap: "md",
|
|
63
|
+
children:
|
|
64
|
+
renderSetHeading({
|
|
65
|
+
children: "Hello, world",
|
|
66
|
+
level: 1,
|
|
67
|
+
responsive: true,
|
|
68
|
+
size: "xl",
|
|
69
|
+
}) +
|
|
70
|
+
renderSetText({
|
|
71
|
+
as: "p",
|
|
72
|
+
children: `${safeTitle} is built with <a href="https://set.monospaced.com">Set</a>. Edit <code>index.html</code> to begin.`,
|
|
73
|
+
responsive: true,
|
|
74
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const footer =
|
|
80
|
+
renderSetDivider({ tone: "subtle" }) +
|
|
81
|
+
renderSetContainer({
|
|
82
|
+
children: renderSetBox({
|
|
83
|
+
paddingBlock: "xs",
|
|
84
|
+
paddingInline: "none",
|
|
85
|
+
children: renderSetText({
|
|
86
|
+
as: "p",
|
|
87
|
+
children: "Built with Set.",
|
|
88
|
+
size: "sm",
|
|
89
|
+
tone: "muted",
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const page = renderSetPage({
|
|
95
|
+
children: main,
|
|
96
|
+
footer,
|
|
97
|
+
header,
|
|
98
|
+
headerBorder: "always",
|
|
99
|
+
headerSize: "sm",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return renderSetRoot({
|
|
103
|
+
appOverscrollBehavior: "none",
|
|
104
|
+
appRoot: true,
|
|
105
|
+
children: themeBootstrap + page,
|
|
106
|
+
});
|
|
107
|
+
}
|
package/src/plan.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as templates from "./templates.mjs";
|
|
2
|
+
import { NODE_MAJOR, PNPM_VERSION } from "./toolchain.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} Answers
|
|
6
|
+
* @property {string} name Package name.
|
|
7
|
+
* @property {string} title Site title.
|
|
8
|
+
* @property {string} description Site description.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Every file the CLI writes, keyed by path relative to the project root.
|
|
13
|
+
* Pure: the same answers always give the same files. Dependencies are not
|
|
14
|
+
* here; they are installed live by `execute`.
|
|
15
|
+
*
|
|
16
|
+
* @param {Answers} answers
|
|
17
|
+
* @returns {Record<string, string>}
|
|
18
|
+
*/
|
|
19
|
+
export function plan(answers) {
|
|
20
|
+
const context = {
|
|
21
|
+
...answers,
|
|
22
|
+
nodeMajor: NODE_MAJOR,
|
|
23
|
+
pnpmVersion: PNPM_VERSION,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
".editorconfig": templates.editorconfig(),
|
|
28
|
+
".gitignore": templates.gitignore(),
|
|
29
|
+
".husky/pre-commit": "pnpm exec lint-staged\n",
|
|
30
|
+
".node-version": `${NODE_MAJOR}\n`,
|
|
31
|
+
".prettierignore": templates.prettierignore(),
|
|
32
|
+
".stylelintrc.mjs": templates.stylelintrc(),
|
|
33
|
+
".vscode/extensions.json": templates.vscodeExtensions(),
|
|
34
|
+
".vscode/settings.json": templates.vscodeSettings(),
|
|
35
|
+
"README.md": templates.readme(context),
|
|
36
|
+
"eslint.config.mjs": templates.eslintConfig(),
|
|
37
|
+
"index.html": templates.indexHtml(context),
|
|
38
|
+
"package.json": templates.packageJson(context),
|
|
39
|
+
"pnpm-workspace.yaml": templates.pnpmWorkspace(),
|
|
40
|
+
"public/manifest.webmanifest": templates.manifest(context),
|
|
41
|
+
"scripts/sync-set.mjs": templates.syncSet(),
|
|
42
|
+
"src/main.ts": templates.mainTs(),
|
|
43
|
+
"src/styles.css": templates.stylesCss(),
|
|
44
|
+
"tsconfig.json": templates.tsconfig(),
|
|
45
|
+
"vite.config.ts": templates.viteConfig(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { OG_IMAGE } from "./constants.mjs";
|
|
2
|
+
import { escapeHtml, renderHelloPage } from "./page.mjs";
|
|
3
|
+
|
|
4
|
+
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
5
|
+
|
|
6
|
+
export const packageJson = ({ description, name, nodeMajor, pnpmVersion }) => {
|
|
7
|
+
const pnpmMajor = Number(pnpmVersion.split(".")[0]);
|
|
8
|
+
|
|
9
|
+
return json({
|
|
10
|
+
name,
|
|
11
|
+
version: "0.0.0",
|
|
12
|
+
description,
|
|
13
|
+
private: true,
|
|
14
|
+
type: "module",
|
|
15
|
+
engines: {
|
|
16
|
+
node: `>=${nodeMajor} <${nodeMajor + 1}`,
|
|
17
|
+
pnpm: `>=${pnpmMajor} <${pnpmMajor + 1}`,
|
|
18
|
+
},
|
|
19
|
+
packageManager: `pnpm@${pnpmVersion}`,
|
|
20
|
+
scripts: {
|
|
21
|
+
build: "vite build",
|
|
22
|
+
dev: "vite",
|
|
23
|
+
format: "prettier --write .",
|
|
24
|
+
"format:check": "prettier --check .",
|
|
25
|
+
lint: "eslint .",
|
|
26
|
+
"lint:fix": "eslint . --fix",
|
|
27
|
+
prepare: "node scripts/sync-set.mjs",
|
|
28
|
+
preview: "vite preview",
|
|
29
|
+
stylelint: 'stylelint "**/*.css" --allow-empty-input',
|
|
30
|
+
"stylelint:fix": 'stylelint "**/*.css" --allow-empty-input --fix',
|
|
31
|
+
test: "vitest run --passWithNoTests",
|
|
32
|
+
typecheck: "tsc --noEmit",
|
|
33
|
+
},
|
|
34
|
+
"lint-staged": {
|
|
35
|
+
"*": "prettier --ignore-unknown --write",
|
|
36
|
+
"*.css": "stylelint --fix",
|
|
37
|
+
"*.{js,mjs,ts}": "eslint --fix",
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const indexHtml = ({ description, title }) => `<!doctype html>
|
|
43
|
+
<html lang="en">
|
|
44
|
+
<head>
|
|
45
|
+
<meta charset="utf-8" />
|
|
46
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
47
|
+
<meta name="color-scheme" content="light dark" />
|
|
48
|
+
<meta name="theme-color" content="#f8fbfb" media="(prefers-color-scheme: light)" />
|
|
49
|
+
<meta name="theme-color" content="#0e0f0f" media="(prefers-color-scheme: dark)" />
|
|
50
|
+
<title>${escapeHtml(title)}</title>
|
|
51
|
+
<meta name="description" content="${escapeHtml(description)}" />
|
|
52
|
+
<!-- Set the canonical URL and og:url once the site has a domain. -->
|
|
53
|
+
<link rel="canonical" href="https://example.com/" />
|
|
54
|
+
<meta property="og:type" content="website" />
|
|
55
|
+
<meta property="og:site_name" content="${escapeHtml(title)}" />
|
|
56
|
+
<meta property="og:title" content="${escapeHtml(title)}" />
|
|
57
|
+
<meta property="og:description" content="${escapeHtml(description)}" />
|
|
58
|
+
<meta property="og:url" content="https://example.com/" />
|
|
59
|
+
<meta property="og:image" content="${OG_IMAGE}" />
|
|
60
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
61
|
+
<meta name="twitter:title" content="${escapeHtml(title)}" />
|
|
62
|
+
<meta name="twitter:description" content="${escapeHtml(description)}" />
|
|
63
|
+
<meta name="twitter:image" content="${OG_IMAGE}" />
|
|
64
|
+
<link rel="icon" href="/favicons/favicon.ico" sizes="32x32" />
|
|
65
|
+
<link rel="icon" href="/favicons/favicon.svg" type="image/svg+xml" />
|
|
66
|
+
<link rel="apple-touch-icon" href="/favicons/apple-touch-icon.png" />
|
|
67
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
68
|
+
</head>
|
|
69
|
+
<body>
|
|
70
|
+
${renderHelloPage({ title })}
|
|
71
|
+
<script type="module" src="/src/main.ts"></script>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
export const manifest = ({ description, title }) =>
|
|
77
|
+
json({
|
|
78
|
+
name: title,
|
|
79
|
+
short_name: title,
|
|
80
|
+
description,
|
|
81
|
+
start_url: "/",
|
|
82
|
+
scope: "/",
|
|
83
|
+
display: "standalone",
|
|
84
|
+
background_color: "#f8fbfb",
|
|
85
|
+
theme_color: "#f8fbfb",
|
|
86
|
+
icons: [192, 512].flatMap((size) =>
|
|
87
|
+
["any", "maskable"].map((purpose) => ({
|
|
88
|
+
purpose,
|
|
89
|
+
sizes: `${size}x${size}`,
|
|
90
|
+
src: `/favicons/maskable-icon-${size}.png`,
|
|
91
|
+
type: "image/png",
|
|
92
|
+
})),
|
|
93
|
+
),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export const mainTs = () => `import "@monospaced/set-assets/fonts.css";
|
|
97
|
+
import "@monospaced/set-core/styles.css";
|
|
98
|
+
import "./styles.css";
|
|
99
|
+
|
|
100
|
+
import { defineSetComponents } from "@monospaced/set-core";
|
|
101
|
+
|
|
102
|
+
defineSetComponents();
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
export const stylesCss =
|
|
106
|
+
() => `/* Project styles. Compose with Set components first; when something needs
|
|
107
|
+
its own CSS, write it here with Set tokens. */
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
export const viteConfig =
|
|
111
|
+
() => `import target from "@monospaced/set-config/browserslist/esbuild";
|
|
112
|
+
import targets from "@monospaced/set-config/browserslist/lightningcss";
|
|
113
|
+
import { defineConfig } from "vite";
|
|
114
|
+
|
|
115
|
+
export default defineConfig({
|
|
116
|
+
build: { cssMinify: "lightningcss", target },
|
|
117
|
+
css: { lightningcss: { targets }, transformer: "lightningcss" },
|
|
118
|
+
});
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
export const tsconfig = () =>
|
|
122
|
+
json({
|
|
123
|
+
compilerOptions: {
|
|
124
|
+
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
125
|
+
module: "ESNext",
|
|
126
|
+
moduleResolution: "Bundler",
|
|
127
|
+
noEmit: true,
|
|
128
|
+
skipLibCheck: true,
|
|
129
|
+
strict: true,
|
|
130
|
+
target: "ES2022",
|
|
131
|
+
types: ["vite/client"],
|
|
132
|
+
},
|
|
133
|
+
include: ["src", "*.config.ts"],
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export const eslintConfig =
|
|
137
|
+
() => `import setEslint from "@monospaced/set-config/eslint";
|
|
138
|
+
|
|
139
|
+
export default [...setEslint];
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
export const stylelintrc = () => `export default {
|
|
143
|
+
extends: ["@monospaced/set-config/stylelint"],
|
|
144
|
+
ignoreFiles: [".vscode/set.catalog.css", "dist/**"],
|
|
145
|
+
};
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
export const prettierignore = () => `.vscode/set.catalog.css
|
|
149
|
+
dist
|
|
150
|
+
pnpm-lock.yaml
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
export const editorconfig = () => `root = true
|
|
154
|
+
|
|
155
|
+
[*]
|
|
156
|
+
charset = utf-8
|
|
157
|
+
end_of_line = lf
|
|
158
|
+
indent_size = 2
|
|
159
|
+
indent_style = space
|
|
160
|
+
insert_final_newline = true
|
|
161
|
+
trim_trailing_whitespace = true
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
export const gitignore = () => `node_modules/
|
|
165
|
+
dist/
|
|
166
|
+
.vscode/set.catalog.css
|
|
167
|
+
.claude/skills/
|
|
168
|
+
.agents/skills/
|
|
169
|
+
.claude/settings.local.json
|
|
170
|
+
.claude/.cc-writes/
|
|
171
|
+
.DS_Store
|
|
172
|
+
.env
|
|
173
|
+
.env.*
|
|
174
|
+
!.env.example
|
|
175
|
+
*.local
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
export const vscodeSettings = () =>
|
|
179
|
+
json({
|
|
180
|
+
"css.validate": false,
|
|
181
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
182
|
+
"editor.formatOnSave": true,
|
|
183
|
+
"stylelint.validate": ["css"],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
export const vscodeExtensions = () =>
|
|
187
|
+
json({
|
|
188
|
+
recommendations: [
|
|
189
|
+
"dbaeumer.vscode-eslint",
|
|
190
|
+
"esbenp.prettier-vscode",
|
|
191
|
+
"stylelint.vscode-stylelint",
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export const pnpmWorkspace =
|
|
196
|
+
() => `# Minimum age of a release before it installs, in minutes (3 days).
|
|
197
|
+
minimumReleaseAge: 4320
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
export const syncSet = () => `/*
|
|
201
|
+
* Copies the Set artifacts this project keeps outside node_modules, so a
|
|
202
|
+
* fresh clone or a Set upgrade refreshes them. Runs from \`prepare\`.
|
|
203
|
+
*/
|
|
204
|
+
import { cpSync, existsSync, rmSync } from "node:fs";
|
|
205
|
+
import { fileURLToPath } from "node:url";
|
|
206
|
+
|
|
207
|
+
const root = fileURLToPath(new URL("../", import.meta.url));
|
|
208
|
+
const from = (path) => new URL(\`../node_modules/\${path}\`, import.meta.url);
|
|
209
|
+
const to = (path) => new URL(\`../\${path}\`, import.meta.url);
|
|
210
|
+
|
|
211
|
+
const copy = (source, target) => {
|
|
212
|
+
rmSync(target, { force: true, recursive: true });
|
|
213
|
+
cpSync(source, target, { recursive: true });
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Token catalog for editor autocomplete and Stylelint validation.
|
|
217
|
+
copy(from("@monospaced/set-config/set.catalog.css"), to(".vscode/set.catalog.css"));
|
|
218
|
+
|
|
219
|
+
// Guardrails for coding agents, wherever they look for skills.
|
|
220
|
+
for (const dir of [".claude/skills", ".agents/skills"]) {
|
|
221
|
+
copy(from("@monospaced/set-skills/src"), to(dir));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Favicons are static files, so they live in public/ rather than being imported.
|
|
225
|
+
copy(from("@monospaced/set-assets/src/favicons"), to("public/favicons"));
|
|
226
|
+
|
|
227
|
+
// Git hooks, where there is a repository to hook into.
|
|
228
|
+
if (existsSync(new URL(".git", \`file://\${root}\`))) {
|
|
229
|
+
const { default: husky } = await import("husky");
|
|
230
|
+
const message = husky();
|
|
231
|
+
if (message) console.log(message);
|
|
232
|
+
}
|
|
233
|
+
`;
|
|
234
|
+
|
|
235
|
+
export const readme = ({ description, title }) => `# ${title}
|
|
236
|
+
|
|
237
|
+
${description}
|
|
238
|
+
|
|
239
|
+
Built with [Set](https://set.monospaced.com), scaffolded by \`@monospaced/create-set\`.
|
|
240
|
+
|
|
241
|
+
## Scripts
|
|
242
|
+
|
|
243
|
+
- \`pnpm dev\` / \`pnpm build\` / \`pnpm preview\`
|
|
244
|
+
- \`pnpm lint\` / \`pnpm lint:fix\`
|
|
245
|
+
- \`pnpm stylelint\` / \`pnpm stylelint:fix\`
|
|
246
|
+
- \`pnpm format\` / \`pnpm format:check\`
|
|
247
|
+
- \`pnpm typecheck\`
|
|
248
|
+
- \`pnpm test\`
|
|
249
|
+
|
|
250
|
+
Prettier, Stylelint and ESLint fixes run on staged files before every commit via Husky and lint-staged.
|
|
251
|
+
|
|
252
|
+
## After bootstrapping
|
|
253
|
+
|
|
254
|
+
- Set the canonical URL and \`og:url\` in \`index.html\` once the site has a domain.
|
|
255
|
+
- \`.vscode/set.catalog.css\`, \`.claude/skills/\`, \`.agents/skills/\` and \`public/favicons/\` are copied from Set on every install by \`scripts/sync-set.mjs\`. Upgrade Set to refresh them rather than editing them by hand.
|
|
256
|
+
- Markdown content? \`pnpm add @monospaced/set-markdown\` renders GFM to safe HTML that pairs with core's \`prose\` component.
|
|
257
|
+
`;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Generated by scripts/sync.mjs from the monorepo toolchain. Do not edit.
|
|
2
|
+
|
|
3
|
+
/** Node major written to `engines` and `.node-version`. */
|
|
4
|
+
export const NODE_MAJOR = 26;
|
|
5
|
+
|
|
6
|
+
/** pnpm version written to `packageManager`; `engines` takes its major. */
|
|
7
|
+
export const PNPM_VERSION = "12.17.0";
|