@codemeall/create-word-pages 0.1.2 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemeall/create-word-pages",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Scaffold a Word Pages Obsidian-to-GitHub-Pages starter.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,32 @@
1
+ const baseUrlPattern = /^[A-Za-z0-9.-]+(\/[A-Za-z0-9._~-]+)*$/
2
+
3
+ export const requiredSiteFields = ["title", "githubUsername", "repositoryName", "baseUrl"]
4
+
5
+ export function validateWordPagesConfig(config) {
6
+ const fieldErrors = {}
7
+ const site = config?.site ?? {}
8
+
9
+ for (const field of requiredSiteFields) {
10
+ if (!String(site[field] ?? "").trim()) {
11
+ fieldErrors[field] = "Required"
12
+ }
13
+ }
14
+
15
+ const baseUrl = String(site.baseUrl ?? "").trim()
16
+ if (baseUrl) {
17
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(baseUrl)) {
18
+ fieldErrors.baseUrl = "Use the Pages URL without https://"
19
+ } else if (/\s/.test(baseUrl)) {
20
+ fieldErrors.baseUrl = "Remove spaces from the Pages URL"
21
+ } else if (baseUrl.startsWith("/") || baseUrl.endsWith("/")) {
22
+ fieldErrors.baseUrl = "Do not use leading or trailing slashes"
23
+ } else if (!baseUrlPattern.test(baseUrl)) {
24
+ fieldErrors.baseUrl = "Use a value like octocat.github.io/my-site"
25
+ }
26
+ }
27
+
28
+ return {
29
+ ok: Object.keys(fieldErrors).length === 0,
30
+ fieldErrors
31
+ }
32
+ }
@@ -1,5 +1,6 @@
1
1
  import React, { useEffect, useMemo, useState } from "react"
2
2
  import { createRoot } from "react-dom/client"
3
+ import { validateWordPagesConfig } from "./configValidation.js"
3
4
  import "./styles.css"
4
5
 
5
6
  type SaveState = "idle" | "dirty" | "saving" | "saved" | "failed"
@@ -23,6 +24,8 @@ type WordPagesConfig = {
23
24
  }
24
25
  }
25
26
 
27
+ type SiteFieldErrors = Partial<Record<keyof WordPagesConfig["site"], string>>
28
+
26
29
  function deriveBaseUrl(username: string, repositoryName: string) {
27
30
  if (!username || !repositoryName) return ""
28
31
  if (repositoryName.toLowerCase() === `${username.toLowerCase()}.github.io`) return `${username}.github.io`
@@ -63,6 +66,12 @@ function App() {
63
66
  return displayPagesUrl(deriveBaseUrl(config.site.githubUsername, config.site.repositoryName))
64
67
  }, [config])
65
68
 
