prestruct 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Creadev.org (AUTHOR: dhaupin)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,160 @@
1
- # Temporary Holding Version
1
+ # Prestruct
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
+ **Build-time SEO prerender for Vite + React.** Validate your `ssr.config.js`,
4
+ build, inject meta, prerender every route to static HTML. No framework
5
+ migration, no runtime SSR.
6
+
7
+ ---
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ # In an existing Vite + React project:
13
+ npm i -D prestruct
14
+ npx prestruct init # never clobbers existing files
15
+ cp ssr.config.example.js ssr.config.js # then edit siteUrl + routes
16
+ npx prestruct build
17
+ ```
18
+
19
+ Build runs the whole pipeline: validate -> `vite build` -> inject meta ->
20
+ prerender routes. Output lands in `dist/` with one `index.html` per route,
21
+ `sitemap.xml`, and `404.html`.
22
+
23
+ **Live demo:** https://prestruct.creadev.org (built with prestruct; source in
24
+ `example/`).
25
+
26
+ Prefer raw npm scripts over the CLI? The `init/` template scaffolds
27
+ `scripts/prerender.js` + `scripts/inject-brand.js` you can wire into
28
+ `package.json` directly -- see [docs/guides/cli.md](./docs/guides/cli.md).
29
+
30
+ ---
31
+
32
+ ## Config
33
+
34
+ ```js
35
+ // ssr.config.js
36
+ export default {
37
+ siteUrl: 'https://yoursite.com',
38
+ siteName: 'Your Site',
39
+ routes: [
40
+ {
41
+ path: '/',
42
+ priority: '1.0',
43
+ meta: {
44
+ title: 'Home | Your Site',
45
+ description: 'Homepage description (150-160 chars).',
46
+ },
47
+ },
48
+ { path: '/about', priority: '0.8', meta: { title: 'About | Your Site' } },
49
+ ],
50
+ }
51
+ ```
52
+
53
+ Full reference (platform, islands, JSON-LD, `fetchRoutes()`, deployId footer,
54
+ `PRESTRUCT_STRICT`): [docs/guides/config.md](./docs/guides/config.md).
55
+
56
+ ---
57
+
58
+ ## CLI
59
+
60
+ ```bash
61
+ npx prestruct init # scaffold template files, never clobber
62
+ npx prestruct build # validate -> vite build -> inject -> prerender
63
+ npx prestruct validate # check ssr.config.js; exit 1 on bad config
64
+ npx prestruct clean # clear the prerender route cache
65
+ ```
66
+
67
+ Incremental builds: `.prestruct/cache/` stores rendered HTML; unchanged routes
68
+ are skipped on rebuild unless the shell changes. Fix a config mistake silently?
69
+ Rebuild with a validation pass first (`npx prestruct validate` prints errors
70
+ line-by-line).
71
+
72
+ ---
73
+
74
+ ## Architecture
75
+
76
+ ```
77
+ ssr.config.js
78
+ |
79
+ v
80
+ prestruct build (or npm run build in the template)
81
+ |
82
+ |- validate structured [prestruct] diagnostics
83
+ |- vite build JS/CSS bundles in dist/assets/
84
+ |- inject-brand meta + OG + JSON-LD into dist/index.html
85
+ `- prerender vite.ssrLoadModule per route -> static HTML
86
+ ```
87
+
88
+ Static HTML is served directly at deploy time (Cloudflare Pages default). No
89
+ runtime SSR. Client-only dynamic content mounts into `<pre-island>` elements
90
+ after hydration.
91
+
92
+ ---
93
+
94
+ ## Repo layout (this monorepos the npm package)
95
+
96
+ | Path | Purpose |
97
+ |------|---------|
98
+ | `engine/` | Single source of truth for `prerender.js`, `inject-brand.js`, `validate-config.js`, islands + usePageMeta. |
99
+ | `bin/cli.mjs` | The `prestruct` executable (init/build/validate/clean). |
100
+ | `init/` | Project template `prestruct init` scaffolds, including `scripts/proxy.js` (VPS) and `proxy.worker.js` (Cloudflare Worker). |
101
+ | `example/` | Working demo site (8 routes), kept byte-identical to engine via `scripts/sync-engine.mjs`. |
102
+ | `docs/` | Full docs site, AGENTS-built with Pagefind. |
103
+ | `tests/` | vitest battery, including the engine sync contract test. |
104
+
105
+ If you touch `engine/`, run `node scripts/sync-engine.mjs` or CI will fail the
106
+ byte-identical check. Details in [AGENTS.md](./AGENTS.md).
107
+
108
+ ---
109
+
110
+ ## Bot proxy (optional, additive)
111
+
112
+ Prerendered HTML covers every route in `ssr.config.js`. Everything else serves
113
+ the SPA shell. The optional render proxy intercepts bots and serves live
114
+ Puppeteer HTML for the rest -- deploy it on a cheap VPS
115
+ ([README-proxy.md](./README-proxy.md), Option A: Node + Puppeteer) or as a
116
+ Cloudflare Worker with Browser Rendering (Option B). Humans always get static
117
+ CF Pages output; the two layers stack, they don't replace each other.
118
+
119
+ ---
120
+
121
+ ## Deploy
122
+
123
+ - Cloudflare Pages: [docs/guides/cloudflare.md](./docs/guides/cloudflare.md)
124
+ (`npm run build`, output `dist/`, Node 18+).
125
+ - Any static host works too: `dist/` is a plain static folder.
126
+
127
+ ---
128
+
129
+ ## Stack
130
+
131
+ - Vite 5+
132
+ - React 18+
133
+ - React Router v6 (v7 works -- engine falls back to the package root when the
134
+ `./server.js` subpath is absent)
135
+ - Cloudflare Pages default; any static host otherwise
136
+ - Node 18+
137
+
138
+ ---
139
+
140
+ ## Publishing state
141
+
142
+ `package.json` is at 1.0.0 but **not tagged and not published**. `publish.yml`
143
+ (OIDC trusted publishing on `v*` tags) is wired and dormant -- it
144
+ fires nothing until the first tag exists, by owner decision. When it fires,
145
+ provenance is automatic and no stored npm token is needed.
146
+
147
+ **Legal:** using prestruct means agreeing to [LICENSE](./LICENSE) and
148
+ [LEGAL.md](./LEGAL.md). Fair-Code: commercial SaaS use is restricted.
149
+
150
+ ---
151
+
152
+ ## Read next
153
+
154
+ - [AGENTS.md](./AGENTS.md) -- engineering history, gotchas, hard rules,
155
+ what's in/out of scope.
156
+ - [labs/prd.md](./labs/prd.md) -- roadmap.
157
+ - [docs/](./docs/index.md) -- full site (SEO, routing, islands, proxy,
158
+ Cloudflare).
159
+
160
+ _No runtime deps added. Config is the API._
package/bin/cli.mjs ADDED
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bin/cli.mjs - prestruct CLI
4
+ * ============================
5
+ * Commands (spec'd in labs/prd.md Phase 2):
6
+ *
7
+ * prestruct init [dir] Scaffold the template into dir (default: cwd).
8
+ * Never clobbers existing files.
9
+ * prestruct build Validate config, run vite build, inject meta,
10
+ * prerender routes -- full pipeline from ssr.config.js.
11
+ * prestruct validate Load + validate ssr.config.js; exit 1 on bad config.
12
+ * prestruct help This text.
13
+ *
14
+ * The CLI runs inside the USER PROJECT: config comes from <workdir>/ssr.config.js,
15
+ * vite/react/react-router-dom resolve from the project's own node_modules
16
+ * (peerDependencies), and build output lands in <workdir>/dist.
17
+ */
18
+
19
+ import fs from 'fs'
20
+ import path from 'path'
21
+ import { fileURLToPath, pathToFileURL } from 'url'
22
+ import { execSync } from 'child_process'
23
+
24
+ process.on('unhandledRejection', (e) => console.error('[prestruct] unhandled rejection:', e && (e.stack || e)))
25
+
26
+ const cliDir = path.dirname(fileURLToPath(import.meta.url))
27
+ const packageRoot = path.resolve(cliDir, '..')
28
+
29
+ const { validateConfig, formatDiagnostics } = await import(
30
+ pathToFileURL(path.join(packageRoot, 'engine/scripts/validate-config.js'))
31
+ )
32
+ const { injectBrand } = await import(
33
+ pathToFileURL(path.join(packageRoot, 'engine/scripts/inject-brand.js'))
34
+ )
35
+ const { prerender, cleanCache, enginePaths } = await import(
36
+ pathToFileURL(path.join(packageRoot, 'engine/scripts/prerender.js'))
37
+ )
38
+
39
+ const HELP = `
40
+ prestruct -- build-time SEO prerender for Vite + React
41
+
42
+ prestruct init [dir] Scaffold the template into dir (default: cwd).
43
+ Existing files are never clobbered.
44
+ prestruct build [--force] [--clean]
45
+ Full pipeline: validate -> vite build -> inject
46
+ meta -> prerender routes. --force rerenders all
47
+ routes; --clean clears the incremental cache first.
48
+ prestruct validate Validate ssr.config.js (exit 1 on bad config).
49
+ prestruct clean Clear the prerender route cache.
50
+ prestruct help This text.
51
+
52
+ Config lives in ssr.config.js -- copy ssr.config.example.js to start.
53
+ `
54
+
55
+ function findWorkdir(args) {
56
+ // Explicit workdir wins; otherwise walk up from cwd until ssr.config.js is found
57
+ const [first] = args
58
+ if (first && !first.startsWith('-')) {
59
+ return path.resolve(first)
60
+ }
61
+ let dir = process.cwd()
62
+ while (dir !== path.dirname(dir)) {
63
+ if (fs.existsSync(path.join(dir, 'ssr.config.js'))) return dir
64
+ dir = path.dirname(dir)
65
+ }
66
+ return process.cwd()
67
+ }
68
+
69
+ async function loadConfig(workdir, { required = true } = {}) {
70
+ const configPath = path.join(workdir, 'ssr.config.js')
71
+ if (!fs.existsSync(configPath)) {
72
+ if (required) {
73
+ console.error(`[prestruct] ssr.config.js not found in ${workdir}`)
74
+ console.error('[prestruct] Copy ssr.config.example.js to ssr.config.js and edit it.')
75
+ }
76
+ return null
77
+ }
78
+ try {
79
+ return (await import(pathToFileURL(configPath))).default
80
+ } catch (err) {
81
+ console.error(`[prestruct] ssr.config.js failed to load: ${err.message}`)
82
+ return null
83
+ }
84
+ }
85
+
86
+ function report(config) {
87
+ // Returns the ok BOOLEAN (not an object) -- callers must not destructure it.
88
+ const { ok, errors } = validateConfig(config)
89
+ if (!ok) formatDiagnostics(errors).forEach(line => console.error(line))
90
+ return ok
91
+ }
92
+
93
+ // -- init ----------------------------------------------------------------------
94
+
95
+ function cmdInit(args) {
96
+ const target = path.resolve(args.find(a => !a.startsWith('-')) || '.')
97
+ const templateRoot = path.join(packageRoot, 'init')
98
+ if (!fs.existsSync(templateRoot)) {
99
+ console.error('[prestruct] template missing from installed package')
100
+ process.exit(1)
101
+ }
102
+
103
+ let created = 0, skipped = 0
104
+ function walk(dir) {
105
+ for (const entry of fs.readdirSync(dir)) {
106
+ if (entry === 'node_modules' || entry === 'dist' || entry === '.prestruct') continue
107
+ // .npmignore belongs to the PACKAGE tarball build (excludes init's local
108
+ // node_modules/dist from npm pack), not to scaffolded apps. Shipping it
109
+ // into a user project would silently prune THEIR files if they ever
110
+ // npm-pack their own package.
111
+ if (entry === '.npmignore') continue
112
+ const src = path.join(dir, entry)
113
+ const dest = path.join(target, path.relative(templateRoot, src))
114
+ const st = fs.statSync(src)
115
+ if (st.isDirectory()) {
116
+ walk(src)
117
+ } else if (fs.existsSync(dest)) {
118
+ skipped++
119
+ console.log(` skip ${path.relative(process.cwd(), dest)} (exists)`)
120
+ } else {
121
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
122
+ fs.copyFileSync(src, dest)
123
+ created++
124
+ console.log(` create ${path.relative(process.cwd(), dest)}`)
125
+ }
126
+ }
127
+ }
128
+ walk(templateRoot)
129
+
130
+ console.log(`\n[prestruct] init done: ${created} created, ${skipped} skipped.`)
131
+ console.log('[prestruct] Next: cp ssr.config.example.js ssr.config.js, edit routes/siteUrl,')
132
+ console.log('[prestruct] add "prestruct build" (or npm run build) as your build step.')
133
+ }
134
+
135
+ // -- build ---------------------------------------------------------------------
136
+
137
+ async function cmdBuild(args) {
138
+ const workdir = findWorkdir(args)
139
+ const config = await loadConfig(workdir)
140
+ if (!config) process.exit(1)
141
+ const configOk = report(config)
142
+ if (!configOk && process.env.PRESTRUCT_STRICT === '1') {
143
+ console.error('[prestruct] PRESTRUCT_STRICT=1 -- failing on config diagnostics')
144
+ process.exit(1)
145
+ }
146
+
147
+ const run = (cmd) => {
148
+ console.log(`[prestruct] $ ${cmd}`)
149
+ execSync(cmd, { cwd: workdir, stdio: 'inherit' })
150
+ }
151
+
152
+ run('npx --no-install vite build')
153
+ injectBrand(config, path.join(workdir, 'dist'))
154
+
155
+ // Forward --force/--clean through to the engine's incremental prerender.
156
+ // Found live by tarball e2e: without this pass-through, engine accepted the
157
+ // force flag but CLI silently dropped it -- "cached / (route)" under --force,
158
+ // which shipped stale static HTML and made users reach for rm -rf.
159
+ const force = args.includes('--force') || args.includes('-f')
160
+ if (args.includes('--clean')) {
161
+ const p = enginePaths(path.resolve(workdir))
162
+ cleanCache({ cacheFile: p.cacheFile })
163
+ console.log('[prestruct] prerender cache cleared (--clean)')
164
+ }
165
+ const result = await prerender(config, { root: workdir, force })
166
+ // Engine prerender() returns { total, succeeded, failed, failures }.
167
+ // Live-fire (2026-10): a route that fails to render still exits 0 -- the
168
+ // broken deploy rides through green. Match the engine's new contract:
169
+ // failures are a failed build.
170
+ if (result && result.failed > 0) {
171
+ console.error(`[prestruct] prerender failed for ${result.failed}/${result.total} route(s) -- failing build`)
172
+ process.exit(1)
173
+ }
174
+ }
175
+
176
+ // -- validate / clean ----------------------------------------------------------
177
+
178
+ async function cmdValidate(args) {
179
+ const workdir = findWorkdir(args)
180
+ const config = await loadConfig(workdir)
181
+ if (!config) process.exit(1)
182
+ const ok = report(config)
183
+ if (ok) console.log(`[prestruct] ssr.config.js is valid (${(config.routes || []).length} routes)`)
184
+ else process.exit(1)
185
+ }
186
+
187
+ function cmdClean(args) {
188
+ const workdir = findWorkdir(args)
189
+ const p = enginePaths(path.resolve(workdir))
190
+ cleanCache({ cacheFile: p.cacheFile })
191
+ }
192
+
193
+ // -- dispatch ------------------------------------------------------------------
194
+
195
+ const [command, ...args] = process.argv.slice(2)
196
+
197
+ try {
198
+ switch (command || 'help') {
199
+ case 'init': cmdInit(args); break
200
+ case 'build': await cmdBuild(args); break
201
+ case 'validate': await cmdValidate(args); break
202
+ case 'clean': cmdClean(args); break
203
+ case 'help': console.log(HELP); break
204
+ default:
205
+ console.error(`[prestruct] unknown command: ${command}`)
206
+ console.log(HELP)
207
+ process.exit(1)
208
+ }
209
+ } catch (err) {
210
+ console.error(`[prestruct] ${err.message}`)
211
+ process.exit(1)
212
+ }