prestruct 1.0.0 → 1.1.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 CHANGED
@@ -99,6 +99,7 @@ after hydration.
99
99
  | `bin/cli.mjs` | The `prestruct` executable (init/build/validate/clean). |
100
100
  | `init/` | Project template `prestruct init` scaffolds, including `scripts/proxy.js` (VPS) and `proxy.worker.js` (Cloudflare Worker). |
101
101
  | `example/` | Working demo site (8 routes), kept byte-identical to engine via `scripts/sync-engine.mjs`. |
102
+ | `packs/` | In-repo crashtest pack (`public/prestruct-proxy` v0.2.0) attacking the bot-proxy pair on both runtimes. |
102
103
  | `docs/` | Full docs site, AGENTS-built with Pagefind. |
103
104
  | `tests/` | vitest battery, including the engine sync contract test. |
104
105
 
@@ -118,31 +119,57 @@ CF Pages output; the two layers stack, they don't replace each other.
118
119
 
119
120
  ---
120
121
 
122
+ ## Crash testing (in-repo pack)
123
+
124
+ `packs/public/prestruct-proxy` (pack v0.2.0) is this repo's own adversarial
125
+ pack for the bot-proxy pair above: four wire-level contracts (method matrix,
126
+ hostile-path fuzz, bot-render 302 fallback, refresh-secret strict parity),
127
+ run through the [crashtest](https://github.com/dhaupin/crashtest) harness
128
+ against both runtimes. Attack route names come from the host `moduleCtx`, so
129
+ the pack runs against any wiring. Latest results live in
130
+ [labs/hardening.md](./labs/hardening.md), the CI-comparable ledger in
131
+ [labs/crashledger.json](./labs/crashledger.json).
132
+
133
+ The pack travels with the checkout, not the npm tarball. Adopting it from
134
+ another repo is one line in that repo's `crashtest.config.js`:
135
+ `packs: ['/abs/path/to/prestruct/packs']` -- contract table and runbook in
136
+ [packs/README.md](./packs/README.md).
137
+
138
+ ---
139
+
121
140
  ## Deploy
122
141
 
123
142
  - Cloudflare Pages: [docs/guides/cloudflare.md](./docs/guides/cloudflare.md)
124
- (`npm run build`, output `dist/`, Node 18+).
143
+ (`npm run build`, output `dist/`, Node 20+).
125
144
  - Any static host works too: `dist/` is a plain static folder.
126
145
 
127
146
  ---
128
147
 
129
148
  ## Stack
130
149
 
131
- - Vite 5+
150
+ - Vite 5+ (examples and the template battery build on Vite 6)
132
151
  - React 18+
133
- - React Router v6 (v7 works -- engine falls back to the package root when the
134
- `./server.js` subpath is absent)
152
+ - React Router v6 and v7 both work: v6 via the `./server.js` subpath, v7 via
153
+ the package-root fallback -- scaffold imports use the namespace+destructure
154
+ form so CJS v7 passes vite SSR (docs and generated HTML render from v7 live)
135
155
  - Cloudflare Pages default; any static host otherwise
136
- - Node 18+
156
+ - Node 20+
137
157
 
138
158
  ---
139
159
 
140
160
  ## Publishing state
141
161
 
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.
162
+ **prestruct is on npm: <https://www.npmjs.com/package/prestruct>** (1.1.0,
163
+ published via `publish.yml`, GitHub Actions OIDC trusted publishing with
164
+ npm provenance -- verify with `npm audit signatures`). Tags drive releases:
165
+ `v*` triggers npm publish plus the GitHub release archive (template zip with
166
+ a self-testing battery). `main` may be ahead of the latest tag; its version
167
+ pins are the next planned release.
168
+
169
+ Local install test from the registry tarball (35 files) passed: `init` never
170
+ clobbers, `build` produces full `dist/` (baked meta, sitemap, 404). The
171
+ release-body template inside `release.yml` was corrected in the v1.0.0
172
+ aftermath to match what the archive actually contains.
146
173
 
