chore: import upstream snapshot with attribution
docmd CI verification / verify (push) Failing after 0s
docmd CI verification / verify (push) Failing after 0s
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -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"
|
||||
`;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user