chore: import upstream snapshot with attribution
docmd CI verification / verify (push) Failing after 0s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:55 +08:00
commit 6db8fca185
437 changed files with 68762 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
/**
* --------------------------------------------------------------------
* docmd : the zero-config documentation engine.
*
* @package @docmd/deployer
* @website https://docmd.io
* @repository https://github.com/docmd-io/docmd
* @license MIT
* @copyright Copyright (c) 2025-present docmd.io
*
* [docmd-source] - Please do not remove this header.
* --------------------------------------------------------------------
*/
/** Normalised project context shared across all deployment providers. */
export interface DeployContext {
title: string;
outDir: string;
siteUrl: string | null;
hostname: string;
isSpa: boolean;
configFile: string | null;
version: string;
}
+99
View File
@@ -0,0 +1,99 @@
/**
* --------------------------------------------------------------------
* docmd : the zero-config documentation engine.
*
* @package @docmd/deployer
* @website https://docmd.io
* @repository https://github.com/docmd-io/docmd
* @license MIT
* @copyright Copyright (c) 2025-present docmd.io
*
* [docmd-source] - Please do not remove this header.
* --------------------------------------------------------------------
*/
import fs from 'node:fs/promises';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { TUI } from '@docmd/api';
import type { DeployContext } from './context.js';
import { generateDocker } from './providers/docker.js';
import { generateNginx } from './providers/nginx.js';
import { generateCaddy } from './providers/caddy.js';
import { generateGithubPages } from './providers/github-pages.js';
import { generateVercel } from './providers/vercel.js';
import { generateNetlify } from './providers/netlify.js';
const pkgUrl = new URL('../package.json', import.meta.url);
const { version } = JSON.parse(readFileSync(pkgUrl, 'utf-8'));
export interface DeployOpts {
docker?: boolean;
nginx?: boolean;
caddy?: boolean;
githubPages?: boolean;
vercel?: boolean;
netlify?: boolean;
force?: boolean;
}
async function write(filePath: string, content: string, label: string, force?: boolean) {
// N-2: when --force is not set, do not overwrite an existing file.
// The deploy command was silently clobbering existing Docker / nginx /
// vercel / etc. configs on every run. The fix: skip with a clear
// TUI line and the existing content is preserved. Pass --force to
// overwrite. This makes the existing `--force` flag meaningful.
if (await fileExists(filePath) && !force) {
TUI.step(`${label} (already exists, skipped — use --force to overwrite)`, 'SKIP', TUI.yellow);
return;
}
await fs.writeFile(filePath, content, 'utf8');
TUI.step(label, 'DONE');
}
async function fileExists(filePath: string): Promise<boolean> {
try { await fs.access(filePath); return true; } catch { return false; }
}
export async function generateDeployConfigs(ctx: DeployContext, opts: DeployOpts): Promise<void> {
const cwd = process.cwd();
const force = opts.force === true;
TUI.section('Deployment Context');
TUI.item('Project', ctx.title);
TUI.item('Output', `${ctx.outDir}/`);
if (ctx.hostname) TUI.item('Host', ctx.hostname);
TUI.footer();
if (opts.docker) {
const hasNginxConf = opts.nginx || await fileExists(resolve(cwd, 'nginx.conf'));
const { dockerfile, dockerignore } = await generateDocker(ctx);
const df = hasNginxConf
? dockerfile.replace('FROM nginx:alpine', 'FROM nginx:alpine\nCOPY nginx.conf /etc/nginx/conf.d/default.conf')
: dockerfile;
await write(resolve(cwd, 'Dockerfile'), df, 'Dockerfile', force);
await write(resolve(cwd, '.dockerignore'), dockerignore, '.dockerignore', force);
}
if (opts.nginx) {
await write(resolve(cwd, 'nginx.conf'), generateNginx(ctx), 'nginx.conf', force);
}
if (opts.caddy) {
await write(resolve(cwd, 'Caddyfile'), generateCaddy(ctx), 'Caddyfile', force);
}
if (opts.githubPages) {
await fs.mkdir(resolve(cwd, '.github', 'workflows'), { recursive: true });
await write(resolve(cwd, '.github', 'workflows', 'deploy.yml'), generateGithubPages(ctx), '.github/workflows/deploy.yml', force);
}
if (opts.vercel) {
await write(resolve(cwd, 'vercel.json'), generateVercel(ctx), 'vercel.json', force);
}
if (opts.netlify) {
await write(resolve(cwd, 'netlify.toml'), generateNetlify(ctx), 'netlify.toml', force);
}
}
+64
View File
@@ -0,0 +1,64 @@
/**
* --------------------------------------------------------------------
* docmd : the zero-config documentation engine.
*
* @package @docmd/deployer
* @website https://docmd.io
* @repository https://github.com/docmd-io/docmd
* @license MIT
* @copyright Copyright (c) 2025-present docmd.io
*
* [docmd-source] - Please do not remove this header.
* --------------------------------------------------------------------
*/
import type { DeployContext } from '../context.js';
export function generateCaddy(ctx: DeployContext): string {
const caddyAddress = ctx.hostname || ':80';
const spaCaddyLine = ctx.isSpa
? ' try_files {path} {path}/ /index.html'
: ' try_files {path} {path}/';
return `# ---------------------------------------------------
# Caddyfile generated by docmd v${ctx.version}
# Project: ${ctx.title}
# https://docmd.io/deployment/caddy
# ---------------------------------------------------
${caddyAddress} {
root * ./${ctx.outDir}
file_server
# SPA Routing Fallback
${spaCaddyLine}
# Security headers
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
# HSTS (1 year). Only enable after verifying HTTPS works end-to-end.
# Strict-Transport-Security "max-age=31536000; includeSubDomains"
# CSP — adjust if you embed third-party widgets or analytics.
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'"
-Server
}
# Custom 404
handle_errors {
rewrite * /404.html
file_server
}
# Cache Static Assets (6 months)
@static {
file
path *.ico *.css *.js *.gif *.jpg *.jpeg *.png *.webp *.avif *.svg *.woff *.woff2 *.eot *.ttf *.otf
}
header @static Cache-Control "public, max-age=15552000, immutable"
}
`;
}
+64
View File
@@ -0,0 +1,64 @@
/**
* --------------------------------------------------------------------
* docmd : the zero-config documentation engine.
*
* @package @docmd/deployer
* @website https://docmd.io
* @repository https://github.com/docmd-io/docmd
* @license MIT
* @copyright Copyright (c) 2025-present docmd.io
*
* [docmd-source] - Please do not remove this header.
* --------------------------------------------------------------------
*/
import type { DeployContext } from '../context.js';
export async function generateDocker(ctx: DeployContext): Promise<{ dockerfile: string; dockerignore: string }> {
const buildCmd = ctx.configFile ? `docmd build --config ${ctx.configFile}` : 'docmd build';
const dockerfile = `# ---------------------------------------------------
# Dockerfile generated by docmd v${ctx.version}
# Project: ${ctx.title}
# https://docmd.io/deployment/docker
# ---------------------------------------------------
# Stage 1: Build the docmd site
FROM node:22-alpine AS builder
WORKDIR /app
# Copy dependency manifests first for optimal layer caching
COPY package*.json ./
RUN if [ -f package.json ]; then npm install --ignore-scripts; fi
# Copy the rest of the project
COPY . .
# Install the exact docmd core version used to configure this deployment
RUN npm install -g @docmd/core@${ctx.version}
# Build the site
RUN ${buildCmd}
# Stage 2: Serve with nginx
FROM nginx:alpine
COPY --from=builder /app/${ctx.outDir} /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
`;
const dockerignore = `node_modules
${ctx.outDir}
dist
.git
.gitignore
.env
.env.*
*.md
!docs/**/*.md
.DS_Store
Thumbs.db
`;
return { dockerfile, dockerignore };
}
@@ -0,0 +1,80 @@
/**
* --------------------------------------------------------------------
* docmd : the zero-config documentation engine.
*
* @package @docmd/deployer
* @website https://docmd.io
* @repository https://github.com/docmd-io/docmd
* @license MIT
* @copyright Copyright (c) 2025-present docmd.io
*
* [docmd-source] - Please do not remove this header.
* --------------------------------------------------------------------
*/
import type { DeployContext } from '../context.js';
export function generateGithubPages(ctx: DeployContext): string {
const buildCmd = ctx.configFile ? `docmd build --config ${ctx.configFile}` : 'docmd build';
return `# ---------------------------------------------------
# GitHub Pages Deployment Workflow
# Generated by docmd v${ctx.version}
# Project: ${ctx.title}
# https://docmd.io/deployment/github-pages
# ---------------------------------------------------
name: Deploy to GitHub Pages
on:
push:
branches:
- main
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm install
- name: Install docmd
run: npm install -g @docmd/core@${ctx.version}
- name: Build site
run: ${buildCmd}
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./${ctx.outDir}
deploy:
environment:
name: github-pages
url: \${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
`;
}
@@ -0,0 +1,64 @@
/**
* --------------------------------------------------------------------
* docmd : the zero-config documentation engine.
*
* @package @docmd/deployer
* @website https://docmd.io
* @repository https://github.com/docmd-io/docmd
* @license MIT
* @copyright Copyright (c) 2025-present docmd.io
*
* [docmd-source] - Please do not remove this header.
* --------------------------------------------------------------------
*/
import type { DeployContext } from '../context.js';
export function generateNetlify(ctx: DeployContext): string {
// T-Z9: do NOT emit a `[[redirects]] from = "/*" status = 200` block.
// docmd generates real HTML files for every route (SPA mode is a thin
// client-side layer on top of pre-rendered HTML, not a single-page
// app served from /). A catch-all to /index.html with status 200
// makes missing routes return 200 with the home page content — a
// soft-404 that hides errors, hurts SEO, and creates a URL
// enumeration surface. Without the redirect, Netlify serves the
// generated file if it exists, or the bundled 404.html (with 404
// status) if not.
return `# ---------------------------------------------------
# netlify.toml generated by docmd v${ctx.version}
# Project: ${ctx.title}
# https://docmd.io/deployment/netlify
# ---------------------------------------------------
[build]
command = "npm install -g @docmd/core@${ctx.version} && docmd build"
publish = "${ctx.outDir}"
[build.environment]
NODE_VERSION = "20"
# Security headers applied to every response.
# HSTS is commented out — enable it after verifying HTTPS works end-to-end.
# CSP: adjust default-src if you embed third-party widgets or analytics.
[[headers]]
for = "/*"
[headers.values]
X-Content-Type-Options = "nosniff"
X-Frame-Options = "SAMEORIGIN"
X-XSS-Protection = "1; mode=block"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "camera=(), microphone=(), geolocation=()"
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'"
# Strict-Transport-Security = "max-age=31536000; includeSubDomains"
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=15552000, immutable"
[[headers]]
for = "/*.html"
[headers.values]
Cache-Control = "public, max-age=0, must-revalidate"
`;
}
+81
View File
@@ -0,0 +1,81 @@
/**
* --------------------------------------------------------------------
* docmd : the zero-config documentation engine.
*
* @package @docmd/deployer
* @website https://docmd.io
* @repository https://github.com/docmd-io/docmd
* @license MIT
* @copyright Copyright (c) 2025-present docmd.io
*
* [docmd-source] - Please do not remove this header.
* --------------------------------------------------------------------
*/
import type { DeployContext } from '../context.js';
export function generateNginx(ctx: DeployContext): string {
const serverName = ctx.hostname || 'localhost';
const spaBlock = ctx.isSpa ? `
# SPA Routing Fallback
location / {
try_files $uri $uri/ /index.html;
}
` : `
location / {
try_files $uri $uri/ =404;
}
`;
return `# ---------------------------------------------------
# nginx.conf generated by docmd v${ctx.version}
# Project: ${ctx.title}
# https://docmd.io/deployment/nginx
# ---------------------------------------------------
server {
listen 80;
server_name ${serverName};
root /usr/share/nginx/html;
index index.html;
# Security headers
server_tokens off;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# HSTS (1 year). Only enable after verifying HTTPS works end-to-end.
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# CSP — adjust 'default-src' if you embed third-party widgets or analytics.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'" always;
# GZIP Compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_min_length 256;
gzip_types
text/plain
text/css
application/json
application/javascript
text/xml
application/xml
application/xml+rss
text/javascript
image/svg+xml;
${spaBlock}
# Custom 404
error_page 404 /404.html;
# Cache Control for Static Assets
location ~* \\.(?:ico|css|js|gif|jpe?g|png|webp|avif|woff2?|eot|ttf|otf|svg)$ {
expires 6M;
access_log off;
add_header Cache-Control "public, immutable";
}
}
`;
}
+60
View File
@@ -0,0 +1,60 @@
/**
* --------------------------------------------------------------------
* docmd : the zero-config documentation engine.
*
* @package @docmd/deployer
* @website https://docmd.io
* @repository https://github.com/docmd-io/docmd
* @license MIT
* @copyright Copyright (c) 2025-present docmd.io
*
* [docmd-source] - Please do not remove this header.
* --------------------------------------------------------------------
*/
import type { DeployContext } from '../context.js';
export function generateVercel(ctx: DeployContext): string {
const routes = ctx.isSpa
? ` "routes": [
{ "handle": "filesystem" },
{ "src": "/(.*)", "dest": "/index.html" }
]`
: ` "cleanUrls": true,
"trailingSlash": false`;
return `{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "docmd build",
"outputDirectory": "${ctx.outDir}",
"installCommand": "npm install && npm install -g @docmd/core@${ctx.version}",
"framework": null,
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
{ "key": "X-XSS-Protection", "value": "1; mode=block" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
{ "key": "Content-Security-Policy", "value": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'" }
]
},
{
"source": "/assets/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=15552000, immutable" }
]
},
{
"source": "/(.*).html",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }
]
}
],
${routes}
}
`;
}