getfilepress 0.1.4 → 0.1.5

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
@@ -73,6 +73,8 @@ pnpm build:www # → sites/getfilepress/build/
73
73
  pnpm ship # build + Wrangler Pages deploy (project: getfilepress)
74
74
  ```
75
75
 
76
+ Two `filepress dev` processes share Vite **5173** unless you pin the port. With [LocalBerth](https://www.npmjs.com/package/localberth) 0.2+, `filepress dev` reads a named lease. Claim once; see [docs/LOCALBERTH.md](docs/LOCALBERTH.md).
77
+
76
78
  ## Site configuration
77
79
 
78
80
  ```ts
@@ -194,7 +196,7 @@ Body content in **Markdown**.
194
196
 
195
197
  Pin the engine in the site `package.json` (`"getfilepress": "^0.1.3"` or `github:Catalyst-Forge-LLC/filepress#v0.1.3`). Do not use `link:` in CI. Set config `url` to the live origin.
196
198
 
197
- Any other static host works the same way: publish `build/` as the web root.
199
+ Any other static host works the same way: publish `build/` as the web root. The build also writes a `_headers` file (HSTS, no framing, no wildcard CORS) for Cloudflare Pages; override it with `static/_headers`.
198
200
 
199
201
  Full notes (including an agent checklist): [`docs/DEPLOY.md`](docs/DEPLOY.md) · product page: [getfilepress.com/deploy](https://getfilepress.com/deploy).
200
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getfilepress",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "FilePress — file-based Markdown blog engine. Link this package from a content-only site (config + posts), then run `filepress build`.",
@@ -36,6 +36,7 @@
36
36
  "scripts/filepress.mjs",
37
37
  "scripts/copy-path-mounts.mjs",
38
38
  "scripts/create-site.mjs",
39
+ "scripts/ensure-lease.mjs",
39
40
  "scripts/postinstall.mjs",
40
41
  "scripts/link-embedded-packages.mjs",
41
42
  "packages/core",
@@ -90,9 +91,9 @@
90
91
  "build:www": "pnpm filepress build --site getfilepress",
91
92
  "ship": "pnpm build:www && wrangler pages deploy sites/getfilepress/build --project-name getfilepress",
92
93
  "deploy:www": "pnpm ship",
93
- "dev": "pnpm filepress dev --site demo",
94
- "dev:demo": "pnpm filepress dev --site demo",
95
- "dev:www": "pnpm filepress dev --site getfilepress",
94
+ "dev": "node scripts/ensure-lease.mjs demo 5179 && pnpm filepress dev --site demo",
95
+ "dev:demo": "node scripts/ensure-lease.mjs demo 5179 && pnpm filepress dev --site demo",
96
+ "dev:www": "node scripts/ensure-lease.mjs getfilepress 5180 && pnpm filepress dev --site getfilepress",
96
97
  "create-site": "node scripts/create-site.mjs",
97
98
  "import": "node scripts/filepress.mjs import",
98
99
  "pack:smoke": "node scripts/pack-smoke.mjs"
@@ -0,0 +1,32 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+
5
+ /** `localberth get <name>` — undefined if the CLI or lease is missing. */
6
+ export function localberthGet(name: string): number | undefined {
7
+ const r = spawnSync('localberth', ['get', name], {
8
+ encoding: 'utf8',
9
+ timeout: 5000,
10
+ windowsHide: true,
11
+ shell: process.platform === 'win32'
12
+ });
13
+ if (r.status !== 0) return undefined;
14
+ const n = Number((r.stdout || '').trim());
15
+ return Number.isInteger(n) && n > 0 && n <= 65535 ? n : undefined;
16
+ }
17
+
18
+ /** FILEPRESS_LEASE, then site package.json name, then the site folder name. */
19
+ export function filepressLeaseName(siteRoot: string): string {
20
+ const fromEnv = process.env.FILEPRESS_LEASE?.trim();
21
+ if (fromEnv) return fromEnv;
22
+ const pkgPath = join(siteRoot, 'package.json');
23
+ if (existsSync(pkgPath)) {
24
+ try {
25
+ const name = JSON.parse(readFileSync(pkgPath, 'utf8')).name;
26
+ if (typeof name === 'string' && name.trim()) return name.trim();
27
+ } catch {
28
+ /* fall through */
29
+ }
30
+ }
31
+ return siteRoot.replace(/[/\\]+$/, '').split(/[/\\]/).pop() || 'filepress';
32
+ }
@@ -10,6 +10,7 @@ import {
10
10
  } from './vite-plugin-critical-theme.ts';
11
11
  import { geniePlugin } from './vite-plugin-genie.ts';
12
12
  import { pathMountsPlugin } from './vite-plugin-path-mounts.ts';
13
+ import { filepressLeaseName, localberthGet } from './localberth-port.ts';
13
14
  import type { PathMount } from '../core/src/lib/paths-shared.ts';
14
15
 
15
16
  const appRoot = dirname(fileURLToPath(import.meta.url));
@@ -84,9 +85,11 @@ async function loadPathMounts(): Promise<PathMount[]> {
84
85
 
85
86
  function resolvePort(): number | undefined {
86
87
  const raw = process.env.FILEPRESS_PORT?.trim();
87
- if (!raw) return undefined;
88
- const n = Number(raw);
89
- return Number.isInteger(n) && n > 0 && n <= 65535 ? n : undefined;
88
+ if (raw) {
89
+ const n = Number(raw);
90
+ return Number.isInteger(n) && n > 0 && n <= 65535 ? n : undefined;
91
+ }
92
+ return localberthGet(filepressLeaseName(siteRoot));
90
93
  }
91
94
 
92
95
  const fixedPort = resolvePort();
@@ -0,0 +1,10 @@
1
+ # Emitted by FilePress. Override by adding static/_headers in the site.
2
+ # Cloudflare Pages and Netlify read this file; it is not served as a page.
3
+
4
+ /*
5
+ Strict-Transport-Security: max-age=31536000
6
+ Content-Security-Policy: frame-ancestors 'none'
7
+ X-Frame-Options: DENY
8
+ X-Content-Type-Options: nosniff
9
+ Referrer-Policy: strict-origin-when-cross-origin
10
+ ! Access-Control-Allow-Origin
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Default Cloudflare Pages / Netlify `_headers` for a FilePress build.
3
+ * A site-owned `static/_headers` wins: adapter-static copies it first, and
4
+ * we refuse to overwrite `build/_headers`.
5
+ */
6
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const templatePath = join(dirname(fileURLToPath(import.meta.url)), 'default-headers.txt');
11
+
12
+ export function defaultSecurityHeaders(): string {
13
+ return readFileSync(templatePath, 'utf8');
14
+ }
15
+
16
+ export function writeBuildHeaders(buildDir: string): 'wrote' | 'kept' {
17
+ const dest = join(buildDir, '_headers');
18
+ if (existsSync(dest)) return 'kept';
19
+ writeFileSync(dest, defaultSecurityHeaders());
20
+ return 'wrote';
21
+ }
@@ -22,6 +22,7 @@ export {
22
22
 
23
23
  export { absoluteUrl, ogImageUrl } from './config';
24
24
  export type { SiteConfig, PathMount } from './config';
25
+ export { defaultSecurityHeaders, writeBuildHeaders } from './headers';
25
26
  export {
26
27
  normalizePathMounts,
27
28
  pathMountReservedSlugs,
@@ -1,21 +1,36 @@
1
1
  /**
2
- * Copy FilePress `paths` mounts into <site>/build after `vite build`.
3
- * Reads `.filepress/path-mounts.json` written when vite.config loads the site config.
2
+ * Post-`vite build` steps for a FilePress site:
3
+ * 1. Write default `build/_headers` unless the site already provided one
4
+ * 2. Copy `paths` mounts (from `.filepress/path-mounts.json`)
4
5
  *
5
6
  * Usage: node scripts/copy-path-mounts.mjs <siteRoot>
6
7
  */
7
- import { cpSync, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
8
- import { join, resolve } from 'node:path';
8
+ import { cpSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
9
+ import { dirname, join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
9
11
 
10
12
  const siteRoot = resolve(process.argv[2] ?? '');
11
13
  const buildDir = join(siteRoot, 'build');
12
14
  const cachePath = join(siteRoot, '.filepress', 'path-mounts.json');
15
+ const headersTemplate = join(
16
+ dirname(fileURLToPath(import.meta.url)),
17
+ '../packages/core/src/lib/default-headers.txt'
18
+ );
13
19
 
14
- if (!existsSync(cachePath)) {
20
+ if (!existsSync(buildDir)) {
21
+ console.warn(`filepress: post-build skipped — build dir missing (${buildDir})`);
15
22
  process.exit(0);
16
23
  }
17
- if (!existsSync(buildDir)) {
18
- console.warn(`filepress: path mounts skipped — build dir missing (${buildDir})`);
24
+
25
+ const headersDest = join(buildDir, '_headers');
26
+ if (existsSync(headersDest)) {
27
+ console.log('filepress: kept site _headers');
28
+ } else if (existsSync(headersTemplate)) {
29
+ writeFileSync(headersDest, readFileSync(headersTemplate, 'utf8'));
30
+ console.log('filepress: wrote default _headers');
31
+ }
32
+
33
+ if (!existsSync(cachePath)) {
19
34
  process.exit(0);
20
35
  }
21
36
 
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Idempotent LocalBerth claim for FilePress (and sibling) dev ports.
4
+ * Usage: node scripts/ensure-lease.mjs <name> <port>
5
+ * Missing CLI → one line, exit 0. Existing lease → keep it (do not rewrite).
6
+ */
7
+ import { spawnSync } from 'node:child_process';
8
+
9
+ const name = process.argv[2];
10
+ const port = process.argv[3];
11
+ if (!name || !port) {
12
+ console.error('usage: node scripts/ensure-lease.mjs <name> <port>');
13
+ process.exit(1);
14
+ }
15
+
16
+ const opt = { encoding: 'utf8', timeout: 8000, windowsHide: true, shell: process.platform === 'win32' };
17
+ const got = spawnSync('localberth', ['get', name], { ...opt, stdio: ['ignore', 'pipe', 'ignore'] });
18
+ if (got.status === 0 && String(got.stdout || '').trim()) {
19
+ process.exit(0);
20
+ }
21
+ const claim = spawnSync('localberth', ['claim', name, '--port', port], { ...opt, stdio: 'inherit' });
22
+ if (claim.error || claim.status !== 0) {
23
+ console.warn(`localberth: skip claim ${name} (install the CLI to pin this site to ${port})`);
24
+ }