@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 CHANGED
@@ -1,3 +1,37 @@
1
- # Temporary Holding Version
1
+ # @monospaced/create-set
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../src/cli.mjs";
3
+
4
+ await run(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "@monospaced/create-set",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
  }
@@ -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
+ ];
@@ -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("&", "&amp;")
19
+ .replaceAll("<", "&lt;")
20
+ .replaceAll(">", "&gt;")
21
+ .replaceAll('"', "&quot;");
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";