edgepress 2026.10.14 → 2026.10.15

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
@@ -11,7 +11,7 @@ Document previews use the browser-only `document-viewer` package: PDF, DOC/DOCX,
11
11
  The dependency is pinned to a full commit in the public [document-viewer repository](https://github.com/jsw-teams/document-viewer). Cloud `npm install` needs no sibling checkout, bundled source snapshot or Office conversion software. Publish and validate document-viewer first, then update the dependency URL and lockfile here. Sites can consume the resulting pinned EdgePress commit without copying either implementation.
12
12
 
13
13
  [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/jsw-teams/edgepress)
14
- [![Deploy to EdgeOne Pages](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?repository-url=https%3A%2F%2Fgithub.com%2Fjsw-teams%2Fedgepress&build-command=npm%20run%20build&install-command=npm%20ci&output-directory=dist)
14
+ [![Deploy to EdgeOne Pages](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?repository-url=https%3A%2F%2Fgithub.com%2Fjsw-teams%2Fedgepress&build-command=npm%20run%20build&install-command=npm%20install&output-directory=dist)
15
15
  [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fjsw-teams%2Fedgepress)
16
16
  [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/jsw-teams/edgepress)
17
17
  [![Import into ESA Pages](content/assets/edgepress/brand/deploy-esa.svg)](https://esa.console.aliyun.com/edge/pages/creation)
@@ -34,9 +34,15 @@ The builder automatically explains the latest article edit. Readers see formatte
34
34
 
35
35
  Buttons open repository setup. Cloudflare, Vercel, Netlify and EdgeOne support repository templates; ESA opens its official import screen, where you authorize GitHub and select the repository. Every static platform uses `npm run build` and `dist/`; no database or website Function is required. Write posts in Markdown, compose pages from editable rows and elements, choose a shared theme, and build the site into dist/.
36
36
 
37
+ Real VM recording: the actual CLI creates a post, Geany saves its Markdown, and `npm run deploy:local` starts the preview before navigating from the homepage to the article. Playback is manual.
38
+
39
+ [![Real CLI, editor and browser demonstration](content/assets/images/previews/edgepress-cli-local.1b61974af33b2bb1.webp)](content/assets/images/previews/edgepress-cli-local.065373ef0ebd26fa.mp4)
40
+
41
+ [Play recording](content/assets/images/previews/edgepress-cli-local.065373ef0ebd26fa.mp4) · [GIF](content/assets/images/previews/edgepress-cli-local.9697a9de52bef936.gif)
42
+
37
43
  ## Quick start
38
44
 
39
- The commands below install the published npm release and initialize a new project directory. For the latest GitHub changes, use the deployment buttons or clone this repository, then run `npm install` and `npm run dev` from its root:
45
+ The commands below install the published npm release and initialize a new project directory. For the latest GitHub changes, use the deployment buttons or clone this repository, then run `npm install` and `npm run deploy:local` from its root:
40
46
 
41
47
  mkdir my-edgepress-site
42
48
  cd my-edgepress-site
@@ -45,9 +51,9 @@ The commands below install the published npm release and initialize a new projec
45
51
  npm install
46
52
  npm run dev
47
53
 
48
- Use Node.js 22.12 or newer. Set the real site URL and operator contact in config.yml before publishing. Deploy with edgepress deploy. When developing EdgePress itself, use `npm install`, then `npm link` to expose the local CLI.
54
+ Use Node.js 22.12 or newer. Set the real site URL and operator contact in config.yml before publishing. Deploy with `npm run deploy:cloudflare`, `npm run deploy:vercel`, `npm run deploy:netlify`, `npm run deploy:esa` or `npm run deploy:edgeone`. `npm run deploy:local` starts the loopback preview without uploading a site. When developing EdgePress itself, use `npm install`, then `npm link` to expose the local CLI.
49
55
 
50
- Run `edgepress init` in a new project directory to scaffold a site pinned to the installed EdgePress release. The initializer keeps the existing package name and scripts.
56
+ Run `edgepress init` in a new project directory to scaffold a site pinned to the installed EdgePress release. The initializer keeps the existing package name and adds the site's CLI scripts.
51
57
 
52
58
  ## Maintainer npm and GitHub releases
53
59
 
@@ -289,4 +295,6 @@ Project recordings use manual play/pause and a seekable progress bar, with theme
289
295
 
290
296
  Development files: tests/ contains regression tests and fixtures; tools/assets/ maintains source assets; tools/recordings/ contains desktop capture workflows; tools/verification/ contains browser audits; tools/reports/ holds generated evidence. Run npm test and npm run check after changes.
291
297
 
292
- Dependency maintenance uses compatible release ranges without retained lockfiles. The media and document viewers follow Git semver release ranges; audits resolve fresh dependency snapshots outside the checkout and remove them after use. Dependabot checks daily, including security updates; changed dependencies must pass the full browser suite before release.
298
+ Dependency maintenance uses compatible release ranges without retained lockfiles. The media and document viewers follow Git semver release ranges; audits resolve fresh dependency snapshots outside the checkout and remove them after use. Dependabot checks weekly, including security updates; changed dependencies must pass the full browser suite before release.
299
+
300
+ Weekly automatic dependency maintenance resolves compatible updates, audits and runs this project's regression suite with read-only access, then uses a separate job to commit verified manifest-only repairs. Failed checks or branch protection retain a repair PR; major upgrades are not forced. No lockfiles are retained. The existing package release process remains separate.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "edgepress",
3
3
  "license": "MIT",
4
- "version": "2026.10.14",
4
+ "version": "2026.10.15",
5
5
  "description": "Static websites from Markdown and editable pages, with themes, consent-controlled embeds and readable article updates.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -65,7 +65,8 @@
65
65
  "deploy:netlify": "node src/cli.js deploy netlify",
66
66
  "fonts:sync": "node tools/assets/sync-fonts.mjs",
67
67
  "editor": "node src/cli.js editor",
68
- "audit": "node tools/maintenance/audit-dependencies.mjs low"
68
+ "audit": "node tools/maintenance/audit-dependencies.mjs low",
69
+ "deploy:local": "node src/cli.js server"
69
70
  },
70
71
  "dependencies": {
71
72
  "@jsw-teams/document-viewer": "git+https://github.com/jsw-teams/document-viewer.git#semver:^2026.10.1",
@@ -73,8 +74,8 @@
73
74
  "esbuild": "^0.28.2",
74
75
  "htmlparser2": "^12.0.0",
75
76
  "image-size": "^2.0.4",
76
- "marked": "^18.0.13",
77
- "postcss": "^8.5.15",
77
+ "marked": "^18.1.1",
78
+ "postcss": "^8.5.31",
78
79
  "sanitize-html": "^2.18.1",
79
80
  "yaml": "^2.9.1"
80
81
  },
@@ -85,8 +86,8 @@
85
86
  "undici": "^7.30.1"
86
87
  },
87
88
  "devDependencies": {
88
- "lucide-static": "^1.55.0",
89
- "playwright": "^1.63.0",
89
+ "lucide-static": "^1.61.0",
90
+ "playwright": "^1.64.1",
90
91
  "vscode-json-languageservice": "^6.0.0-next.3",
91
92
  "wrangler": "^4.152.0"
92
93
  }
package/src/cli.js CHANGED
@@ -92,7 +92,8 @@ async function initializeProject() {
92
92
  'deploy:netlify': 'edgepress deploy netlify',
93
93
  'deploy:vercel': 'edgepress deploy vercel',
94
94
  'deploy:edgeone': 'edgepress deploy edgeone',
95
- 'deploy:esa': 'edgepress deploy esa'
95
+ 'deploy:esa': 'edgepress deploy esa',
96
+ 'deploy:local': 'edgepress server'
96
97
  },
97
98
  overrides: {
98
99
  ...(packageData.overrides || {}),
@@ -0,0 +1,32 @@
1
+ // Capture real guest terminal/editor/browser interactions, never host input or drawn terminal frames.
2
+ import assert from 'node:assert/strict';
3
+ import {mkdir,writeFile} from 'node:fs/promises';
4
+ import {resolve} from 'node:path';
5
+ import {chromium} from 'playwright';
6
+ import {workbench} from './demo-workbench.mjs';
7
+ import {recordingContext,record,closeRecording} from './desktop-recording.mjs';
8
+ import {activate,pointAt} from './vm-desktop.mjs';
9
+
10
+ const root=resolve(import.meta.dirname,'../..'),artifact=resolve(root,'tools/.recordings/cli-native');
11
+ const output=resolve(root,'content/assets/images/previews');
12
+ await mkdir(artifact,{recursive:true});await mkdir(output,{recursive:true});
13
+ const browser=await chromium.connectOverCDP('http://127.0.0.1:9222');
14
+ const context=await recordingContext(browser,{locale:'en'},artifact);
15
+ let bench;
16
+ try{
17
+ bench=await workbench({artifact,packageRoot:root,locale:'en',context});
18
+ const page=await context.newPage(),errors=[];page.on('pageerror',error=>errors.push(error.message));
19
+ await record(page,'edgepress-cli-local',async({omitWait})=>{
20
+ await bench.activateTerminal();await bench.newPost();
21
+ await bench.editPost('## A working local site\n\nThis article was created in the real CLI and saved in Geany.\n\nThe local preview runs with **npm run deploy:local**.');
22
+ await bench.activateTerminal();await bench.startServer({omitWait,command:'npm run deploy:local --'});
23
+ await activate('Chromium');await page.goto('https://edgepress-demo.test/');
24
+ const link=page.getByRole('link',{name:bench.title,exact:true}).first();
25
+ await link.waitFor();await pointAt(page,link);await link.click();
26
+ await page.getByRole('heading',{name:'A working local site',exact:true}).waitFor();
27
+ await new Promise(done=>setTimeout(done,2400));
28
+ },{foreground:()=>bench.activateTerminal()});
29
+ assert.deepEqual(errors,[]);
30
+ const entries=await closeRecording(context,{artifact,output,source:'tools/recordings/capture-cli-demo.mjs'});
31
+ await writeFile(resolve(artifact,'result.json'),JSON.stringify(entries,null,2)+'\n');
32
+ }finally{await bench?.dispose();await browser.close();}
@@ -2,7 +2,7 @@
2
2
  // Geany edits the actual generated file. Nothing is installed into production.
3
3
  import assert from 'node:assert/strict';
4
4
  import {readFile,writeFile,mkdir,rm,cp} from 'node:fs/promises';
5
- import {resolve,sep,extname} from 'node:path';
5
+ import {resolve,sep} from 'node:path';
6
6
  import {spawn} from 'node:child_process';
7
7
  import {pathToFileURL} from 'node:url';
8
8
  import {guest,guestPath,typeCommand,editFile,openApp,activate} from './vm-desktop.mjs';
@@ -23,7 +23,7 @@ export async function workbench({artifact,packageRoot,photoPath,locale,context,e
23
23
  await rm(resolve(root,'content/pages'),{recursive:true,force:true});await mkdir(resolve(root,'content/pages/home'),{recursive:true});
24
24
  await writeFile(resolve(root,'content/pages/home',locale.toLowerCase()+'.json'),JSON.stringify({title:settings.site.title,lang:locale,slug:'home',homepage:true,blocks:[{columns:1,cells:[[{type:'hero',eyebrow:chinese?'生活里的小事':'Notes from everyday life',title:settings.site.title,text:settings.site.description}]]},{columns:1,cells:[[{type:'post-list',title:chinese?'最近的文章':'Recent stories',category:'uncategorized',count:3}]]}]},null,2)+'\n');
25
25
  if(embedding){settings.plugins.consent.services=[{id:'ishare',provider:'oembed',backendUrl:'https://ishare.js.gripe',enabled:true,name:chinese?'爱分享':'ishare',purpose:chinese?'在你同意后显示作者分享的图片与视频。':'Show the author’s shared photos and videos after you agree.',dataCategories:chinese?'请求的媒体和 IP 地址。':'Requested media and IP address.',recipient:'ishare',retention:chinese?'直到发布者删除。':'Until the publisher deletes it.',privacyUrl:'https://ishare.js.gripe/privacy/'}];await writeFile(resolve(root,'config.yml'),stringify(settings));}
26
- await mkdir(resolve(root,'content/assets/photos'),{recursive:true});await cp(photoPath,resolve(root,'content/assets/photos/seaside.jpg'));
26
+ if(photoPath){await mkdir(resolve(root,'content/assets/photos'),{recursive:true});await cp(photoPath,resolve(root,'content/assets/photos/seaside.jpg'));}
27
27
  let file=null,port;
28
28
  const terminalTitle='EdgePress '+locale;
29
29
  const {loadConfig}=await import(pathToFileURL(resolve(upstream,'src/config.js')));const {readDocuments}=await import(pathToFileURL(resolve(upstream,'src/content.js')));
@@ -37,9 +37,9 @@ export async function workbench({artifact,packageRoot,photoPath,locale,context,e
37
37
  async function createPost(){await run(['new','post',title]);const article=await findPost();assert.ok(article);return file;}
38
38
  async function newPost(){await typeCommand(root,terminalTitle,"edgepress new post '"+title+"'");for(let attempt=0;attempt<30;attempt++){const article=await findPost();if(article)return;await new Promise(done=>setTimeout(done,300));}throw Error('CLI did not create the post');}
39
39
  async function editPost(body){assert.ok(file);const front=(await readFile(file,'utf8')).split('---')[1];await editFile(root,file,'---'+front+'---\n\n'+body+'\n');}
40
- async function startServer({omitWait=task=>task()}={}){
40
+ async function startServer({omitWait=task=>task(),command='edgepress server'}={}){
41
41
  const {createServer}=await import('node:net');const socket=createServer();await new Promise(done=>socket.listen(0,'127.0.0.1',done));port=socket.address().port;await new Promise(done=>socket.close(done));
42
- await typeCommand(root,terminalTitle,'edgepress server --port '+port);
42
+ await typeCommand(root,terminalTitle,command+' --port '+port);
43
43
  await new Promise(done=>setTimeout(done,1600));
44
44
  await omitWait(async()=>{for(let attempt=0;attempt<180;attempt++){try{const response=await fetch('http://127.0.0.1:'+port+'/',{signal:AbortSignal.timeout(1200)});if(response.ok)return;}catch{}await new Promise(done=>setTimeout(done,500));}throw Error('Guest preview did not become ready');});
45
45
  }
@@ -57,7 +57,7 @@ export async function record(page, name, story, {foreground} = {}) {
57
57
  }
58
58
 
59
59
  // Export the VM desktop master after the browser fixture has been disposed.
60
- export async function closeRecording(context, {artifact, output}) {
60
+ export async function closeRecording(context, {artifact, output, source='tools/recordings/desktop-recording.mjs', kind}) {
61
61
  await context.close();
62
62
  const session = sessions.get(context);
63
63
  const entries = [];
@@ -81,7 +81,7 @@ export async function closeRecording(context, {artifact, output}) {
81
81
  const poster = name + '.' + hash(await readFile(posterPath)) + '.webp';
82
82
  for (const [source, target] of [[gif, file], [movie, movieFile], [posterPath, poster]]) await cp(source, resolve(output, target));
83
83
  const entry = {name, file, poster, movie:movieFile, movieBytes:movieBytes.length, duration, bytes:bytes.length,
84
- date:new Date().toISOString().slice(0, 10), kind:'FFmpeg recording of an isolated WSL2 desktop; '+(name.startsWith('edgepress-')||name.startsWith('ishare-sharing-')?'real terminal, editor and Chromium; preview startup wait shortened':'actual Chromium interface')+'; isolated service fixtures', source:'tools/recordings/capture-project-demos.mjs'};
84
+ date:new Date().toISOString().slice(0, 10), kind:kind||'FFmpeg recording of an isolated WSL2 desktop; '+(name.startsWith('edgepress-')||name.startsWith('ishare-sharing-')?'real terminal, editor and Chromium; preview startup wait shortened':'actual Chromium interface')+'; isolated service fixtures', source};
85
85
  entries.push(entry);
86
86
  const manifest = resolve(artifact, 'manifest.json'), previous = await readFile(manifest, 'utf8').then(JSON.parse).catch(() => []);
87
87
  await writeFile(manifest, JSON.stringify([...previous.filter(item => item.name !== name), entry], null, 2) + '\n');