147
174
  **Legal:** using prestruct means agreeing to [LICENSE](./LICENSE) and
148
175
  [LEGAL.md](./LEGAL.md). Fair-Code: commercial SaaS use is restricted.
@@ -153,7 +180,7 @@ provenance is automatic and no stored npm token is needed.
153
180
 
154
181
  - [AGENTS.md](./AGENTS.md) -- engineering history, gotchas, hard rules,
155
182
  what's in/out of scope.
156
- - [labs/prd.md](./labs/prd.md) -- roadmap.
183
+ - [labs/prd-prestruct.md](./labs/prd-prestruct.md) -- roadmap.
157
184
  - [docs/](./docs/index.md) -- full site (SEO, routing, islands, proxy,
158
185
  Cloudflare).
159
186
 
package/bin/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * bin/cli.mjs - prestruct CLI
4
4
  * ============================
5
- * Commands (spec'd in labs/prd.md Phase 2):
5
+ * Commands (spec'd in labs/prd-prestruct.md Phase 2):
6
6
  *
7
7
  * prestruct init [dir] Scaffold the template into dir (default: cwd).
8
8
  * Never clobbers existing files.
@@ -272,6 +272,30 @@ export function cleanCache({ cacheFile = enginePaths().cacheFile } = {}) {
272
272
 
273
273
  // -- Sitemap -------------------------------------------------------------------
274
274
 
275
+ //
276
+ // AI-crawler policy preset (aeo option):
277
+ // Search-visibility bots get explicit Allow groups; TRAINING-crawl preference
278
+ // is a separate choice via config.robots.aiBots ('allow' | 'block', default
279
+ // 'block'). Bot names are the token each vendor documents for its own robots
280
+ // product (2026: OpenAI OAI-SearchBot/GPTBot, Anthropic
281
+ // Claude-SearchBot/ClaudeBot, PerplexityBot, Apple Applebot/Applebot-Extended,
282
+ // Common Crawl CCBot, Google-Extended)
283
+ const AEO_SEARCH_BOTS = [
284
+ 'OAI-SearchBot', // OpenAI: surfaces sites in ChatGPT search
285
+ 'PerplexityBot', // Perplexity: search index, not model training
286
+ 'Claude-SearchBot', // Anthropic: Claude's search
287
+ 'Applebot', // Apple: Siri/Spotlight/Safari search
288
+ 'Googlebot', // Google Search incl. AI Overviews eligibility
289
+ 'Bingbot', // Bing / Copilot discovery
290
+ ]
291
+ const AEO_TRAINING_BOTS = [
292
+ 'GPTBot', // OpenAI: model training crawl
293
+ 'ClaudeBot', // Anthropic: model development crawl
294
+ 'Google-Extended', // Gemini/Vertex grounding (product token, no HTTP hits)
295
+ 'Applebot-Extended', // Apple: foundation model training
296
+ 'CCBot', // Common Crawl: open corpus crawlers
297
+ ]
298
+
275
299
  /**
276
300
  * Generate robots.txt when config.robots is opted in; otherwise null so the
277
301
  * author's own public/robots.txt is left untouched. The Sitemap: line points
@@ -290,16 +314,25 @@ export function generateRobots(config) {
290
314
  const cfgBody = typeof cfg === 'string' ? cfg : (cfg.body || '')
291
315
  const cfgSitemap = cfgBool || (cfg && typeof cfg === 'object' ? cfg.sitemap !== false : true)
292
316
  const siteUrl = (config.siteUrl || '').replace(/\/$/, '')
293
-
294
- if (cfgBool) {
295
- const lines = [
296
- 'User-agent: *',
297
- 'Allow: /',
298
- ]
299
- if (cfgSitemap && siteUrl) {
300
- lines.push('', `Sitemap: ${siteUrl}/sitemap.xml`)
317
+ const sitemapLine = cfgSitemap && siteUrl ? ['', `Sitemap: ${siteUrl}/sitemap.xml`] : []
318
+
319
+ // { aeo: true | {} } -> search bots allowed, training crawls blocked by
320
+ // default. { aeo: { aiBots: 'allow' } } flips training to Allow. aeo with
321
+ // an explicit body: the body is appended after the preset groups (letting
322
+ // owners add extra paths) only via body; the preset itself ignores body.
323
+ const aeo = !cfgBool && cfg && cfg.aeo
324
+ const aeoCli = aeo ? (aeo.aiBots === 'allow' ? 'allow' : 'block') : null
325
+
326
+ if (cfgBool || aeo) {
327
+ const lines = []
328
+ if (cfgBool) {
329
+ lines.push('User-agent: *', 'Allow: /')
330
+ } else {
331
+ for (const bot of AEO_SEARCH_BOTS) lines.push(`User-agent: ${bot}`, 'Allow: /', '')
332
+ const verdict = aeoCli === 'allow' ? 'Allow: /' : 'Disallow: /'
333
+ for (const bot of AEO_TRAINING_BOTS) lines.push(`User-agent: ${bot}`, verdict, '')
301
334
  }
302
- return lines.join('\n') + '\n'
335
+ return lines.concat(sitemapLine).join('\n').replace(/\n+$/, '\n')
303
336
  }
304
337
 
305
338
  // Custom body; sitemap pointer appended unless { sitemap: false }
@@ -649,19 +682,21 @@ export async function prerender(cfg = config, { force = false, root } = {}) {
649
682
  console.log('[prerender] ok /robots.txt')
650
683
  }
651
684
 
652
- // Cache stats -- static-sites-safe monitor: a JSON snapshot beside dist/
653
- // (Intentionally not a runtime endpoint; static hosts serve files, not
654
- // processes. Follows the PRD "static-sites-safe option preferred" note.)
685
+ // Cache stats -- static-sites-safe monitor: a JSON snapshot in the
686
+ // project's .prestruct/ dir, NOT dist/. Build telemetry must not become a
687
+ // deployed URL (issue 23): dist/ is the public artifact on every static
688
+ // host, and the old wall-clock generatedAt made two byte-identical
689
+ // builds differ. Same monitor, invisible to deploys, no opt-out needed
690
+ // because there is nothing to opt out of shipping.
655
691
  const stats = {
656
- generatedAt: new Date().toISOString(),
657
692
  routeCount: ROUTES.length,
658
693
  succeeded,
659
694
  cached: Object.keys(cache).length,
660
695
  force,
661
696
  routes: ROUTES.map(r => ({ path: r.path, cached: Boolean(cache[r.path]) })),
662
697
  }
663
- fs.mkdirSync(path.join(p.dist, '.prestruct'), { recursive: true })
664
- fs.writeFileSync(path.join(p.dist, '.prestruct', 'status.json'), JSON.stringify(stats, null, 2), 'utf-8')
698
+ fs.mkdirSync(path.join(p.cacheDir, '..'), { recursive: true })
699
+ fs.writeFileSync(path.join(p.root, '.prestruct', 'status.json'), JSON.stringify(stats, null, 2), 'utf-8')
665
700
 
666
701
  console.log(`[prerender] Done. ${succeeded}/${ROUTES.length} pages rendered.\n`)
667
702
 
@@ -43,7 +43,14 @@
43
43
  */
44
44
 
45
45
  import { useEffect } from 'react'
46
- import { useLocation } from 'react-router-dom'
46
+ import * as rrdNS from 'react-router-dom'
47
+ // Neither plain named imports nor a bare namespace destructure survive
48
+ // both majors: v7 ships CJS (vite SSR rejects named imports; node interop
49
+ // gives a namespace whose only reliable export is `default` -- the exports
50
+ // object), while v6 is ESM with possibly no default. Destructure from
51
+ // whichever carries the exports.
52
+ const rrd = rrdNS.default ?? rrdNS
53
+ const { useLocation } = rrd
47
54
 
48
55
  /** Update the SPA head tags on every route change (see the module head doc). */
49
56
  export default function usePageMeta({ title, description, path, siteUrl }) {
package/init/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.1.0