69
+ const fieldErrors = useMemo<SiteFieldErrors>(() => {
70
+ if (!config) return {}
71
+ return validateWordPagesConfig(config).fieldErrors
72
+ }, [config])
73
+ const canSave = Object.keys(fieldErrors).length === 0 && saveState !== "saving"
74
+
66
75
  if (!config) {
67
76
  return (
68
77
  <main className="shell">
@@ -95,6 +104,14 @@ function App() {
95
104
  }
96
105
 
97
106
  async function save() {
107
+ const validation = validateWordPagesConfig(config)
108
+ if (!validation.ok) {
109
+ setStatus("Required fields missing")
110
+ setSaveState("failed")
111
+ setMessage("Complete the required fields before saving word-pages.config.json.")
112
+ return
113
+ }
114
+
98
115
  try {
99
116
  setStatus("Saving")
100
117
  setSaveState("saving")
@@ -107,6 +124,9 @@ function App() {
107
124
  })
108
125
  const result = await response.json().catch(() => ({ message: "No response body" }))
109
126
  if (!response.ok || result.ok === false) {
127
+ if (result.fieldErrors) {
128
+ throw new Error(result.message ?? "Complete the required fields before saving.")
129
+ }
110
130
  throw new Error(result.message ?? "Save failed")
111
131
  }
112
132
 
@@ -130,7 +150,7 @@ function App() {
130
150
  <h1>Configure your Obsidian-powered site.</h1>
131
151
  <p className="lede">This wizard writes local configuration only. It does not ask for GitHub credentials and does not upload your vault.</p>
132
152
  </div>
133
- <button type="button" onClick={save} disabled={saveState === "saving"}>
153
+ <button type="button" onClick={save} disabled={!canSave}>
134
154
  {saveState === "saving" ? "Saving..." : "Save changes"}
135
155
  </button>
136
156
  </section>
@@ -146,7 +166,8 @@ function App() {
146
166
  <section className="grid">
147
167
  <label>
148
168
  Site title
149
- <input value={config.site.title} onChange={(event) => updateSite("title", event.target.value)} />
169
+ <input value={config.site.title} onChange={(event) => updateSite("title", event.target.value)} aria-invalid={Boolean(fieldErrors.title)} />
170
+ {fieldErrors.title && <span className="field-error">{fieldErrors.title}</span>}
150
171
  </label>
151
172
  <label>
152
173
  Author or organization
@@ -158,11 +179,13 @@ function App() {
158
179
  </label>
159
180
  <label>
160
181
  GitHub username
161
- <input value={config.site.githubUsername} onChange={(event) => updateSite("githubUsername", event.target.value)} />
182
+ <input value={config.site.githubUsername} onChange={(event) => updateSite("githubUsername", event.target.value)} aria-invalid={Boolean(fieldErrors.githubUsername)} />
183
+ {fieldErrors.githubUsername && <span className="field-error">{fieldErrors.githubUsername}</span>}
162
184
  </label>
163
185
  <label>
164
186
  Repository name
165
- <input value={config.site.repositoryName} onChange={(event) => updateSite("repositoryName", event.target.value)} />
187
+ <input value={config.site.repositoryName} onChange={(event) => updateSite("repositoryName", event.target.value)} aria-invalid={Boolean(fieldErrors.repositoryName)} />
188
+ {fieldErrors.repositoryName && <span className="field-error">{fieldErrors.repositoryName}</span>}
166
189
  </label>
167
190
  <label>
168
191
  Repository visibility
@@ -174,7 +197,8 @@ function App() {
174
197
  </label>
175
198
  <label>
176
199
  GitHub Pages base URL
177
- <input value={config.site.baseUrl} onChange={(event) => updateSite("baseUrl", event.target.value)} />
200
+ <input value={config.site.baseUrl} onChange={(event) => updateSite("baseUrl", event.target.value)} aria-invalid={Boolean(fieldErrors.baseUrl)} />
201
+ {fieldErrors.baseUrl && <span className="field-error">{fieldErrors.baseUrl}</span>}
178
202
  </label>
179
203
  </section>
180
204
 
@@ -60,7 +60,7 @@ button {
60
60
  }
61
61
 
62
62
  button:disabled {
63
- cursor: wait;
63
+ cursor: not-allowed;
64
64
  opacity: 0.72;
65
65
  }
66
66
 
@@ -131,6 +131,17 @@ textarea {
131
131
  font: inherit;
132
132
  }
133
133
 
134
+ input[aria-invalid="true"] {
135
+ border-color: #b83232;
136
+ outline-color: #b83232;
137
+ }
138
+
139
+ .field-error {
140
+ color: #9f2525;
141
+ font-size: 0.88rem;
142
+ font-weight: 700;
143
+ }
144
+
134
145
  textarea {
135
146
  min-height: 104px;
136
147
  resize: vertical;
@@ -2,8 +2,23 @@ import { defineConfig } from "vite"
2
2
  import react from "@vitejs/plugin-react"
3
3
  import { readFile, writeFile } from "node:fs/promises"
4
4
  import path from "node:path"
5
+ import { validateWordPagesConfig } from "./src/configValidation.js"
5
6
 
6
7
  const configPath = path.resolve(process.cwd(), "word-pages.config.json")
8
+ let printedCompletionHandoff = false
9
+
10
+ function printCompletionHandoff() {
11
+ if (printedCompletionHandoff) return
12
+ printedCompletionHandoff = true
13
+ console.log(`
14
+ Word Pages setup saved.
15
+
16
+ Next steps:
17
+ 1. Open content/ in Obsidian and edit your pages, posts, and notes.
18
+ 2. In another terminal, run: npm run preview
19
+ 3. When ready, commit, push to GitHub, and enable Pages with GitHub Actions.
20
+ `)
21
+ }
7
22
 
8
23
  export default defineConfig({
9
24
  root: "wizard",
@@ -29,7 +44,20 @@ export default defineConfig({
29
44
  req.on("end", async () => {
30
45
  try {
31
46
  const parsed = JSON.parse(raw)
47
+ const validation = validateWordPagesConfig(parsed)
48
+ if (!validation.ok) {
49
+ res.statusCode = 400
50
+ res.setHeader("Content-Type", "application/json")
51
+ res.end(JSON.stringify({
52
+ ok: false,
53
+ message: "Complete the required fields before saving.",
54
+ fieldErrors: validation.fieldErrors
55
+ }))
56
+ return
57
+ }
58
+
32
59
  await writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`)
60
+ printCompletionHandoff()
33
61
  res.setHeader("Content-Type", "application/json")
34
62
  res.end(JSON.stringify({ ok: true, path: configPath }))
35
63
  } catch (error) {