@monospaced/create-set 0.20.2 → 0.22.0
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 +17 -6
- package/package.json +3 -2
- package/src/cli.mjs +36 -11
- package/src/constants.mjs +11 -6
- package/src/execute.mjs +99 -37
- package/src/page.mjs +11 -26
- package/src/plan.mjs +34 -6
- package/src/templates.mjs +448 -20
- package/src/toolchain.mjs +21 -0
package/README.md
CHANGED
|
@@ -6,17 +6,28 @@ Project scaffolding for Set. Bootstraps a site or app with Set's components, too
|
|
|
6
6
|
pnpm create @monospaced/set my-site
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Prompts for a package name, site title and
|
|
9
|
+
Prompts for a package name, site title, description and variant (`html`, `prerender` or `react`), each showing its default, which Enter accepts. Each has a flag (`--name`, `--title`, `--description`, `--variant`), 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
|
+
## Variants
|
|
12
|
+
|
|
13
|
+
Three ways to author, sharing everything else.
|
|
14
|
+
|
|
15
|
+
- **`html`** (default): pages are HTML you write against Set's CSS variables and custom elements. The simplest possible Set site; the file you write is the file that ships.
|
|
16
|
+
- **`prerender`**: pages are render functions in `src/pages`, prerendered to static HTML at build time by a small Vite plugin and hydrated by `defineSetComponents()`. One shared head in `src/document.ts`, per-page title and description, and nothing of the renderer ships to the browser.
|
|
17
|
+
- **`react`**: an app rendered on the client from `@monospaced/set-react` components, with `@vitejs/plugin-react`, the React ESLint preset, and jsdom for component tests. `Root` takes its initial theme from the stored lightswitch choice.
|
|
18
|
+
|
|
19
|
+
In `html` and `prerender`, a page is a folder with an `index.html` and its path is its URL; Vite discovers them, so adding a page is adding a folder.
|
|
10
20
|
|
|
11
21
|
## What you get
|
|
12
22
|
|
|
13
23
|
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
24
|
|
|
15
|
-
-
|
|
25
|
+
- Every version has an owner. Set packages install at latest; tools the config package declares as peers install at its declared ranges; the rest of the tooling installs at the monorepo's own ranges, so a scaffold gets the versions Set develops and tests with.
|
|
16
26
|
- `scripts/sync-set.mjs` copies the token catalog, agent skills and favicons into place on every install.
|
|
17
27
|
- Git is initialised first, with an initial commit at the end.
|
|
28
|
+
- A GitHub Actions workflow runs the project's checks on every pull request, and Dependabot proposes grouped updates weekly with a three-day cooldown.
|
|
18
29
|
|
|
19
|
-
The Node and pnpm versions written to `engines`, `packageManager` and `.node-version
|
|
30
|
+
The Node and pnpm versions written to `engines`, `packageManager` and `.node-version`, and the tooling ranges, are generated into `src/toolchain.mjs` from the monorepo's toolchain and pnpm catalog by `pnpm create:sync`; `pnpm create:verify` fails CI when they drift.
|
|
20
31
|
|
|
21
32
|
## Developing
|
|
22
33
|
|
|
@@ -27,11 +38,11 @@ pnpm create:test
|
|
|
27
38
|
pnpm create:dev ../scratch/my-site
|
|
28
39
|
```
|
|
29
40
|
|
|
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:
|
|
41
|
+
`create:dev` builds core first and runs the CLI from source. The `scaffold` workflow (`.github/workflows/scaffold.yml`) does the end-to-end run in CI: both variants, against this checkout's packed packages on pull requests and pushes, and against the registry on demand. 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
42
|
|
|
32
43
|
```sh
|
|
33
|
-
pnpm --filter
|
|
44
|
+
pnpm --filter @monospaced/set-assets --filter @monospaced/set-config --filter @monospaced/set-core --filter @monospaced/set-icons --filter @monospaced/set-react --filter @monospaced/set-skills --filter @monospaced/set-tokens pack --pack-destination ../scratch/pack
|
|
34
45
|
CREATE_SET_PACK_DIR=../scratch/pack pnpm create:dev ../scratch/my-site
|
|
35
46
|
```
|
|
36
47
|
|
|
37
|
-
`plan()` in `src/plan.mjs` is pure and returns every file for a set of answers; the tests snapshot
|
|
48
|
+
`plan()` in `src/plan.mjs` is pure and returns every file for a set of answers; the tests snapshot both variants, and one test executes the generated prerender modules to check they render the same document as the `html` starter. `execute()` does the I/O: git, files, `pnpm add`, the sync script, the commit.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monospaced/create-set",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Project scaffolding for Set. Bootstraps a site or app with Set's components, tooling and defaults.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@clack/prompts": "^1.8.1",
|
|
21
|
-
"@monospaced/set-core": "0.
|
|
21
|
+
"@monospaced/set-core": "0.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
+
"prettier": "^3.8.3",
|
|
24
25
|
"vitest": "^4.1.6"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
package/src/cli.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
toTitle,
|
|
10
10
|
validatePackageName,
|
|
11
11
|
} from "./answers.mjs";
|
|
12
|
+
import { VARIANTS } from "./constants.mjs";
|
|
12
13
|
import { execute, isEmptyDirectory } from "./execute.mjs";
|
|
13
14
|
|
|
14
15
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
@@ -22,6 +23,9 @@ Options:
|
|
|
22
23
|
--name <name> Package name (default: the directory name)
|
|
23
24
|
--title <title> Site title (default: from the package name)
|
|
24
25
|
--description <text> Site description (default: "<title>, built with Set.")
|
|
26
|
+
--variant <variant> html: pages are HTML you write (default)
|
|
27
|
+
prerender: pages are render functions, built to HTML
|
|
28
|
+
react: an app rendered by React
|
|
25
29
|
--defaults Take every default without prompting
|
|
26
30
|
-h, --help Show this help
|
|
27
31
|
-v, --version Show the version
|
|
@@ -33,9 +37,20 @@ const OPTIONS = {
|
|
|
33
37
|
help: { short: "h", type: "boolean" },
|
|
34
38
|
name: { type: "string" },
|
|
35
39
|
title: { type: "string" },
|
|
40
|
+
variant: { type: "string" },
|
|
36
41
|
version: { short: "v", type: "boolean" },
|
|
37
42
|
};
|
|
38
43
|
|
|
44
|
+
const VARIANT_OPTIONS = [
|
|
45
|
+
{ hint: "pages are HTML you write", label: "HTML", value: "html" },
|
|
46
|
+
{
|
|
47
|
+
hint: "pages are render functions, built to HTML",
|
|
48
|
+
label: "Prerender",
|
|
49
|
+
value: "prerender",
|
|
50
|
+
},
|
|
51
|
+
{ hint: "an app rendered by React", label: "React", value: "react" },
|
|
52
|
+
];
|
|
53
|
+
|
|
39
54
|
/** Prompts, or returns `undefined` when the user cancels. */
|
|
40
55
|
async function prompt(promise) {
|
|
41
56
|
const value = await promise;
|
|
@@ -44,9 +59,10 @@ async function prompt(promise) {
|
|
|
44
59
|
|
|
45
60
|
/**
|
|
46
61
|
* Answers a question from its flag, its default (with `--defaults`), or a
|
|
47
|
-
* prompt whose hint is the default
|
|
62
|
+
* prompt: a text field whose hint is the default, or a select when `options`
|
|
63
|
+
* are given.
|
|
48
64
|
*/
|
|
49
|
-
async function answer(values, key, fallback, { missing,
|
|
65
|
+
async function answer(values, key, fallback, { missing, options, ...rest }) {
|
|
50
66
|
if (values[key] !== undefined) return values[key];
|
|
51
67
|
if (values.defaults) return fallback;
|
|
52
68
|
if (!process.stdin.isTTY) {
|
|
@@ -56,7 +72,9 @@ async function answer(values, key, fallback, { missing, ...options }) {
|
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
return prompt(
|
|
59
|
-
|
|
75
|
+
options
|
|
76
|
+
? p.select({ initialValue: fallback, options, ...rest })
|
|
77
|
+
: p.text({ defaultValue: fallback, placeholder: fallback, ...rest }),
|
|
60
78
|
);
|
|
61
79
|
}
|
|
62
80
|
|
|
@@ -100,7 +118,18 @@ async function ask(values, positional) {
|
|
|
100
118
|
);
|
|
101
119
|
if (description === undefined) return;
|
|
102
120
|
|
|
103
|
-
|
|
121
|
+
const variant = await answer(values, "variant", VARIANTS[0], {
|
|
122
|
+
message: "How are pages authored?",
|
|
123
|
+
options: VARIANT_OPTIONS,
|
|
124
|
+
});
|
|
125
|
+
if (variant === undefined) return;
|
|
126
|
+
|
|
127
|
+
if (!VARIANTS.includes(variant)) {
|
|
128
|
+
p.cancel(`${variant} is not a variant. Use ${VARIANTS.join(" or ")}.`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { description, dir, name, title, variant };
|
|
104
133
|
}
|
|
105
134
|
|
|
106
135
|
export async function run(argv) {
|
|
@@ -146,17 +175,13 @@ export async function run(argv) {
|
|
|
146
175
|
return;
|
|
147
176
|
}
|
|
148
177
|
|
|
149
|
-
const spinner = p.spinner();
|
|
150
|
-
spinner.start("Creating project");
|
|
151
|
-
|
|
152
178
|
try {
|
|
153
|
-
const root = execute(answers, {
|
|
154
|
-
step: (message) =>
|
|
179
|
+
const root = await execute(answers, {
|
|
180
|
+
step: (message) => p.log.step(message),
|
|
155
181
|
warn: (message) => p.log.warn(message),
|
|
156
182
|
});
|
|
157
|
-
|
|
183
|
+
p.log.success(`Created ${root}`);
|
|
158
184
|
} catch (error) {
|
|
159
|
-
spinner.stop("Failed", 1);
|
|
160
185
|
p.log.error(error.message);
|
|
161
186
|
process.exitCode = 1;
|
|
162
187
|
return;
|
package/src/constants.mjs
CHANGED
|
@@ -2,19 +2,24 @@
|
|
|
2
2
|
export const OG_IMAGE =
|
|
3
3
|
"https://res.cloudinary.com/monospaced/image/upload/v1789307953/2018-04-20_15.28.26--cyan--og--mid.png";
|
|
4
4
|
|
|
5
|
+
/** How pages are authored. The first is the default. */
|
|
6
|
+
export const VARIANTS = ["html", "prerender", "react"];
|
|
7
|
+
|
|
5
8
|
/** Installed live at generation time, so nothing in the template goes stale. */
|
|
6
9
|
export const RUNTIME_DEPENDENCIES = [
|
|
7
10
|
"@monospaced/set-assets",
|
|
8
11
|
"@monospaced/set-core",
|
|
9
12
|
];
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
/** The React variant's extra Set package. */
|
|
15
|
+
export const REACT_RUNTIME_DEPENDENCIES = ["@monospaced/set-react"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set's own dev packages, at latest like the runtime ones. The rest of the
|
|
19
|
+
* tooling installs at the monorepo's ranges, from `toolchain.mjs`.
|
|
20
|
+
*/
|
|
21
|
+
export const SET_DEV_DEPENDENCIES = [
|
|
12
22
|
"@monospaced/set-config",
|
|
13
23
|
"@monospaced/set-skills",
|
|
14
24
|
"@monospaced/set-tokens",
|
|
15
|
-
"husky",
|
|
16
|
-
"lint-staged",
|
|
17
|
-
"prettier",
|
|
18
|
-
"vite",
|
|
19
|
-
"vitest",
|
|
20
25
|
];
|
package/src/execute.mjs
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
3
9
|
import { dirname, join, resolve } from "node:path";
|
|
4
10
|
|
|
5
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
REACT_RUNTIME_DEPENDENCIES,
|
|
13
|
+
RUNTIME_DEPENDENCIES,
|
|
14
|
+
SET_DEV_DEPENDENCIES,
|
|
15
|
+
} from "./constants.mjs";
|
|
6
16
|
import { plan } from "./plan.mjs";
|
|
17
|
+
import { REACT, TOOLING } from "./toolchain.mjs";
|
|
7
18
|
|
|
8
19
|
/** True when `dir` does not exist or holds nothing. */
|
|
9
20
|
export function isEmptyDirectory(dir) {
|
|
@@ -15,21 +26,29 @@ export function isEmptyDirectory(dir) {
|
|
|
15
26
|
}
|
|
16
27
|
}
|
|
17
28
|
|
|
29
|
+
/** Runs a command to completion, off the event loop so Ctrl-C still works. */
|
|
18
30
|
function run(command, args, cwd) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const child = spawn(command, args, {
|
|
33
|
+
cwd,
|
|
34
|
+
shell: process.platform === "win32",
|
|
35
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
36
|
+
});
|
|
37
|
+
let output = "";
|
|
38
|
+
child.stdout.on("data", (chunk) => (output += chunk));
|
|
39
|
+
child.stderr.on("data", (chunk) => (output += chunk));
|
|
40
|
+
child.on("error", reject);
|
|
41
|
+
child.on("close", (status) => {
|
|
42
|
+
if (status === 0) resolve(output);
|
|
43
|
+
else {
|
|
44
|
+
reject(
|
|
45
|
+
new Error(
|
|
46
|
+
`\`${command} ${args.join(" ")}\` failed:\n${output.trim()}`,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
24
51
|
});
|
|
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
52
|
}
|
|
34
53
|
|
|
35
54
|
const pnpm = (args, cwd) => run("pnpm", args, cwd);
|
|
@@ -39,19 +58,46 @@ const pnpm = (args, cwd) => run("pnpm", args, cwd);
|
|
|
39
58
|
* `pnpm pack` tarballs in that directory instead of the registry, so the CLI
|
|
40
59
|
* can be exercised against unreleased packages. Users always get the registry.
|
|
41
60
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
const packDir = process.env.CREATE_SET_PACK_DIR;
|
|
62
|
+
|
|
63
|
+
/** Packed Set packages, by name, from the tarball filenames. */
|
|
64
|
+
function packed() {
|
|
65
|
+
if (!packDir) return {};
|
|
66
|
+
|
|
67
|
+
return Object.fromEntries(
|
|
68
|
+
readdirSync(packDir)
|
|
69
|
+
.filter((file) => file.endsWith(".tgz"))
|
|
70
|
+
.map((file) => [
|
|
71
|
+
`@${file.replace(/-\d+\.\d+\.\d+.*\.tgz$/, "").replace("-", "/")}`,
|
|
72
|
+
resolve(packDir, file),
|
|
73
|
+
]),
|
|
49
74
|
);
|
|
75
|
+
}
|
|
50
76
|
|
|
51
|
-
|
|
77
|
+
function setSpec(name) {
|
|
78
|
+
return packed()[name] ?? name;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The packed packages also as pnpm overrides, so a packed core resolves the
|
|
83
|
+
* packed icons it depends on rather than a registry version that may not be
|
|
84
|
+
* published yet. Appended to the generated workspace file in harness mode.
|
|
85
|
+
*/
|
|
86
|
+
function packOverrides() {
|
|
87
|
+
const entries = Object.entries(packed());
|
|
88
|
+
if (entries.length === 0) return "";
|
|
89
|
+
|
|
90
|
+
return `
|
|
91
|
+
# Dev harness: Set packages from CREATE_SET_PACK_DIR, transitively too.
|
|
92
|
+
overrides:
|
|
93
|
+
${entries.map(([name, tarball]) => ` "${name}": "file:${tarball}"`).join("\n")}
|
|
94
|
+
`;
|
|
52
95
|
}
|
|
53
96
|
const git = (args, cwd) => run("git", args, cwd);
|
|
54
97
|
|
|
98
|
+
const specs = (dependencies) =>
|
|
99
|
+
Object.entries(dependencies).map(([name, range]) => `${name}@${range}`);
|
|
100
|
+
|
|
55
101
|
/**
|
|
56
102
|
* The tools the config package drives, as `name@range` specs. Its presets
|
|
57
103
|
* declare them as peers, so they install at the range it supports rather
|
|
@@ -65,9 +111,7 @@ function configPeerSpecs(root) {
|
|
|
65
111
|
),
|
|
66
112
|
);
|
|
67
113
|
|
|
68
|
-
return
|
|
69
|
-
([name, range]) => `${name}@${range}`,
|
|
70
|
-
);
|
|
114
|
+
return specs(manifest.peerDependencies);
|
|
71
115
|
}
|
|
72
116
|
|
|
73
117
|
/**
|
|
@@ -77,13 +121,14 @@ function configPeerSpecs(root) {
|
|
|
77
121
|
* @param {{ dir: string } & import("./plan.mjs").Answers} answers
|
|
78
122
|
* @param {{ step(message: string): void, warn(message: string): void }} report
|
|
79
123
|
*/
|
|
80
|
-
export function execute({ dir, ...answers }, report) {
|
|
124
|
+
export async function execute({ dir, ...answers }, report) {
|
|
81
125
|
const root = resolve(dir);
|
|
82
126
|
const files = plan(answers);
|
|
127
|
+
const react = answers.variant === "react";
|
|
83
128
|
|
|
84
129
|
report.step("Initialising git");
|
|
85
130
|
mkdirSync(root, { recursive: true });
|
|
86
|
-
git(["init", "--initial-branch=main"], root);
|
|
131
|
+
await git(["init", "--initial-branch=main"], root);
|
|
87
132
|
|
|
88
133
|
report.step("Writing files");
|
|
89
134
|
for (const [path, content] of Object.entries(files)) {
|
|
@@ -91,32 +136,49 @@ export function execute({ dir, ...answers }, report) {
|
|
|
91
136
|
mkdirSync(dirname(target), { recursive: true });
|
|
92
137
|
writeFileSync(target, content);
|
|
93
138
|
}
|
|
139
|
+
appendFileSync(join(root, "pnpm-workspace.yaml"), packOverrides());
|
|
94
140
|
|
|
95
141
|
// Lifecycle scripts wait until everything `prepare` needs is installed.
|
|
96
142
|
report.step("Installing Set");
|
|
97
|
-
pnpm(
|
|
143
|
+
await pnpm(
|
|
144
|
+
[
|
|
145
|
+
"add",
|
|
146
|
+
"--ignore-scripts",
|
|
147
|
+
...RUNTIME_DEPENDENCIES.map(setSpec),
|
|
148
|
+
...(react ? REACT_RUNTIME_DEPENDENCIES.map(setSpec) : []),
|
|
149
|
+
...(react ? specs(REACT.dependencies) : []),
|
|
150
|
+
],
|
|
151
|
+
root,
|
|
152
|
+
);
|
|
98
153
|
|
|
99
154
|
report.step("Installing tooling");
|
|
100
|
-
pnpm(
|
|
101
|
-
[
|
|
155
|
+
await pnpm(
|
|
156
|
+
[
|
|
157
|
+
"add",
|
|
158
|
+
"--ignore-scripts",
|
|
159
|
+
"--save-dev",
|
|
160
|
+
...SET_DEV_DEPENDENCIES.map(setSpec),
|
|
161
|
+
...specs(TOOLING),
|
|
162
|
+
...(react ? specs(REACT.devDependencies) : []),
|
|
163
|
+
],
|
|
102
164
|
root,
|
|
103
165
|
);
|
|
104
|
-
pnpm(
|
|
166
|
+
await pnpm(
|
|
105
167
|
["add", "--ignore-scripts", "--save-dev", ...configPeerSpecs(root)],
|
|
106
168
|
root,
|
|
107
169
|
);
|
|
108
170
|
|
|
109
171
|
// Explicit: `pnpm install` skips lifecycle scripts when nothing changed.
|
|
110
172
|
report.step("Syncing Set artifacts and git hooks");
|
|
111
|
-
pnpm(["run", "prepare"], root);
|
|
173
|
+
await pnpm(["run", "prepare"], root);
|
|
112
174
|
|
|
113
175
|
report.step("Formatting");
|
|
114
|
-
pnpm(["exec", "prettier", "--write", "."], root);
|
|
176
|
+
await pnpm(["exec", "prettier", "--write", "."], root);
|
|
115
177
|
|
|
116
178
|
report.step("Committing");
|
|
117
|
-
git(["add", "--all"], root);
|
|
179
|
+
await git(["add", "--all"], root);
|
|
118
180
|
try {
|
|
119
|
-
git(
|
|
181
|
+
await git(
|
|
120
182
|
["commit", "--message", "chore: bootstrap with @monospaced/create-set"],
|
|
121
183
|
root,
|
|
122
184
|
);
|
package/src/page.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
renderSetBox,
|
|
3
3
|
renderSetContainer,
|
|
4
|
-
renderSetDivider,
|
|
5
4
|
renderSetHeading,
|
|
6
5
|
renderSetInline,
|
|
7
6
|
renderSetLightswitch,
|
|
@@ -26,19 +25,18 @@ export const escapeHtml = (value) =>
|
|
|
26
25
|
* root's first child: earlier and the root doesn't exist yet, later and
|
|
27
26
|
* content above it may already have painted in the wrong theme.
|
|
28
27
|
*/
|
|
29
|
-
const themeBootstrap = `<script>try{var t=localStorage.getItem(${JSON.stringify(
|
|
28
|
+
export const themeBootstrap = `<script>try{var t=localStorage.getItem(${JSON.stringify(
|
|
30
29
|
SET_LIGHTSWITCH_STORAGE_KEY,
|
|
31
30
|
)});if(t==="light"||t==="dark")document.currentScript.closest(".set").setAttribute("data-set-theme",t)}catch(e){}</script>`;
|
|
32
31
|
|
|
33
32
|
/**
|
|
34
|
-
* The
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* The starter page, built from Set components only and rendered with core at
|
|
34
|
+
* generation time. The `prerender` variant writes the same composition as
|
|
35
|
+
* `src/document.ts` and `src/pages/index.ts`.
|
|
37
36
|
*/
|
|
38
|
-
export function renderHelloPage({ title }) {
|
|
39
|
-
const safeTitle = escapeHtml(title);
|
|
40
|
-
|
|
37
|
+
export function renderHelloPage({ editHint, title }) {
|
|
41
38
|
const header = renderSetContainer({
|
|
39
|
+
maxInlineSize: "none",
|
|
42
40
|
children: renderSetBox({
|
|
43
41
|
background: "transparent",
|
|
44
42
|
paddingBlock: "2xs",
|
|
@@ -47,7 +45,7 @@ export function renderHelloPage({ title }) {
|
|
|
47
45
|
gap: "sm",
|
|
48
46
|
justify: "between",
|
|
49
47
|
children:
|
|
50
|
-
renderSetHeading({ children:
|
|
48
|
+
renderSetHeading({ children: escapeHtml(title), size: "md" }) +
|
|
51
49
|
renderSetLightswitch({ size: "sm" }),
|
|
52
50
|
}),
|
|
53
51
|
}),
|
|
@@ -69,31 +67,18 @@ export function renderHelloPage({ title }) {
|
|
|
69
67
|
}) +
|
|
70
68
|
renderSetText({
|
|
71
69
|
as: "p",
|
|
72
|
-
children:
|
|
70
|
+
children: `Edit <code>${editHint}</code> to begin.`,
|
|
73
71
|
responsive: true,
|
|
72
|
+
size: "sm",
|
|
74
73
|
}),
|
|
75
74
|
}),
|
|
76
75
|
}),
|
|
77
76
|
});
|
|
78
77
|
|
|
79
|
-
|
|
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
|
-
|
|
78
|
+
// Page always renders its footer region; the starter leaves it empty.
|
|
94
79
|
const page = renderSetPage({
|
|
95
80
|
children: main,
|
|
96
|
-
footer,
|
|
81
|
+
footer: "",
|
|
97
82
|
header,
|
|
98
83
|
headerBorder: "always",
|
|
99
84
|
headerSize: "sm",
|
package/src/plan.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { NODE_MAJOR, PNPM_VERSION } from "./toolchain.mjs";
|
|
|
6
6
|
* @property {string} name Package name.
|
|
7
7
|
* @property {string} title Site title.
|
|
8
8
|
* @property {string} description Site description.
|
|
9
|
+
* @property {"html" | "prerender"} variant How pages are authored.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -23,8 +24,10 @@ export function plan(answers) {
|
|
|
23
24
|
pnpmVersion: PNPM_VERSION,
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
const shared = {
|
|
27
28
|
".editorconfig": templates.editorconfig(),
|
|
29
|
+
".github/dependabot.yml": templates.dependabotYml(),
|
|
30
|
+
".github/workflows/ci.yml": templates.ciYml(),
|
|
28
31
|
".gitignore": templates.gitignore(),
|
|
29
32
|
".husky/pre-commit": "pnpm exec lint-staged\n",
|
|
30
33
|
".node-version": `${NODE_MAJOR}\n`,
|
|
@@ -33,15 +36,40 @@ export function plan(answers) {
|
|
|
33
36
|
".vscode/extensions.json": templates.vscodeExtensions(),
|
|
34
37
|
".vscode/settings.json": templates.vscodeSettings(),
|
|
35
38
|
"README.md": templates.readme(context),
|
|
36
|
-
"eslint.config.mjs": templates.eslintConfig(),
|
|
37
|
-
"index.html": templates.indexHtml(context),
|
|
39
|
+
"eslint.config.mjs": templates.eslintConfig(context),
|
|
38
40
|
"package.json": templates.packageJson(context),
|
|
39
41
|
"pnpm-workspace.yaml": templates.pnpmWorkspace(),
|
|
40
42
|
"public/manifest.webmanifest": templates.manifest(context),
|
|
41
43
|
"scripts/sync-set.mjs": templates.syncSet(),
|
|
42
|
-
"src/main.ts": templates.mainTs(),
|
|
43
44
|
"src/styles.css": templates.stylesCss(),
|
|
44
|
-
"tsconfig.json": templates.tsconfig(),
|
|
45
|
-
"vite.config.ts": templates.viteConfig(),
|
|
45
|
+
"tsconfig.json": templates.tsconfig(context),
|
|
46
|
+
"vite.config.ts": templates.viteConfig(context),
|
|
46
47
|
};
|
|
48
|
+
|
|
49
|
+
const variants = {
|
|
50
|
+
html: {
|
|
51
|
+
"index.html": templates.indexHtml(context),
|
|
52
|
+
"src/main.ts": templates.mainTs(),
|
|
53
|
+
},
|
|
54
|
+
prerender: {
|
|
55
|
+
"index.html": templates.routeStub(),
|
|
56
|
+
"src/document.ts": templates.documentTs(),
|
|
57
|
+
"src/main.ts": templates.mainTs(),
|
|
58
|
+
"src/pages.ts": templates.pagesTs(),
|
|
59
|
+
"src/pages/index.ts": templates.indexPageTs(),
|
|
60
|
+
"src/site.ts": templates.siteTs(context),
|
|
61
|
+
},
|
|
62
|
+
react: {
|
|
63
|
+
"index.html": templates.indexHtml(context),
|
|
64
|
+
"src/App.tsx": templates.appTsx(context),
|
|
65
|
+
"src/main.tsx": templates.mainTsx(),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const variant = variants[answers.variant];
|
|
69
|
+
|
|
70
|
+
return Object.fromEntries(
|
|
71
|
+
Object.entries({ ...shared, ...variant }).sort(([a], [b]) =>
|
|
72
|
+
a.localeCompare(b),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
47
75
|
}
|
package/src/templates.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OG_IMAGE } from "./constants.mjs";
|
|
2
|
-
import { escapeHtml, renderHelloPage } from "./page.mjs";
|
|
2
|
+
import { escapeHtml, renderHelloPage, themeBootstrap } from "./page.mjs";
|
|
3
3
|
|
|
4
4
|
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
5
5
|
|
|
@@ -34,12 +34,12 @@ export const packageJson = ({ description, name, nodeMajor, pnpmVersion }) => {
|
|
|
34
34
|
"lint-staged": {
|
|
35
35
|
"*": "prettier --ignore-unknown --write",
|
|
36
36
|
"*.css": "stylelint --fix",
|
|
37
|
-
"*.{js,mjs,ts}": "eslint --fix",
|
|
37
|
+
"*.{js,jsx,mjs,ts,tsx}": "eslint --fix",
|
|
38
38
|
},
|
|
39
39
|
});
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
export const indexHtml = ({ description, title }) => `<!doctype html>
|
|
42
|
+
export const indexHtml = ({ description, title, variant }) => `<!doctype html>
|
|
43
43
|
<html lang="en">
|
|
44
44
|
<head>
|
|
45
45
|
<meta charset="utf-8" />
|
|
@@ -67,8 +67,13 @@ export const indexHtml = ({ description, title }) => `<!doctype html>
|
|
|
67
67
|
<link rel="manifest" href="/manifest.webmanifest" />
|
|
68
68
|
</head>
|
|
69
69
|
<body>
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
${
|
|
71
|
+
variant === "react"
|
|
72
|
+
? ` <div id="root"></div>
|
|
73
|
+
<script type="module" src="/src/main.tsx"></script>`
|
|
74
|
+
: ` ${renderHelloPage({ editHint: "index.html", title })}
|
|
75
|
+
<script type="module" src="/src/main.ts"></script>`
|
|
76
|
+
}
|
|
72
77
|
</body>
|
|
73
78
|
</html>
|
|
74
79
|
`;
|
|
@@ -107,20 +112,315 @@ export const stylesCss =
|
|
|
107
112
|
its own CSS, write it here with Set tokens. */
|
|
108
113
|
`;
|
|
109
114
|
|
|
110
|
-
export const viteConfig =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
export const viteConfig = ({ variant }) => {
|
|
116
|
+
const prerender = variant === "prerender";
|
|
117
|
+
const react = variant === "react";
|
|
118
|
+
|
|
119
|
+
return `import { globSync } from "node:fs";
|
|
114
120
|
|
|
121
|
+
import target from "@monospaced/set-config/browserslist/esbuild";
|
|
122
|
+
import targets from "@monospaced/set-config/browserslist/lightningcss";
|
|
123
|
+
${react ? 'import react from "@vitejs/plugin-react";\n' : ""}import { defineConfig${prerender ? ", type Plugin" : ""} } from "${react ? "vitest/config" : "vite"}";
|
|
124
|
+
${
|
|
125
|
+
prerender
|
|
126
|
+
? `
|
|
127
|
+
import { renderDocument } from "./src/document.ts";
|
|
128
|
+
import { pages } from "./src/pages.ts";
|
|
129
|
+
`
|
|
130
|
+
: ""
|
|
131
|
+
}
|
|
132
|
+
// Every index.html is a page; its folder is its route.
|
|
133
|
+
const input = globSync("**/index.html", {
|
|
134
|
+
exclude: ["dist/**", "node_modules/**"],
|
|
135
|
+
});
|
|
136
|
+
${
|
|
137
|
+
prerender
|
|
138
|
+
? `
|
|
139
|
+
// Renders each route's document from src/pages at dev and build time, so the
|
|
140
|
+
// render functions never ship to the browser. The page modules are imported
|
|
141
|
+
// above, so the dev server restarts when one changes.
|
|
142
|
+
const prerender: Plugin = {
|
|
143
|
+
name: "set-prerender",
|
|
144
|
+
transformIndexHtml: {
|
|
145
|
+
order: "pre",
|
|
146
|
+
handler: (_html, { path }) => {
|
|
147
|
+
const route = path.replace(/index\\.html$/, "");
|
|
148
|
+
const page = pages[route];
|
|
149
|
+
if (!page) throw new Error(\`No page for \${route}. Add it to src/pages.ts.\`);
|
|
150
|
+
return renderDocument(page, route);
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
`
|
|
155
|
+
: ""
|
|
156
|
+
}
|
|
115
157
|
export default defineConfig({
|
|
116
|
-
build: { cssMinify: "lightningcss", target },
|
|
117
|
-
css: { lightningcss: { targets }, transformer: "lightningcss" }
|
|
158
|
+
build: { cssMinify: "lightningcss", rollupOptions: { input }, target },
|
|
159
|
+
css: { lightningcss: { targets }, transformer: "lightningcss" },${
|
|
160
|
+
prerender ? "\n plugins: [prerender]," : ""
|
|
161
|
+
}${react ? '\n plugins: [react()],\n test: { environment: "jsdom" },' : ""}
|
|
118
162
|
});
|
|
119
163
|
`;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const mainTsx = () => `import "@monospaced/set-assets/fonts.css";
|
|
167
|
+
import "@monospaced/set-core/styles.css";
|
|
168
|
+
import "./styles.css";
|
|
169
|
+
|
|
170
|
+
import { SET_LIGHTSWITCH_STORAGE_KEY } from "@monospaced/set-core";
|
|
171
|
+
import { defineSetAll } from "@monospaced/set-react";
|
|
172
|
+
import { StrictMode } from "react";
|
|
173
|
+
import { createRoot } from "react-dom/client";
|
|
174
|
+
|
|
175
|
+
import { App } from "./App.tsx";
|
|
176
|
+
|
|
177
|
+
defineSetAll();
|
|
178
|
+
|
|
179
|
+
// A stored lightswitch override paints from the first render; Lightswitch
|
|
180
|
+
// keeps it in sync after mount.
|
|
181
|
+
function storedTheme(): "light" | "dark" | undefined {
|
|
182
|
+
try {
|
|
183
|
+
const theme = localStorage.getItem(SET_LIGHTSWITCH_STORAGE_KEY);
|
|
184
|
+
return theme === "light" || theme === "dark" ? theme : undefined;
|
|
185
|
+
} catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const container = document.getElementById("root");
|
|
191
|
+
if (!container) throw new Error("#root not found");
|
|
192
|
+
|
|
193
|
+
createRoot(container).render(
|
|
194
|
+
<StrictMode>
|
|
195
|
+
<App theme={storedTheme()} />
|
|
196
|
+
</StrictMode>,
|
|
197
|
+
);
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
export const appTsx = ({ title }) => `import {
|
|
201
|
+
Box,
|
|
202
|
+
Container,
|
|
203
|
+
Heading,
|
|
204
|
+
Inline,
|
|
205
|
+
Lightswitch,
|
|
206
|
+
Page,
|
|
207
|
+
Root,
|
|
208
|
+
Stack,
|
|
209
|
+
Text,
|
|
210
|
+
} from "@monospaced/set-react";
|
|
211
|
+
|
|
212
|
+
export function App({ theme }: { theme?: "light" | "dark" }) {
|
|
213
|
+
return (
|
|
214
|
+
<Root appOverscrollBehavior="none" appRoot theme={theme}>
|
|
215
|
+
<Page
|
|
216
|
+
footer={null}
|
|
217
|
+
header={
|
|
218
|
+
<Container maxInlineSize="none">
|
|
219
|
+
<Box background="transparent" paddingBlock="2xs" paddingInline="none">
|
|
220
|
+
<Inline gap="sm" justify="between">
|
|
221
|
+
<Heading size="md">${escapeHtml(title)}</Heading>
|
|
222
|
+
<Lightswitch size="sm" />
|
|
223
|
+
</Inline>
|
|
224
|
+
</Box>
|
|
225
|
+
</Container>
|
|
226
|
+
}
|
|
227
|
+
headerBorder="always"
|
|
228
|
+
headerSize="sm"
|
|
229
|
+
>
|
|
230
|
+
<Container>
|
|
231
|
+
<Box paddingBlock="lg" paddingInline="none" responsive>
|
|
232
|
+
<Stack gap="md">
|
|
233
|
+
<Heading level={1} responsive size="xl">
|
|
234
|
+
Hello, world
|
|
235
|
+
</Heading>
|
|
236
|
+
<Text as="p" responsive size="sm">
|
|
237
|
+
Edit <code>src/App.tsx</code> to begin.
|
|
238
|
+
</Text>
|
|
239
|
+
</Stack>
|
|
240
|
+
</Box>
|
|
241
|
+
</Container>
|
|
242
|
+
</Page>
|
|
243
|
+
</Root>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
|
|
248
|
+
export const routeStub = () => `<!doctype html>
|
|
249
|
+
<!-- Route marker. This page is rendered from src/pages.ts by the plugin in vite.config.ts. -->
|
|
250
|
+
`;
|
|
251
|
+
|
|
252
|
+
export const siteTs = ({ description, title }) => `export const site = {
|
|
253
|
+
name: ${JSON.stringify(title)},
|
|
254
|
+
description: ${JSON.stringify(description)},
|
|
255
|
+
// Set once the site has a domain; canonical and og:url derive from it.
|
|
256
|
+
url: "https://example.com",
|
|
257
|
+
ogImage: ${JSON.stringify(OG_IMAGE)},
|
|
258
|
+
};
|
|
259
|
+
`;
|
|
260
|
+
|
|
261
|
+
export const documentTs = () => `import {
|
|
262
|
+
renderSetBox,
|
|
263
|
+
renderSetContainer,
|
|
264
|
+
renderSetHeading,
|
|
265
|
+
renderSetInline,
|
|
266
|
+
renderSetLightswitch,
|
|
267
|
+
renderSetPage,
|
|
268
|
+
renderSetRoot,
|
|
269
|
+
} from "@monospaced/set-core";
|
|
270
|
+
|
|
271
|
+
import { site } from "./site.ts";
|
|
272
|
+
|
|
273
|
+
export interface Page {
|
|
274
|
+
/** Page title. The site name alone when omitted. */
|
|
275
|
+
title?: string;
|
|
276
|
+
/** Meta description. The site description when omitted. */
|
|
277
|
+
description?: string;
|
|
278
|
+
/** The main region, from Set's render functions. */
|
|
279
|
+
render: () => string | Promise<string>;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const escapeHtml = (value: string): string =>
|
|
283
|
+
value
|
|
284
|
+
.replaceAll("&", "&")
|
|
285
|
+
.replaceAll("<", "<")
|
|
286
|
+
.replaceAll(">", ">")
|
|
287
|
+
.replaceAll('"', """);
|
|
288
|
+
|
|
289
|
+
/*
|
|
290
|
+
* Stamps a stored lightswitch override onto the root before its content
|
|
291
|
+
* parses, so an overridden theme paints correctly on first load. Must be the
|
|
292
|
+
* root's first child: earlier and the root doesn't exist yet, later and
|
|
293
|
+
* content above it may already have painted in the wrong theme.
|
|
294
|
+
*/
|
|
295
|
+
const themeBootstrap = ${JSON.stringify(themeBootstrap)};
|
|
296
|
+
|
|
297
|
+
const header = (): string =>
|
|
298
|
+
renderSetContainer({
|
|
299
|
+
maxInlineSize: "none",
|
|
300
|
+
children: renderSetBox({
|
|
301
|
+
background: "transparent",
|
|
302
|
+
paddingBlock: "2xs",
|
|
303
|
+
paddingInline: "none",
|
|
304
|
+
children: renderSetInline({
|
|
305
|
+
gap: "sm",
|
|
306
|
+
justify: "between",
|
|
307
|
+
children:
|
|
308
|
+
renderSetHeading({ children: escapeHtml(site.name), size: "md" }) +
|
|
309
|
+
renderSetLightswitch({ size: "sm" }),
|
|
310
|
+
}),
|
|
311
|
+
}),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
/** The full HTML document for \`page\` at \`route\`. */
|
|
315
|
+
export async function renderDocument(page: Page, route: string): Promise<string> {
|
|
316
|
+
const title = page.title ? \`\${page.title} | \${site.name}\` : site.name;
|
|
317
|
+
const description = page.description ?? site.description;
|
|
318
|
+
const url = \`\${site.url}\${route}\`;
|
|
319
|
+
|
|
320
|
+
// Page always renders its footer region; the starter leaves it empty.
|
|
321
|
+
const body = renderSetRoot({
|
|
322
|
+
appOverscrollBehavior: "none",
|
|
323
|
+
appRoot: true,
|
|
324
|
+
children:
|
|
325
|
+
themeBootstrap +
|
|
326
|
+
renderSetPage({
|
|
327
|
+
children: await page.render(),
|
|
328
|
+
footer: "",
|
|
329
|
+
header: header(),
|
|
330
|
+
headerBorder: "always",
|
|
331
|
+
headerSize: "sm",
|
|
332
|
+
}),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return \`<!doctype html>
|
|
336
|
+
<html lang="en">
|
|
337
|
+
<head>
|
|
338
|
+
<meta charset="utf-8" />
|
|
339
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
340
|
+
<meta name="color-scheme" content="light dark" />
|
|
341
|
+
<meta name="theme-color" content="#f8fbfb" media="(prefers-color-scheme: light)" />
|
|
342
|
+
<meta name="theme-color" content="#0e0f0f" media="(prefers-color-scheme: dark)" />
|
|
343
|
+
<title>\${escapeHtml(title)}</title>
|
|
344
|
+
<meta name="description" content="\${escapeHtml(description)}" />
|
|
345
|
+
<link rel="canonical" href="\${escapeHtml(url)}" />
|
|
346
|
+
<meta property="og:type" content="website" />
|
|
347
|
+
<meta property="og:site_name" content="\${escapeHtml(site.name)}" />
|
|
348
|
+
<meta property="og:title" content="\${escapeHtml(title)}" />
|
|
349
|
+
<meta property="og:description" content="\${escapeHtml(description)}" />
|
|
350
|
+
<meta property="og:url" content="\${escapeHtml(url)}" />
|
|
351
|
+
<meta property="og:image" content="\${site.ogImage}" />
|
|
352
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
353
|
+
<meta name="twitter:title" content="\${escapeHtml(title)}" />
|
|
354
|
+
<meta name="twitter:description" content="\${escapeHtml(description)}" />
|
|
355
|
+
<meta name="twitter:image" content="\${site.ogImage}" />
|
|
356
|
+
<link rel="icon" href="/favicons/favicon.ico" sizes="32x32" />
|
|
357
|
+
<link rel="icon" href="/favicons/favicon.svg" type="image/svg+xml" />
|
|
358
|
+
<link rel="apple-touch-icon" href="/favicons/apple-touch-icon.png" />
|
|
359
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
360
|
+
</head>
|
|
361
|
+
<body>
|
|
362
|
+
\${body}
|
|
363
|
+
<script type="module" src="/src/main.ts"></script>
|
|
364
|
+
</body>
|
|
365
|
+
</html>
|
|
366
|
+
\`;
|
|
367
|
+
}
|
|
368
|
+
`;
|
|
369
|
+
|
|
370
|
+
export const pagesTs = () => `import type { Page } from "./document.ts";
|
|
371
|
+
import index from "./pages/index.ts";
|
|
372
|
+
|
|
373
|
+
/** Routes to pages. A route is a folder holding an index.html; add both. */
|
|
374
|
+
export const pages: Record<string, Page> = {
|
|
375
|
+
"/": index,
|
|
376
|
+
};
|
|
377
|
+
`;
|
|
120
378
|
|
|
121
|
-
export const
|
|
379
|
+
export const indexPageTs = () => `import {
|
|
380
|
+
renderSetBox,
|
|
381
|
+
renderSetContainer,
|
|
382
|
+
renderSetHeading,
|
|
383
|
+
renderSetStack,
|
|
384
|
+
renderSetText,
|
|
385
|
+
} from "@monospaced/set-core";
|
|
386
|
+
|
|
387
|
+
import type { Page } from "../document.ts";
|
|
388
|
+
|
|
389
|
+
const page: Page = {
|
|
390
|
+
render: () =>
|
|
391
|
+
renderSetContainer({
|
|
392
|
+
children: renderSetBox({
|
|
393
|
+
paddingBlock: "lg",
|
|
394
|
+
paddingInline: "none",
|
|
395
|
+
responsive: true,
|
|
396
|
+
children: renderSetStack({
|
|
397
|
+
gap: "md",
|
|
398
|
+
children:
|
|
399
|
+
renderSetHeading({
|
|
400
|
+
children: "Hello, world",
|
|
401
|
+
level: 1,
|
|
402
|
+
responsive: true,
|
|
403
|
+
size: "xl",
|
|
404
|
+
}) +
|
|
405
|
+
renderSetText({
|
|
406
|
+
as: "p",
|
|
407
|
+
children: "Edit <code>src/pages/index.ts</code> to begin.",
|
|
408
|
+
responsive: true,
|
|
409
|
+
size: "sm",
|
|
410
|
+
}),
|
|
411
|
+
}),
|
|
412
|
+
}),
|
|
413
|
+
}),
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
export default page;
|
|
417
|
+
`;
|
|
418
|
+
|
|
419
|
+
export const tsconfig = ({ variant }) =>
|
|
122
420
|
json({
|
|
123
421
|
compilerOptions: {
|
|
422
|
+
allowImportingTsExtensions: true,
|
|
423
|
+
...(variant === "react" ? { jsx: "react-jsx" } : {}),
|
|
124
424
|
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
125
425
|
module: "ESNext",
|
|
126
426
|
moduleResolution: "Bundler",
|
|
@@ -128,13 +428,14 @@ export const tsconfig = () =>
|
|
|
128
428
|
skipLibCheck: true,
|
|
129
429
|
strict: true,
|
|
130
430
|
target: "ES2022",
|
|
131
|
-
types: ["vite/client"],
|
|
431
|
+
types: ["node", "vite/client"],
|
|
132
432
|
},
|
|
133
433
|
include: ["src", "*.config.ts"],
|
|
134
434
|
});
|
|
135
435
|
|
|
136
|
-
export const eslintConfig =
|
|
137
|
-
|
|
436
|
+
export const eslintConfig = ({
|
|
437
|
+
variant,
|
|
438
|
+
}) => `import setEslint from "@monospaced/set-config/eslint${variant === "react" ? "/react" : ""}";
|
|
138
439
|
|
|
139
440
|
export default [...setEslint];
|
|
140
441
|
`;
|
|
@@ -195,6 +496,9 @@ export const vscodeExtensions = () =>
|
|
|
195
496
|
export const pnpmWorkspace =
|
|
196
497
|
() => `# Minimum age of a release before it installs, in minutes (3 days).
|
|
197
498
|
minimumReleaseAge: 4320
|
|
499
|
+
# Set's own packages install as soon as they are released.
|
|
500
|
+
minimumReleaseAgeExclude:
|
|
501
|
+
- "@monospaced/*"
|
|
198
502
|
`;
|
|
199
503
|
|
|
200
504
|
export const syncSet = () => `/*
|
|
@@ -232,7 +536,122 @@ if (existsSync(new URL(".git", \`file://\${root}\`))) {
|
|
|
232
536
|
}
|
|
233
537
|
`;
|
|
234
538
|
|
|
235
|
-
export const
|
|
539
|
+
export const ciYml = () => `name: ci
|
|
540
|
+
|
|
541
|
+
on:
|
|
542
|
+
pull_request:
|
|
543
|
+
push:
|
|
544
|
+
branches:
|
|
545
|
+
- main
|
|
546
|
+
|
|
547
|
+
permissions:
|
|
548
|
+
contents: read
|
|
549
|
+
|
|
550
|
+
jobs:
|
|
551
|
+
check:
|
|
552
|
+
runs-on: ubuntu-latest
|
|
553
|
+
|
|
554
|
+
steps:
|
|
555
|
+
- name: Checkout
|
|
556
|
+
uses: actions/checkout@v6
|
|
557
|
+
|
|
558
|
+
- name: Setup pnpm
|
|
559
|
+
uses: pnpm/action-setup@v6
|
|
560
|
+
|
|
561
|
+
- name: Setup Node
|
|
562
|
+
uses: actions/setup-node@v6
|
|
563
|
+
with:
|
|
564
|
+
node-version-file: .node-version
|
|
565
|
+
cache: pnpm
|
|
566
|
+
|
|
567
|
+
- name: Install
|
|
568
|
+
run: pnpm install --frozen-lockfile
|
|
569
|
+
|
|
570
|
+
- name: Lint
|
|
571
|
+
run: pnpm run lint
|
|
572
|
+
|
|
573
|
+
- name: Stylelint
|
|
574
|
+
run: pnpm run stylelint
|
|
575
|
+
|
|
576
|
+
- name: Format check
|
|
577
|
+
run: pnpm run format:check
|
|
578
|
+
|
|
579
|
+
- name: Typecheck
|
|
580
|
+
run: pnpm run typecheck
|
|
581
|
+
|
|
582
|
+
- name: Test
|
|
583
|
+
run: pnpm run test
|
|
584
|
+
|
|
585
|
+
- name: Build
|
|
586
|
+
run: pnpm run build
|
|
587
|
+
`;
|
|
588
|
+
|
|
589
|
+
export const dependabotYml = () => `version: 2
|
|
590
|
+
updates:
|
|
591
|
+
- package-ecosystem: npm
|
|
592
|
+
directory: /
|
|
593
|
+
schedule:
|
|
594
|
+
interval: weekly
|
|
595
|
+
# Mirrors minimumReleaseAge in pnpm-workspace.yaml, so proposals stay
|
|
596
|
+
# installable under it.
|
|
597
|
+
cooldown:
|
|
598
|
+
default-days: 3
|
|
599
|
+
groups:
|
|
600
|
+
minor-and-patch:
|
|
601
|
+
update-types:
|
|
602
|
+
- minor
|
|
603
|
+
- patch
|
|
604
|
+
|
|
605
|
+
- package-ecosystem: github-actions
|
|
606
|
+
directory: /
|
|
607
|
+
schedule:
|
|
608
|
+
interval: weekly
|
|
609
|
+
`;
|
|
610
|
+
|
|
611
|
+
export const readme = ({ description, title, variant }) => {
|
|
612
|
+
const pages =
|
|
613
|
+
variant === "react"
|
|
614
|
+
? `## Pages
|
|
615
|
+
|
|
616
|
+
This variant is an app: one \`index.html\`, rendered on the client by React from \`src/main.tsx\`. \`src/App.tsx\` composes the page from \`@monospaced/set-react\` components, and \`Root\` takes its initial \`theme\` from the stored lightswitch choice so an overridden theme paints from the first render. Routes live in React, not in folders; for a site with pages as files, the \`prerender\` variant of \`@monospaced/create-set\` renders them from code.
|
|
617
|
+
|
|
618
|
+
Component tests run in jsdom (\`test.environment\` in \`vite.config.ts\`).
|
|
619
|
+
`
|
|
620
|
+
: variant === "prerender"
|
|
621
|
+
? `## Pages
|
|
622
|
+
|
|
623
|
+
A page is a folder with an \`index.html\` route marker and a module in \`src/pages\` that exports its title, description and a \`render()\` built from Set's render functions. \`src/pages.ts\` maps routes to modules, and \`src/document.ts\` wraps each page in the shared head and shell at build time. To add \`/about/\`:
|
|
624
|
+
|
|
625
|
+
1. Copy \`index.html\` to \`about/index.html\`.
|
|
626
|
+
2. Add \`src/pages/about.ts\` (or \`src/pages/about/index.ts\`, to keep content beside it).
|
|
627
|
+
3. Register it in \`src/pages.ts\`.
|
|
628
|
+
|
|
629
|
+
\`renderDocument\` in \`src/document.ts\` owns the shell around every page and receives the route, so a nav in its header can mark the current link (\`current: "page"\` on \`renderSetLink\`) by comparing hrefs to it.
|
|
630
|
+
|
|
631
|
+
Page modules are imported by \`vite.config.ts\`, so the dev server restarts when one changes rather than hot-replacing. Keep them to render functions and data: no CSS imports and no DOM at the top level, so \`main.ts\` can import one to re-render on the client.
|
|
632
|
+
|
|
633
|
+
For Markdown content, read the file beside the page and render it in \`Prose\` with \`@monospaced/set-markdown\` (\`pnpm add @monospaced/set-markdown\`):
|
|
634
|
+
|
|
635
|
+
\`\`\`ts
|
|
636
|
+
import { readFileSync } from "node:fs";
|
|
637
|
+
|
|
638
|
+
import { renderSetProse } from "@monospaced/set-core";
|
|
639
|
+
import { processMarkdown } from "@monospaced/set-markdown";
|
|
640
|
+
|
|
641
|
+
const content = readFileSync(new URL("./content.md", import.meta.url), "utf8");
|
|
642
|
+
|
|
643
|
+
export default {
|
|
644
|
+
title: "About",
|
|
645
|
+
render: () => renderSetProse({ children: processMarkdown(content) }),
|
|
646
|
+
};
|
|
647
|
+
\`\`\`
|
|
648
|
+
`
|
|
649
|
+
: `## Pages
|
|
650
|
+
|
|
651
|
+
A page is a folder with an \`index.html\`; its path is its URL (\`about/index.html\` is \`/about/\`). Vite discovers them, so adding a page is adding the folder. Each page carries its own head and its own \`<script type="module" src="/src/main.ts">\`; copy \`index.html\` to start one. When keeping the heads in step by hand becomes a chore, the \`prerender\` variant of \`@monospaced/create-set\` renders pages from code with one shared head.
|
|
652
|
+
`;
|
|
653
|
+
|
|
654
|
+
return `# ${title}
|
|
236
655
|
|
|
237
656
|
${description}
|
|
238
657
|
|
|
@@ -247,11 +666,20 @@ Built with [Set](https://set.monospaced.com), scaffolded by \`@monospaced/create
|
|
|
247
666
|
- \`pnpm typecheck\`
|
|
248
667
|
- \`pnpm test\`
|
|
249
668
|
|
|
250
|
-
Prettier, Stylelint and ESLint fixes run on staged files before every commit via Husky and lint-staged.
|
|
669
|
+
Prettier, Stylelint and ESLint fixes run on staged files before every commit via Husky and lint-staged. CI runs the same checks plus typecheck, test and build on every pull request (\`.github/workflows/ci.yml\`), and Dependabot proposes grouped updates weekly.
|
|
251
670
|
|
|
671
|
+
${pages}
|
|
252
672
|
## After bootstrapping
|
|
253
673
|
|
|
254
|
-
-
|
|
674
|
+
- ${
|
|
675
|
+
variant === "prerender"
|
|
676
|
+
? "Set `url` in `src/site.ts` once the site has a domain; canonical URLs derive from it."
|
|
677
|
+
: "Set the canonical URL and `og:url` in `index.html` once the site has a domain."
|
|
678
|
+
}
|
|
255
679
|
- \`.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
|
-
|
|
257
|
-
|
|
680
|
+
${
|
|
681
|
+
variant === "prerender"
|
|
682
|
+
? ""
|
|
683
|
+
: "- Markdown content? `pnpm add @monospaced/set-markdown` renders GFM to safe HTML that pairs with core's `prose` component.\n"
|
|
684
|
+
}`;
|
|
685
|
+
};
|
package/src/toolchain.mjs
CHANGED
|
@@ -5,3 +5,24 @@ export const NODE_MAJOR = 26;
|
|
|
5
5
|
|
|
6
6
|
/** pnpm version written to `packageManager`; `engines` takes its major. */
|
|
7
7
|
export const PNPM_VERSION = "12.17.0";
|
|
8
|
+
|
|
9
|
+
/** Tooling installed into every project, at the monorepo's ranges. */
|
|
10
|
+
export const TOOLING = {
|
|
11
|
+
"@types/node": "^26.6.6",
|
|
12
|
+
husky: "^9.1.7",
|
|
13
|
+
"lint-staged": "^17.0.4",
|
|
14
|
+
prettier: "^3.8.3",
|
|
15
|
+
vite: "^7.3.2",
|
|
16
|
+
vitest: "^4.1.6",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** What the React variant adds, at the monorepo's ranges. */
|
|
20
|
+
export const REACT = {
|
|
21
|
+
dependencies: { react: "^19.2.6", "react-dom": "^19.2.6" },
|
|
22
|
+
devDependencies: {
|
|
23
|
+
"@types/react": "^19.2.14",
|
|
24
|
+
"@types/react-dom": "^19.2.3",
|
|
25
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
26
|
+
jsdom: "^29.1.1",
|
|
27
|
+
},
|
|
28
|
+
};
|