@askrjs/cli 0.0.2 → 0.0.4
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 +46 -7
- package/dist/add.d.ts +16 -0
- package/dist/add.js +558 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +43 -16
- package/dist/create.d.ts +5 -0
- package/dist/create.js +104 -26
- package/dist/directory-swap-DWoHtx7C.js +35 -0
- package/dist/discovery-Difb7Y4G.js +323 -0
- package/dist/generate.d.ts +5 -0
- package/dist/generate.js +461 -0
- package/dist/is-direct-execution-Cdlr-ZUl.js +0 -2
- package/dist/openapi.d.ts +25 -0
- package/dist/openapi.js +49 -16
- package/dist/planner-VAj7qlxr.js +285 -0
- package/dist/range-YUs9eimn.js +125 -0
- package/dist/registry-CyrRHRG5.js +209 -0
- package/dist/skills/askr-dashboard-charts/SKILL.md +61 -44
- package/dist/skills/askr-dashboard-charts/agents/openai.yaml +2 -2
- package/dist/skills/askr-ssr-ssg/SKILL.md +3 -1
- package/dist/{skills-Bh0mEhIx.js → skills-B7CbWur9.js} +89 -26
- package/dist/skills.d.ts +18 -0
- package/dist/skills.js +1 -1
- package/dist/specification-DXnDOC-0.js +49 -0
- package/dist/ssg-config.d.ts +41 -0
- package/dist/ssg-config.js +1 -0
- package/dist/ssg.d.ts +61 -0
- package/dist/ssg.js +360 -24
- package/dist/templates/full-stack/AGENTS.md +0 -1
- package/dist/templates/full-stack/README.md +0 -1
- package/dist/templates/full-stack/index.html +0 -1
- package/dist/templates/full-stack/package.json +5 -3
- package/dist/templates/full-stack/server.ts +5 -9
- package/dist/templates/full-stack/src/action-authorizations.ts +2 -2
- package/dist/templates/full-stack/src/actions/create-message.ts +5 -5
- package/dist/templates/full-stack/src/i18n.ts +4 -4
- package/dist/templates/full-stack/src/main.tsx +4 -5
- package/dist/templates/full-stack/src/pages/home.tsx +5 -5
- package/dist/templates/full-stack/src/pages/layout.tsx +15 -5
- package/dist/templates/full-stack/src/pages/not-found.tsx +5 -2
- package/dist/templates/full-stack/src/routes.tsx +11 -12
- package/dist/templates/full-stack/src/schemas.ts +1 -2
- package/dist/templates/full-stack/src/server/action-registry.ts +5 -10
- package/dist/templates/full-stack/src/server/actions/create-message.ts +4 -4
- package/dist/templates/full-stack/src/server/app.ts +67 -57
- package/dist/templates/full-stack/src/server/dependencies.ts +10 -10
- package/dist/templates/full-stack/src/server/entry-server.ts +3 -3
- package/dist/templates/full-stack/src/telemetry.ts +1 -2
- package/dist/templates/full-stack/tests/actions/create-message.test.ts +7 -8
- package/dist/templates/full-stack/tsconfig.json +1 -2
- package/dist/templates/full-stack/vite.config.ts +5 -6
- package/dist/templates/spa/AGENTS.md +1 -1
- package/dist/templates/spa/package.json +3 -4
- package/dist/templates/spa/src/adapters/operations-client.ts +7 -2
- package/dist/templates/spa/src/pages/app/_routes.tsx +2 -0
- package/dist/templates/spa/src/pages/app/admin-home.tsx +21 -3
- package/dist/templates/spa/src/pages/public/_routes.tsx +2 -0
- package/dist/templates/spa/src/styles.css +1 -1
- package/dist/templates/spa/tsconfig.json +2 -1
- package/dist/templates/spa/vitest.config.ts +0 -4
- package/dist/templates/ssg/README.md +1 -1
- package/dist/templates/ssg/package.json +1 -1
- package/dist/templates/ssg/ssg.config.ts +7 -0
- package/dist/templates/ssg/tests/ssg-config.test.ts +5 -3
- package/dist/templates/ssg/tsconfig.json +2 -1
- package/dist/templates/ssg/vitest.config.ts +0 -4
- package/dist/templates/ssr/package.json +2 -2
- package/dist/templates/ssr/server.ts +4 -10
- package/dist/templates/ssr/src/entry-server.tsx +6 -4
- package/dist/templates/startkit/package.json +2 -2
- package/dist/templates/startkit/src/lib/mock-data.ts +17 -39
- package/dist/templates/startkit/tsconfig.json +2 -1
- package/dist/update.d.ts +41 -0
- package/dist/update.js +282 -2
- package/dist/writer-D8qe_7ud.js +226 -0
- package/package.json +23 -22
- package/dist/add-DqhqRzBQ.js +0 -555
- package/dist/add-DqhqRzBQ.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/create.js.map +0 -1
- package/dist/is-direct-execution-Cdlr-ZUl.js.map +0 -1
- package/dist/openapi.js.map +0 -1
- package/dist/skills-Bh0mEhIx.js.map +0 -1
- package/dist/ssg.js.map +0 -1
- package/dist/templates/full-stack/src/vite-server.d.ts +0 -6
- package/dist/templates/spa/src/styles.d.ts +0 -1
- package/dist/templates/ssg/src/vite-env.d.ts +0 -5
- package/dist/templates/ssr/src/vite-server.d.ts +0 -7
- package/dist/templates/startkit/src/vite-env.d.ts +0 -5
- package/dist/update-DyW1na5V.js +0 -1140
- package/dist/update-DyW1na5V.js.map +0 -1
- package/skills/askr-accessibility/SKILL.md +0 -68
- package/skills/askr-accessibility/agents/openai.yaml +0 -4
- package/skills/askr-agent-execution/SKILL.md +0 -85
- package/skills/askr-agent-execution/agents/openai.yaml +0 -4
- package/skills/askr-agent-workflows/SKILL.md +0 -57
- package/skills/askr-agent-workflows/agents/openai.yaml +0 -4
- package/skills/askr-api-integration/SKILL.md +0 -79
- package/skills/askr-api-integration/agents/openai.yaml +0 -4
- package/skills/askr-app-builder/SKILL.md +0 -99
- package/skills/askr-app-builder/agents/openai.yaml +0 -4
- package/skills/askr-auth-access/SKILL.md +0 -74
- package/skills/askr-auth-access/agents/openai.yaml +0 -4
- package/skills/askr-cli-vite/SKILL.md +0 -76
- package/skills/askr-cli-vite/agents/openai.yaml +0 -4
- package/skills/askr-dashboard-charts/SKILL.md +0 -82
- package/skills/askr-dashboard-charts/agents/openai.yaml +0 -4
- package/skills/askr-design-system/SKILL.md +0 -86
- package/skills/askr-design-system/agents/openai.yaml +0 -4
- package/skills/askr-env-config/SKILL.md +0 -74
- package/skills/askr-env-config/agents/openai.yaml +0 -4
- package/skills/askr-error-loading-empty/SKILL.md +0 -89
- package/skills/askr-error-loading-empty/agents/openai.yaml +0 -4
- package/skills/askr-file-upload-artifacts/SKILL.md +0 -77
- package/skills/askr-file-upload-artifacts/agents/openai.yaml +0 -4
- package/skills/askr-forms-tables-crud/SKILL.md +0 -98
- package/skills/askr-forms-tables-crud/agents/openai.yaml +0 -4
- package/skills/askr-mental-model/SKILL.md +0 -109
- package/skills/askr-mental-model/agents/openai.yaml +0 -4
- package/skills/askr-migration-react/SKILL.md +0 -81
- package/skills/askr-migration-react/agents/openai.yaml +0 -4
- package/skills/askr-observability-debugging/SKILL.md +0 -83
- package/skills/askr-observability-debugging/agents/openai.yaml +0 -4
- package/skills/askr-project-structure/SKILL.md +0 -79
- package/skills/askr-project-structure/agents/openai.yaml +0 -4
- package/skills/askr-query-mutation/SKILL.md +0 -115
- package/skills/askr-query-mutation/agents/openai.yaml +0 -4
- package/skills/askr-realtime-streaming/SKILL.md +0 -76
- package/skills/askr-realtime-streaming/agents/openai.yaml +0 -4
- package/skills/askr-resources-data/SKILL.md +0 -100
- package/skills/askr-resources-data/agents/openai.yaml +0 -4
- package/skills/askr-routing-layouts/SKILL.md +0 -107
- package/skills/askr-routing-layouts/agents/openai.yaml +0 -4
- package/skills/askr-runtime-reactivity/SKILL.md +0 -92
- package/skills/askr-runtime-reactivity/agents/openai.yaml +0 -4
- package/skills/askr-ssr-ssg/SKILL.md +0 -76
- package/skills/askr-ssr-ssg/agents/openai.yaml +0 -4
- package/skills/askr-testing-determinism/SKILL.md +0 -75
- package/skills/askr-testing-determinism/agents/openai.yaml +0 -4
- package/skills/askr-theming/SKILL.md +0 -86
- package/skills/askr-theming/agents/openai.yaml +0 -4
- package/skills/askr-ui-composition/SKILL.md +0 -92
- package/skills/askr-ui-composition/agents/openai.yaml +0 -4
- package/templates/full-stack/AGENTS.md +0 -12
- package/templates/full-stack/README.md +0 -18
- package/templates/full-stack/gitignore.template +0 -4
- package/templates/full-stack/index.html +0 -14
- package/templates/full-stack/package.json +0 -37
- package/templates/full-stack/server.ts +0 -13
- package/templates/full-stack/src/action-authorizations.ts +0 -10
- package/templates/full-stack/src/actions/create-message.ts +0 -10
- package/templates/full-stack/src/i18n.ts +0 -8
- package/templates/full-stack/src/main.tsx +0 -12
- package/templates/full-stack/src/pages/home.tsx +0 -25
- package/templates/full-stack/src/pages/layout.tsx +0 -33
- package/templates/full-stack/src/pages/not-found.tsx +0 -4
- package/templates/full-stack/src/routes.tsx +0 -20
- package/templates/full-stack/src/schemas.ts +0 -11
- package/templates/full-stack/src/server/action-registry.ts +0 -12
- package/templates/full-stack/src/server/actions/create-message.ts +0 -14
- package/templates/full-stack/src/server/app.ts +0 -66
- package/templates/full-stack/src/server/dependencies.ts +0 -55
- package/templates/full-stack/src/server/entry-server.ts +0 -8
- package/templates/full-stack/src/telemetry.ts +0 -4
- package/templates/full-stack/src/vite-server.d.ts +0 -6
- package/templates/full-stack/tests/actions/create-message.test.ts +0 -13
- package/templates/full-stack/tsconfig.json +0 -16
- package/templates/full-stack/vite.config.ts +0 -11
- package/templates/spa/AGENTS.md +0 -60
- package/templates/spa/README.md +0 -96
- package/templates/spa/gitignore.template +0 -16
- package/templates/spa/index.html +0 -13
- package/templates/spa/package.json +0 -35
- package/templates/spa/src/adapters/operations-client.ts +0 -90
- package/templates/spa/src/components/shared/metric-card.tsx +0 -27
- package/templates/spa/src/components/shared/status-badge.tsx +0 -24
- package/templates/spa/src/features/operations/operations.query.ts +0 -9
- package/templates/spa/src/main.tsx +0 -10
- package/templates/spa/src/pages/_layout.tsx +0 -9
- package/templates/spa/src/pages/_routes.tsx +0 -27
- package/templates/spa/src/pages/app/_layout.tsx +0 -94
- package/templates/spa/src/pages/app/_routes.tsx +0 -10
- package/templates/spa/src/pages/app/admin-home.tsx +0 -152
- package/templates/spa/src/pages/app/agent-runs.tsx +0 -93
- package/templates/spa/src/pages/app/settings.tsx +0 -91
- package/templates/spa/src/pages/auth/_layout.tsx +0 -38
- package/templates/spa/src/pages/auth/_routes.tsx +0 -6
- package/templates/spa/src/pages/auth/login.tsx +0 -63
- package/templates/spa/src/pages/not-found.tsx +0 -22
- package/templates/spa/src/pages/public/_layout.tsx +0 -47
- package/templates/spa/src/pages/public/_routes.tsx +0 -6
- package/templates/spa/src/pages/public/home.tsx +0 -143
- package/templates/spa/src/shared/format.ts +0 -12
- package/templates/spa/src/shared/navigation.ts +0 -15
- package/templates/spa/src/styles/components.css +0 -49
- package/templates/spa/src/styles/layout.css +0 -192
- package/templates/spa/src/styles/reset.css +0 -44
- package/templates/spa/src/styles/theme.css +0 -57
- package/templates/spa/src/styles/tokens.css +0 -20
- package/templates/spa/src/styles.css +0 -7
- package/templates/spa/src/styles.d.ts +0 -1
- package/templates/spa/tests/app.test.tsx +0 -37
- package/templates/spa/tests/components/shared.test.tsx +0 -12
- package/templates/spa/tests/resources.test.ts +0 -22
- package/templates/spa/tsconfig.json +0 -20
- package/templates/spa/tsconfig.node.json +0 -10
- package/templates/spa/vite.config.ts +0 -24
- package/templates/spa/vitest.config.ts +0 -17
- package/templates/ssg/AGENTS.md +0 -55
- package/templates/ssg/README.md +0 -39
- package/templates/ssg/gitignore.template +0 -17
- package/templates/ssg/index.html +0 -13
- package/templates/ssg/package.json +0 -35
- package/templates/ssg/src/app.tsx +0 -14
- package/templates/ssg/src/components/badge.tsx +0 -3
- package/templates/ssg/src/components/counter.tsx +0 -30
- package/templates/ssg/src/components/site-shell.tsx +0 -111
- package/templates/ssg/src/jsx.d.ts +0 -23
- package/templates/ssg/src/main.tsx +0 -8
- package/templates/ssg/src/pages/about.tsx +0 -82
- package/templates/ssg/src/pages/content.tsx +0 -63
- package/templates/ssg/src/pages/example.tsx +0 -97
- package/templates/ssg/src/pages/home.tsx +0 -51
- package/templates/ssg/src/resources/user.ts +0 -15
- package/templates/ssg/src/routes.tsx +0 -15
- package/templates/ssg/src/styles.css +0 -89
- package/templates/ssg/src/vite-env.d.ts +0 -5
- package/templates/ssg/ssg.config.ts +0 -34
- package/templates/ssg/tests/app.test.tsx +0 -19
- package/templates/ssg/tests/components/counter.test.tsx +0 -18
- package/templates/ssg/tests/resources.test.ts +0 -11
- package/templates/ssg/tests/ssg-config.test.ts +0 -11
- package/templates/ssg/tsconfig.json +0 -20
- package/templates/ssg/tsconfig.node.json +0 -10
- package/templates/ssg/vite.config.ts +0 -24
- package/templates/ssg/vitest.config.ts +0 -17
- package/templates/ssr/AGENTS.md +0 -28
- package/templates/ssr/README.md +0 -38
- package/templates/ssr/gitignore.template +0 -16
- package/templates/ssr/index.html +0 -13
- package/templates/ssr/package.json +0 -34
- package/templates/ssr/server.ts +0 -16
- package/templates/ssr/src/app.tsx +0 -21
- package/templates/ssr/src/components/counter.tsx +0 -26
- package/templates/ssr/src/entry-server.tsx +0 -10
- package/templates/ssr/src/main.tsx +0 -11
- package/templates/ssr/src/pages/about.tsx +0 -75
- package/templates/ssr/src/pages/example.tsx +0 -113
- package/templates/ssr/src/pages/home.tsx +0 -56
- package/templates/ssr/src/resources/user.ts +0 -15
- package/templates/ssr/src/routes.tsx +0 -13
- package/templates/ssr/src/styles.css +0 -148
- package/templates/ssr/src/vite-server.d.ts +0 -7
- package/templates/ssr/tests/app.test.tsx +0 -24
- package/templates/ssr/tests/components/counter.test.tsx +0 -13
- package/templates/ssr/tests/resources.test.ts +0 -11
- package/templates/ssr/tsconfig.json +0 -22
- package/templates/ssr/tsconfig.node.json +0 -10
- package/templates/ssr/tsconfig.server.json +0 -15
- package/templates/ssr/vite.config.ts +0 -25
- package/templates/ssr/vitest.config.ts +0 -13
- package/templates/startkit/AGENTS.md +0 -77
- package/templates/startkit/README.md +0 -192
- package/templates/startkit/gitignore.template +0 -16
- package/templates/startkit/index.html +0 -13
- package/templates/startkit/package.json +0 -35
- package/templates/startkit/src/components/app-header.tsx +0 -93
- package/templates/startkit/src/components/app-sidebar.tsx +0 -95
- package/templates/startkit/src/components/data-table.tsx +0 -80
- package/templates/startkit/src/components/empty-state.tsx +0 -13
- package/templates/startkit/src/components/page-header.tsx +0 -23
- package/templates/startkit/src/components/stat-card.tsx +0 -24
- package/templates/startkit/src/features/accounts/account-filters.tsx +0 -61
- package/templates/startkit/src/features/accounts/account-table.tsx +0 -79
- package/templates/startkit/src/lib/format.ts +0 -31
- package/templates/startkit/src/lib/mock-data.ts +0 -441
- package/templates/startkit/src/lib/routes.ts +0 -116
- package/templates/startkit/src/main.tsx +0 -12
- package/templates/startkit/src/pages/_layout.tsx +0 -56
- package/templates/startkit/src/pages/auth/_layout.tsx +0 -7
- package/templates/startkit/src/pages/auth/login.tsx +0 -129
- package/templates/startkit/src/pages/home.tsx +0 -83
- package/templates/startkit/src/pages/not-found.tsx +0 -24
- package/templates/startkit/src/pages/workspace/_layout.tsx +0 -17
- package/templates/startkit/src/pages/workspace/accounts/index.tsx +0 -217
- package/templates/startkit/src/pages/workspace/dashboard.tsx +0 -118
- package/templates/startkit/src/pages/workspace/settings.tsx +0 -218
- package/templates/startkit/src/router.tsx +0 -7
- package/templates/startkit/src/routes/auth-config.ts +0 -38
- package/templates/startkit/src/routes/auth.ts +0 -8
- package/templates/startkit/src/routes/index.ts +0 -28
- package/templates/startkit/src/routes/public.ts +0 -8
- package/templates/startkit/src/routes/workspace/accounts.ts +0 -8
- package/templates/startkit/src/routes/workspace/index.ts +0 -14
- package/templates/startkit/src/styles/components.css +0 -396
- package/templates/startkit/src/styles/layout.css +0 -192
- package/templates/startkit/src/styles/reset.css +0 -38
- package/templates/startkit/src/styles/theme.css +0 -76
- package/templates/startkit/src/styles/tokens.css +0 -67
- package/templates/startkit/src/styles.css +0 -7
- package/templates/startkit/src/toast.ts +0 -55
- package/templates/startkit/src/utils/join-classes.ts +0 -4
- package/templates/startkit/src/vite-env.d.ts +0 -5
- package/templates/startkit/tests/app.test.tsx +0 -32
- package/templates/startkit/tests/preferences.test.ts +0 -43
- package/templates/startkit/tests/resources.test.ts +0 -82
- package/templates/startkit/tsconfig.json +0 -20
- package/templates/startkit/tsconfig.node.json +0 -10
- package/templates/startkit/vite.config.ts +0 -32
- package/templates/startkit/vitest.config.ts +0 -13
package/dist/ssg.js
CHANGED
|
@@ -1,9 +1,272 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { n as publishStagedDirectory, t as createSiblingStage } from "./directory-swap-DWoHtx7C.js";
|
|
4
|
+
import * as fs$1 from "node:fs/promises";
|
|
5
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import * as path$1 from "node:path";
|
|
7
|
+
import path, { dirname, resolve } from "node:path";
|
|
8
|
+
import { constants, existsSync } from "node:fs";
|
|
5
9
|
import { pathToFileURL } from "node:url";
|
|
6
10
|
import { register } from "tsx/esm/api";
|
|
11
|
+
//#region src/ssg/sitemap.ts
|
|
12
|
+
const datePattern = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-](\d{2}):(\d{2})))?$/;
|
|
13
|
+
const languagePattern = /^(?:x-default|[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*)$/;
|
|
14
|
+
const changeFrequencies = /* @__PURE__ */ new Set([
|
|
15
|
+
"always",
|
|
16
|
+
"hourly",
|
|
17
|
+
"daily",
|
|
18
|
+
"weekly",
|
|
19
|
+
"monthly",
|
|
20
|
+
"yearly",
|
|
21
|
+
"never"
|
|
22
|
+
]);
|
|
23
|
+
const MAX_URLS_PER_SITEMAP = 5e4;
|
|
24
|
+
const MAX_SITEMAP_BYTES = 50 * 1024 * 1024;
|
|
25
|
+
const MANIFEST_PATH = ".askr/sitemap-manifest.json";
|
|
26
|
+
function escapeXml(value) {
|
|
27
|
+
return value.replace(/[&<>"']/g, (character) => {
|
|
28
|
+
return {
|
|
29
|
+
"&": "&",
|
|
30
|
+
"<": "<",
|
|
31
|
+
">": ">",
|
|
32
|
+
"\"": """,
|
|
33
|
+
"'": "'"
|
|
34
|
+
}[character];
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function resolveSiteUrl(siteUrl) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = new URL(siteUrl);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`SSG siteUrl must be an absolute HTTP(S) URL: ${siteUrl}`);
|
|
43
|
+
}
|
|
44
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`SSG siteUrl must use HTTP or HTTPS: ${siteUrl}`);
|
|
45
|
+
if (parsed.search || parsed.hash) throw new Error("SSG siteUrl must not contain a query string or fragment");
|
|
46
|
+
if (parsed.username || parsed.password) throw new Error("SSG siteUrl must not contain credentials");
|
|
47
|
+
if (!parsed.pathname.endsWith("/")) parsed.pathname += "/";
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
function resolveLocation(value, siteUrl) {
|
|
51
|
+
let resolved;
|
|
52
|
+
try {
|
|
53
|
+
resolved = new URL(value.replace(/^\//, ""), siteUrl);
|
|
54
|
+
} catch {
|
|
55
|
+
throw new Error(`Invalid sitemap URL: ${value}`);
|
|
56
|
+
}
|
|
57
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") throw new Error(`Sitemap URLs must use HTTP or HTTPS: ${value}`);
|
|
58
|
+
if (resolved.hash) throw new Error(`Sitemap URLs must not contain fragments: ${value}`);
|
|
59
|
+
return resolved.href;
|
|
60
|
+
}
|
|
61
|
+
function formatLastModified(value) {
|
|
62
|
+
if (value instanceof Date) {
|
|
63
|
+
if (Number.isNaN(value.getTime())) throw new Error("Invalid sitemap lastModified Date");
|
|
64
|
+
return value.toISOString();
|
|
65
|
+
}
|
|
66
|
+
const match = datePattern.exec(value);
|
|
67
|
+
if (!match) throw new Error(`Invalid sitemap lastModified value: ${value}`);
|
|
68
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText, zone, offsetHour, offsetMinute] = match;
|
|
69
|
+
const year = Number(yearText);
|
|
70
|
+
const month = Number(monthText);
|
|
71
|
+
const day = Number(dayText);
|
|
72
|
+
const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
73
|
+
const invalidTime = hourText !== void 0 && (Number(hourText) > 23 || Number(minuteText) > 59 || Number(secondText) > 59 || zone !== "Z" && (Number(offsetHour) > 14 || Number(offsetMinute) > 59 || Number(offsetHour) === 14 && Number(offsetMinute) !== 0));
|
|
74
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth || invalidTime || Number.isNaN(Date.parse(value))) throw new Error(`Invalid sitemap lastModified value: ${value}`);
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
function validateChangeFrequency(value) {
|
|
78
|
+
if (!changeFrequencies.has(value)) throw new Error(`Invalid sitemap changeFrequency value: ${value}`);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function validatePriority(value) {
|
|
82
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) throw new Error(`Sitemap priority must be between 0 and 1: ${value}`);
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function outputPath(outputDir, configuredPath) {
|
|
86
|
+
const relativePath = configuredPath ?? "sitemap.xml";
|
|
87
|
+
if (path.isAbsolute(relativePath)) throw new Error(`Sitemap output must be relative to the SSG output directory: ${relativePath}`);
|
|
88
|
+
const resolved = path.resolve(outputDir, relativePath);
|
|
89
|
+
const relative = path.relative(path.resolve(outputDir), resolved);
|
|
90
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Sitemap output must stay inside the SSG output directory: ${relativePath}`);
|
|
91
|
+
return resolved;
|
|
92
|
+
}
|
|
93
|
+
function relativeOutputPath(outputDir, absolutePath) {
|
|
94
|
+
return path.relative(path.resolve(outputDir), absolutePath).replace(/\\/g, "/");
|
|
95
|
+
}
|
|
96
|
+
function validateLimit(value, maximum, label) {
|
|
97
|
+
const resolved = value ?? maximum;
|
|
98
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > maximum) throw new Error(`${label} must be an integer from 1 through ${maximum}`);
|
|
99
|
+
return resolved;
|
|
100
|
+
}
|
|
101
|
+
async function atomicWrite(destination, contents) {
|
|
102
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
103
|
+
const temporary = `${destination}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
104
|
+
try {
|
|
105
|
+
await writeFile(temporary, contents, "utf8");
|
|
106
|
+
await rename(temporary, destination);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
await rm(temporary, { force: true });
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function renderUrlSet(routes) {
|
|
113
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset${routes.some((route) => route.alternates.length > 0) ? " xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"" : " xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\""}>\n${routes.map(renderRoute).join("\n")}\n</urlset>\n`;
|
|
114
|
+
}
|
|
115
|
+
function partitionRoutes(routes, urlsPerFile, bytesPerFile) {
|
|
116
|
+
if (routes.length === 0) return [[]];
|
|
117
|
+
const chunks = [];
|
|
118
|
+
let current = [];
|
|
119
|
+
let currentEntryBytes = 0;
|
|
120
|
+
let currentHasAlternates = false;
|
|
121
|
+
const envelopeBytes = {
|
|
122
|
+
plain: Buffer.byteLength(renderUrlSet([])),
|
|
123
|
+
alternates: Buffer.byteLength("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\n\n</urlset>\n")
|
|
124
|
+
};
|
|
125
|
+
const envelope = (hasAlternates) => hasAlternates ? envelopeBytes.alternates : envelopeBytes.plain;
|
|
126
|
+
for (const route of routes) {
|
|
127
|
+
const routeBytes = Buffer.byteLength(renderRoute(route));
|
|
128
|
+
const nextHasAlternates = currentHasAlternates || route.alternates.length > 0;
|
|
129
|
+
const separatorBytes = current.length > 0 ? 1 : 0;
|
|
130
|
+
const nextBytes = envelope(nextHasAlternates) + currentEntryBytes + separatorBytes + routeBytes;
|
|
131
|
+
if (current.length > 0 && (current.length + 1 > urlsPerFile || nextBytes > bytesPerFile)) {
|
|
132
|
+
chunks.push(current);
|
|
133
|
+
current = [route];
|
|
134
|
+
currentEntryBytes = routeBytes;
|
|
135
|
+
currentHasAlternates = route.alternates.length > 0;
|
|
136
|
+
} else {
|
|
137
|
+
current.push(route);
|
|
138
|
+
currentEntryBytes += separatorBytes + routeBytes;
|
|
139
|
+
currentHasAlternates = nextHasAlternates;
|
|
140
|
+
}
|
|
141
|
+
if (envelope(currentHasAlternates) + currentEntryBytes > bytesPerFile) throw new Error(`A sitemap URL entry exceeds the configured ${bytesPerFile}-byte limit`);
|
|
142
|
+
}
|
|
143
|
+
chunks.push(current);
|
|
144
|
+
return chunks;
|
|
145
|
+
}
|
|
146
|
+
function sitemapIndex(locations) {
|
|
147
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${locations.map((location) => ` <sitemap>\n <loc>${escapeXml(location)}</loc>\n </sitemap>`).join("\n")}\n</sitemapindex>\n`;
|
|
148
|
+
}
|
|
149
|
+
async function mapConcurrent(values, concurrency, mapper) {
|
|
150
|
+
const results = Array.from({ length: values.length });
|
|
151
|
+
let cursor = 0;
|
|
152
|
+
const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
153
|
+
while (cursor < values.length) {
|
|
154
|
+
const index = cursor;
|
|
155
|
+
cursor += 1;
|
|
156
|
+
results[index] = await mapper(values[index], index);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
await Promise.all(workers);
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
async function readManifest(outputDir) {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(await readFile(outputPath(outputDir, MANIFEST_PATH), "utf8"));
|
|
165
|
+
return {
|
|
166
|
+
files: Array.isArray(parsed.files) ? parsed.files.filter((value) => typeof value === "string") : [],
|
|
167
|
+
...typeof parsed.robots === "string" ? { robots: parsed.robots } : {}
|
|
168
|
+
};
|
|
169
|
+
} catch {
|
|
170
|
+
return { files: [] };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function withoutSitemapDirective(contents) {
|
|
174
|
+
return contents.split(/\r?\n/).filter((line) => !/^\s*Sitemap\s*:/i.test(line)).join("\n").replace(/\n+$/, "");
|
|
175
|
+
}
|
|
176
|
+
async function updateRobots(outputDir, previousPath, configured, sitemapUrl) {
|
|
177
|
+
const nextPath = configured === false ? void 0 : configured && typeof configured === "object" ? configured.output ?? "robots.txt" : "robots.txt";
|
|
178
|
+
const paths = new Set([previousPath, nextPath].filter((value) => Boolean(value)));
|
|
179
|
+
for (const relativePath of paths) {
|
|
180
|
+
const destination = outputPath(outputDir, relativePath);
|
|
181
|
+
let contents = withoutSitemapDirective(await readFile(destination, "utf8").catch(() => ""));
|
|
182
|
+
if (relativePath === nextPath) {
|
|
183
|
+
if (!contents) contents = "User-agent: *\nAllow: /";
|
|
184
|
+
contents = `${contents}\nSitemap: ${sitemapUrl}`;
|
|
185
|
+
}
|
|
186
|
+
if (contents) await atomicWrite(destination, `${contents}\n`);
|
|
187
|
+
else await rm(destination, { force: true });
|
|
188
|
+
}
|
|
189
|
+
return nextPath;
|
|
190
|
+
}
|
|
191
|
+
async function removeGeneratedSitemap(outputDir) {
|
|
192
|
+
const previous = await readManifest(outputDir);
|
|
193
|
+
await mapConcurrent(previous.files, 32, async (file) => rm(outputPath(outputDir, file), { force: true }));
|
|
194
|
+
await updateRobots(outputDir, previous.robots, false, "");
|
|
195
|
+
await rm(outputPath(outputDir, MANIFEST_PATH), { force: true });
|
|
196
|
+
}
|
|
197
|
+
function renderRoute(route) {
|
|
198
|
+
const lines = [" <url>", ` <loc>${escapeXml(route.location)}</loc>`];
|
|
199
|
+
if (route.lastModified) lines.push(` <lastmod>${escapeXml(route.lastModified)}</lastmod>`);
|
|
200
|
+
if (route.changeFrequency) lines.push(` <changefreq>${route.changeFrequency}</changefreq>`);
|
|
201
|
+
if (route.priority !== void 0) lines.push(` <priority>${route.priority}</priority>`);
|
|
202
|
+
for (const alternate of route.alternates) lines.push(` <xhtml:link rel="alternate" hreflang="${escapeXml(alternate.language)}" href="${escapeXml(alternate.location)}" />`);
|
|
203
|
+
lines.push(" </url>");
|
|
204
|
+
return lines.join("\n");
|
|
205
|
+
}
|
|
206
|
+
async function generateSitemap(outputDir, siteUrl, routes, config = {}) {
|
|
207
|
+
const baseUrl = resolveSiteUrl(siteUrl);
|
|
208
|
+
const resolverConcurrency = validateLimit(config.resolverConcurrency ?? 16, 128, "Sitemap resolver concurrency");
|
|
209
|
+
const generated = (await mapConcurrent(routes.filter((route) => (route.status === "success" || route.status === "skipped") && !route.path.includes("*")), resolverConcurrency, async (route) => {
|
|
210
|
+
const exact = config.routes?.[route.path];
|
|
211
|
+
if (exact === false) return void 0;
|
|
212
|
+
const resolved = await config.resolve?.(route);
|
|
213
|
+
if (resolved === false) return void 0;
|
|
214
|
+
const merged = {
|
|
215
|
+
...config.defaults,
|
|
216
|
+
...exact,
|
|
217
|
+
...resolved
|
|
218
|
+
};
|
|
219
|
+
const alternates = [];
|
|
220
|
+
for (const [language, location] of Object.entries(merged.alternates ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
|
|
221
|
+
if (!languagePattern.test(language)) throw new Error(`Invalid sitemap hreflang value: ${language}`);
|
|
222
|
+
alternates.push({
|
|
223
|
+
language,
|
|
224
|
+
location: resolveLocation(location, baseUrl)
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
location: resolveLocation(merged.url ?? route.path, baseUrl),
|
|
229
|
+
...merged.lastModified !== void 0 ? { lastModified: formatLastModified(merged.lastModified) } : {},
|
|
230
|
+
...merged.changeFrequency ? { changeFrequency: validateChangeFrequency(merged.changeFrequency) } : {},
|
|
231
|
+
...merged.priority !== void 0 ? { priority: validatePriority(merged.priority) } : {},
|
|
232
|
+
alternates
|
|
233
|
+
};
|
|
234
|
+
})).filter((route) => Boolean(route));
|
|
235
|
+
generated.sort((a, b) => a.location.localeCompare(b.location));
|
|
236
|
+
const locations = /* @__PURE__ */ new Set();
|
|
237
|
+
for (const route of generated) {
|
|
238
|
+
if (locations.has(route.location)) throw new Error(`Sitemap contains a duplicate canonical URL: ${route.location}`);
|
|
239
|
+
locations.add(route.location);
|
|
240
|
+
}
|
|
241
|
+
const destination = outputPath(outputDir, config.output);
|
|
242
|
+
const chunks = partitionRoutes(generated, validateLimit(config.limits?.urlsPerFile, MAX_URLS_PER_SITEMAP, "Sitemap URL limit"), validateLimit(config.limits?.bytesPerFile, MAX_SITEMAP_BYTES, "Sitemap byte limit"));
|
|
243
|
+
if (chunks.length > MAX_URLS_PER_SITEMAP) throw new Error(`Sitemap index exceeds ${MAX_URLS_PER_SITEMAP} sitemap entries`);
|
|
244
|
+
const generatedFiles = [];
|
|
245
|
+
if (chunks.length === 1) {
|
|
246
|
+
await atomicWrite(destination, renderUrlSet(chunks[0]));
|
|
247
|
+
generatedFiles.push(relativeOutputPath(outputDir, destination));
|
|
248
|
+
} else {
|
|
249
|
+
const extension = path.extname(destination);
|
|
250
|
+
const base = destination.slice(0, extension ? -extension.length : void 0);
|
|
251
|
+
const chunkDestinations = chunks.map((_, index) => `${base}-${index + 1}.xml`);
|
|
252
|
+
await mapConcurrent(chunks, 16, async (chunk, index) => atomicWrite(chunkDestinations[index], renderUrlSet(chunk)));
|
|
253
|
+
const indexXml = sitemapIndex(chunkDestinations.map((chunkPath) => resolveLocation(relativeOutputPath(outputDir, chunkPath), baseUrl)));
|
|
254
|
+
if (Buffer.byteLength(indexXml) > MAX_SITEMAP_BYTES) throw new Error(`Sitemap index exceeds ${MAX_SITEMAP_BYTES} bytes`);
|
|
255
|
+
await atomicWrite(destination, indexXml);
|
|
256
|
+
generatedFiles.push(relativeOutputPath(outputDir, destination), ...chunkDestinations.map((chunkPath) => relativeOutputPath(outputDir, chunkPath)));
|
|
257
|
+
}
|
|
258
|
+
const previous = await readManifest(outputDir);
|
|
259
|
+
const current = new Set(generatedFiles);
|
|
260
|
+
await mapConcurrent(previous.files.filter((file) => !current.has(file)), 32, async (file) => rm(outputPath(outputDir, file), { force: true }));
|
|
261
|
+
const sitemapUrl = resolveLocation(relativeOutputPath(outputDir, destination), baseUrl);
|
|
262
|
+
const robots = await updateRobots(outputDir, previous.robots, config.robots, sitemapUrl);
|
|
263
|
+
await atomicWrite(outputPath(outputDir, MANIFEST_PATH), `${JSON.stringify({
|
|
264
|
+
files: generatedFiles,
|
|
265
|
+
...robots ? { robots } : {}
|
|
266
|
+
}, null, 2)}\n`);
|
|
267
|
+
return destination;
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
7
270
|
//#region src/bin/ssg.ts
|
|
8
271
|
const helpText = `
|
|
9
272
|
askr ssg - Static Site Generation for Askr
|
|
@@ -30,6 +293,14 @@ const defaultDeps = {
|
|
|
30
293
|
now: () => Date.now(),
|
|
31
294
|
existsSync
|
|
32
295
|
};
|
|
296
|
+
async function pathExists(filePath) {
|
|
297
|
+
try {
|
|
298
|
+
await fs$1.access(filePath);
|
|
299
|
+
return true;
|
|
300
|
+
} catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
33
304
|
async function importProjectConfig(filePath) {
|
|
34
305
|
const tsconfig = resolve(dirname(filePath), "tsconfig.json");
|
|
35
306
|
const unregister = register(existsSync(tsconfig) ? { tsconfig } : void 0);
|
|
@@ -57,26 +328,56 @@ function parseCliArgs(args) {
|
|
|
57
328
|
changedKeys: [],
|
|
58
329
|
changedRoutes: [],
|
|
59
330
|
forceFull: false,
|
|
60
|
-
help: false
|
|
331
|
+
help: false,
|
|
332
|
+
errors: []
|
|
333
|
+
};
|
|
334
|
+
const valueFor = (index, option) => {
|
|
335
|
+
const value = args[index + 1];
|
|
336
|
+
if (!value || value.startsWith("-")) {
|
|
337
|
+
parsed.errors.push(`Missing value for ${option}`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
return value;
|
|
61
341
|
};
|
|
62
|
-
for (let i = 0; i < args.length; i += 1) if (args[i] === "--config"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} else if (args[i] === "--
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
} else if (args[i] === "--
|
|
75
|
-
|
|
76
|
-
|
|
342
|
+
for (let i = 0; i < args.length; i += 1) if (args[i] === "--config") {
|
|
343
|
+
const value = valueFor(i, "--config");
|
|
344
|
+
if (value) {
|
|
345
|
+
parsed.configPath = value;
|
|
346
|
+
i += 1;
|
|
347
|
+
}
|
|
348
|
+
} else if (args[i] === "--output") {
|
|
349
|
+
const value = valueFor(i, "--output");
|
|
350
|
+
if (value) {
|
|
351
|
+
parsed.outputDir = value;
|
|
352
|
+
i += 1;
|
|
353
|
+
}
|
|
354
|
+
} else if (args[i] === "--workers") {
|
|
355
|
+
const value = valueFor(i, "--workers");
|
|
356
|
+
if (value) {
|
|
357
|
+
if (value === "auto") parsed.workers = "auto";
|
|
358
|
+
else {
|
|
359
|
+
const workers = Number(value);
|
|
360
|
+
if (!Number.isSafeInteger(workers) || workers < 1) parsed.errors.push("--workers must be 'auto' or a positive integer");
|
|
361
|
+
else parsed.workers = workers;
|
|
362
|
+
}
|
|
363
|
+
i += 1;
|
|
364
|
+
}
|
|
365
|
+
} else if (args[i] === "--changed-key") {
|
|
366
|
+
const value = valueFor(i, "--changed-key");
|
|
367
|
+
if (value) {
|
|
368
|
+
parsed.changedKeys.push(value);
|
|
369
|
+
i += 1;
|
|
370
|
+
}
|
|
371
|
+
} else if (args[i] === "--changed-route") {
|
|
372
|
+
const value = valueFor(i, "--changed-route");
|
|
373
|
+
if (value) {
|
|
374
|
+
parsed.changedRoutes.push(value);
|
|
375
|
+
i += 1;
|
|
376
|
+
}
|
|
77
377
|
} else if (args[i] === "--incremental") parsed.incremental = true;
|
|
78
378
|
else if (args[i] === "--force-full") parsed.forceFull = true;
|
|
79
379
|
else if (args[i] === "--help" || args[i] === "-h") parsed.help = true;
|
|
380
|
+
else parsed.errors.push(`Unknown option: ${args[i]}`);
|
|
80
381
|
return parsed;
|
|
81
382
|
}
|
|
82
383
|
function toGenerateOptions(args) {
|
|
@@ -87,7 +388,7 @@ function toGenerateOptions(args) {
|
|
|
87
388
|
forceFull: args.forceFull
|
|
88
389
|
};
|
|
89
390
|
}
|
|
90
|
-
function printSummary(io, outputDir, durationSeconds, result) {
|
|
391
|
+
function printSummary(io, outputDir, durationSeconds, result, sitemapPath) {
|
|
91
392
|
io.log("");
|
|
92
393
|
io.log(`Generation complete in ${durationSeconds}s`);
|
|
93
394
|
io.log(` Mode: ${result.mode}`);
|
|
@@ -99,6 +400,7 @@ function printSummary(io, outputDir, durationSeconds, result) {
|
|
|
99
400
|
io.log(` CacheHit: ${result.cacheHits} routes`);
|
|
100
401
|
io.log(` Output: ${outputDir}`);
|
|
101
402
|
io.log(` Metadata: ${outputDir}/metadata.json`);
|
|
403
|
+
if (sitemapPath) io.log(` Sitemap: ${sitemapPath}`);
|
|
102
404
|
io.log("");
|
|
103
405
|
}
|
|
104
406
|
async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console) {
|
|
@@ -107,6 +409,10 @@ async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console)
|
|
|
107
409
|
io.log(helpText);
|
|
108
410
|
return 0;
|
|
109
411
|
}
|
|
412
|
+
if (parsed.errors.length > 0) {
|
|
413
|
+
for (const error of parsed.errors) io.error(`Error: ${error}`);
|
|
414
|
+
return 1;
|
|
415
|
+
}
|
|
110
416
|
if (!parsed.configPath) {
|
|
111
417
|
io.error("Error: --config argument is required");
|
|
112
418
|
return 1;
|
|
@@ -125,11 +431,22 @@ async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console)
|
|
|
125
431
|
};
|
|
126
432
|
const resolvedConfigPath = resolve(resolvedDeps.cwd(), parsed.configPath);
|
|
127
433
|
const resolvedOutputDir = resolve(resolvedDeps.cwd(), parsed.outputDir);
|
|
434
|
+
const outputStat = await fs$1.stat(resolvedOutputDir).catch(() => null);
|
|
435
|
+
if (outputStat && !outputStat.isDirectory()) {
|
|
436
|
+
io.error(`Error: SSG output exists and is not a directory: ${resolvedOutputDir}`);
|
|
437
|
+
return 1;
|
|
438
|
+
}
|
|
439
|
+
const configWithinOutput = path$1.relative(resolvedOutputDir, resolvedConfigPath);
|
|
440
|
+
if (resolvedOutputDir === path$1.resolve(resolvedDeps.cwd()) || !configWithinOutput.startsWith("..") && !path$1.isAbsolute(configWithinOutput)) {
|
|
441
|
+
io.error("Error: --output must not contain the project or SSG config file");
|
|
442
|
+
return 1;
|
|
443
|
+
}
|
|
128
444
|
if (!resolvedDeps.existsSync(resolvedConfigPath)) {
|
|
129
445
|
io.error(`Error: Config file not found: ${resolvedConfigPath}`);
|
|
130
446
|
return 1;
|
|
131
447
|
}
|
|
132
448
|
let unregisterProjectLoader;
|
|
449
|
+
let cliStagingDir;
|
|
133
450
|
try {
|
|
134
451
|
io.log(`Loading config: ${resolvedConfigPath}`);
|
|
135
452
|
let imported;
|
|
@@ -147,10 +464,21 @@ async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console)
|
|
|
147
464
|
return 1;
|
|
148
465
|
}
|
|
149
466
|
const config = candidate;
|
|
467
|
+
if (config.sitemap !== false && !config.siteUrl) {
|
|
468
|
+
io.error("Error: Config must provide siteUrl to generate sitemap.xml, or set sitemap: false");
|
|
469
|
+
return 1;
|
|
470
|
+
}
|
|
150
471
|
io.log(hasRoutes ? `Generating ${config.routes?.length ?? 0} routes...` : "Generating registered routes...");
|
|
151
|
-
const
|
|
472
|
+
const createStaticGen = typeof resolvedDeps.createStaticGen === "function" ? resolvedDeps.createStaticGen : await loadCreateStaticGen();
|
|
473
|
+
cliStagingDir = await createSiblingStage(resolvedOutputDir, "askr-ssg");
|
|
474
|
+
if (parsed.incremental && !parsed.forceFull && await pathExists(resolvedOutputDir)) await fs$1.cp(resolvedOutputDir, cliStagingDir, {
|
|
475
|
+
recursive: true,
|
|
476
|
+
mode: constants.COPYFILE_FICLONE
|
|
477
|
+
});
|
|
478
|
+
const generationOutputDir = cliStagingDir;
|
|
479
|
+
const ssg = createStaticGen({
|
|
152
480
|
...hasRoutes ? { routes: config.routes } : { registry: config.registry },
|
|
153
|
-
outputDir:
|
|
481
|
+
outputDir: generationOutputDir,
|
|
154
482
|
seed: config.seed,
|
|
155
483
|
dataOverrides: config.dataOverrides,
|
|
156
484
|
concurrency: config.concurrency,
|
|
@@ -160,7 +488,13 @@ async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console)
|
|
|
160
488
|
});
|
|
161
489
|
const startTime = resolvedDeps.now();
|
|
162
490
|
const result = await ssg.generate(toGenerateOptions(parsed));
|
|
163
|
-
|
|
491
|
+
const sitemapPath = result.failed === 0 && config.sitemap !== false && config.siteUrl ? await generateSitemap(generationOutputDir, config.siteUrl, result.routes, config.sitemap) : void 0;
|
|
492
|
+
if (result.failed === 0 && config.sitemap === false) await removeGeneratedSitemap(generationOutputDir);
|
|
493
|
+
if (result.failed === 0 && cliStagingDir) {
|
|
494
|
+
await publishStagedDirectory(cliStagingDir, resolvedOutputDir);
|
|
495
|
+
cliStagingDir = void 0;
|
|
496
|
+
}
|
|
497
|
+
printSummary(io, resolvedOutputDir, ((resolvedDeps.now() - startTime) / 1e3).toFixed(2), result, sitemapPath ? path$1.join(resolvedOutputDir, path$1.relative(generationOutputDir, sitemapPath)) : void 0);
|
|
164
498
|
if (result.failed > 0) {
|
|
165
499
|
io.log("Errors encountered:");
|
|
166
500
|
for (const route of result.routes) if (route.status === "error") io.log(` ${route.path}: ${route.error}`);
|
|
@@ -174,6 +508,10 @@ async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console)
|
|
|
174
508
|
if (error instanceof Error && error.stack) io.error(error.stack);
|
|
175
509
|
return 1;
|
|
176
510
|
} finally {
|
|
511
|
+
if (cliStagingDir) await fs$1.rm(cliStagingDir, {
|
|
512
|
+
recursive: true,
|
|
513
|
+
force: true
|
|
514
|
+
});
|
|
177
515
|
await unregisterProjectLoader?.();
|
|
178
516
|
}
|
|
179
517
|
}
|
|
@@ -184,5 +522,3 @@ async function main() {
|
|
|
184
522
|
if (isDirectExecution(import.meta.url)) main();
|
|
185
523
|
//#endregion
|
|
186
524
|
export { parseCliArgs, runSsgCli };
|
|
187
|
-
|
|
188
|
-
//# sourceMappingURL=ssg.js.map
|
|
@@ -9,4 +9,3 @@ This project is the progressive full-stack stage of an Askr application.
|
|
|
9
9
|
- Keep `index.html` as the only document source. Preserve exactly one `<!--askr-head-->` and one `<!--askr-app-->` marker.
|
|
10
10
|
- Use native forms first; enhanced submission must preserve the same validation and authorization behavior.
|
|
11
11
|
- Never log cookies, authorization values, tokens, form fields, request bodies, or personal data.
|
|
12
|
-
|
|
@@ -15,4 +15,3 @@ npm run preview
|
|
|
15
15
|
Use `askr add action <name> --route <path>` to generate a browser-safe descriptor, server handler, composition-root registration, route authorization, and focused test.
|
|
16
16
|
|
|
17
17
|
`index.html` owns the static document. `@askrjs/vite/server` injects only Askr-owned head nodes and composes the streamed app response at `<!--askr-app-->`.
|
|
18
|
-
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"check": "npm run lint && npm run typecheck && npm test && npm run build"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@askrjs/auth": ">=0.0.1 <0.1.0",
|
|
18
17
|
"@askrjs/askr": ">=0.0.53 <0.1.0",
|
|
18
|
+
"@askrjs/auth": ">=0.0.1 <0.1.0",
|
|
19
19
|
"@askrjs/i18n": ">=0.0.1 <0.1.0",
|
|
20
20
|
"@askrjs/node": ">=0.0.1 <0.1.0",
|
|
21
21
|
"@askrjs/otel": ">=0.0.1 <0.1.0",
|
|
@@ -27,11 +27,13 @@
|
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@askrjs/vite": ">=0.0.6 <0.1.0",
|
|
30
|
-
"@types/node": "^
|
|
30
|
+
"@types/node": "^20.19.0",
|
|
31
31
|
"jsdom": "^29.1.1",
|
|
32
32
|
"typescript": "^6.0.3",
|
|
33
33
|
"vite-plus": "^0.2.4",
|
|
34
34
|
"vitest": "^4.1.10"
|
|
35
35
|
},
|
|
36
|
-
"engines": {
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
38
|
+
}
|
|
37
39
|
}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import app from
|
|
1
|
+
import { serve } from "@askrjs/node";
|
|
2
|
+
import app from "virtual:askr-server";
|
|
3
3
|
|
|
4
4
|
const port = Number(process.env.PORT ?? 3000);
|
|
5
|
-
|
|
6
|
-
host: process.env.HOST ??
|
|
5
|
+
await serve(app, {
|
|
6
|
+
host: process.env.HOST ?? "127.0.0.1",
|
|
7
7
|
port,
|
|
8
|
+
assets: { root: new URL("./dist", import.meta.url).pathname },
|
|
8
9
|
});
|
|
9
|
-
|
|
10
|
-
const shutdown = () => server.close();
|
|
11
|
-
process.once('SIGINT', shutdown);
|
|
12
|
-
process.once('SIGTERM', shutdown);
|
|
13
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ActionDescriptor } from
|
|
2
|
-
import { createMessageAction } from
|
|
1
|
+
import type { ActionDescriptor } from "@askrjs/askr/actions";
|
|
2
|
+
import { createMessageAction } from "./actions/create-message";
|
|
3
3
|
|
|
4
4
|
const actionsByRoute: Readonly<Record<string, readonly ActionDescriptor[]>> = Object.freeze({
|
|
5
5
|
"/": [createMessageAction],
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { defineAction } from
|
|
2
|
-
import { MessageInput } from
|
|
1
|
+
import { defineAction } from "@askrjs/askr/actions";
|
|
2
|
+
import { MessageInput } from "../schemas";
|
|
3
3
|
|
|
4
|
-
export const createMessageActionRoute =
|
|
4
|
+
export const createMessageActionRoute = "/";
|
|
5
5
|
|
|
6
6
|
export const createMessageAction = defineAction({
|
|
7
|
-
id:
|
|
7
|
+
id: "create-message",
|
|
8
8
|
input: MessageInput,
|
|
9
|
-
invalidates: [
|
|
9
|
+
invalidates: ["messages"],
|
|
10
10
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createI18n } from
|
|
1
|
+
import { createI18n } from "@askrjs/i18n";
|
|
2
2
|
|
|
3
|
-
export const messages = createI18n(
|
|
3
|
+
export const messages = createI18n("en", {
|
|
4
4
|
en: {
|
|
5
|
-
title: () =>
|
|
6
|
-
intro: () =>
|
|
5
|
+
title: () => "{{appName}}",
|
|
6
|
+
intro: () => "A progressive full-stack Askr application.",
|
|
7
7
|
},
|
|
8
8
|
});
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { createSPA, hydrateSPA } from
|
|
2
|
-
import { pageRegistry } from
|
|
1
|
+
import { createSPA, hydrateSPA } from "@askrjs/askr/boot";
|
|
2
|
+
import { pageRegistry } from "./routes";
|
|
3
3
|
|
|
4
|
-
const root = document.getElementById(
|
|
5
|
-
if (!root) throw new Error(
|
|
4
|
+
const root = document.getElementById("app");
|
|
5
|
+
if (!root) throw new Error("Missing #app root element.");
|
|
6
6
|
|
|
7
7
|
if (root.hasChildNodes()) {
|
|
8
8
|
await hydrateSPA({ root, registry: pageRegistry });
|
|
9
9
|
} else {
|
|
10
10
|
await createSPA({ root, registry: pageRegistry });
|
|
11
11
|
}
|
|
12
|
-
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { ActionForm } from
|
|
2
|
-
import { createMessageAction } from
|
|
3
|
-
import { messages } from
|
|
1
|
+
import { ActionForm } from "@askrjs/askr/actions";
|
|
2
|
+
import { createMessageAction } from "../actions/create-message";
|
|
3
|
+
import { messages } from "../i18n";
|
|
4
4
|
|
|
5
5
|
export function HomePage() {
|
|
6
6
|
return (
|
|
7
7
|
<main>
|
|
8
|
-
<h1>{messages.text(
|
|
9
|
-
<p>{messages.text(
|
|
8
|
+
<h1>{messages.text("title")}</h1>
|
|
9
|
+
<p>{messages.text("intro")}</p>
|
|
10
10
|
<form method="post" action="/api/session">
|
|
11
11
|
<button type="submit">Start demo session</button>
|
|
12
12
|
</form>
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { Link } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { Link } from "@askrjs/askr/router";
|
|
2
|
+
import {
|
|
3
|
+
Block,
|
|
4
|
+
Container,
|
|
5
|
+
Header,
|
|
6
|
+
Navbar,
|
|
7
|
+
NavBrand,
|
|
8
|
+
NavGroup,
|
|
9
|
+
NavLink,
|
|
10
|
+
} from "@askrjs/themes/components";
|
|
11
|
+
import { ThemeScope, ThemeToggle } from "@askrjs/themes/theme";
|
|
12
|
+
import { messages } from "../i18n";
|
|
5
13
|
|
|
6
14
|
function ScopedLayout({ children }: { children?: unknown }) {
|
|
7
15
|
return (
|
|
@@ -10,7 +18,9 @@ function ScopedLayout({ children }: { children?: unknown }) {
|
|
|
10
18
|
<Header sticky>
|
|
11
19
|
<Container paddingY="sm">
|
|
12
20
|
<Navbar aria-label="Primary navigation" width="full">
|
|
13
|
-
<NavBrand
|
|
21
|
+
<NavBrand>
|
|
22
|
+
<Link href="/">{messages.text("title")}</Link>
|
|
23
|
+
</NavBrand>
|
|
14
24
|
<NavGroup align="end">
|
|
15
25
|
<NavLink href="/">Home</NavLink>
|
|
16
26
|
<ThemeToggle aria-label="Toggle theme" />
|