create-lumfall 1.0.1 → 1.1.1

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.
Files changed (55) hide show
  1. package/README.md +24 -17
  2. package/cli.js +177 -22
  3. package/package.json +5 -7
  4. package/templates/basic/README.md +0 -113
  5. package/templates/basic/app/controller/demo.js +0 -28
  6. package/templates/basic/app/extend/README.md +0 -3
  7. package/templates/basic/app/middleware/README.md +0 -2
  8. package/templates/basic/app/middleware.js +0 -13
  9. package/templates/basic/app/pages/home/entry.home.js +0 -4
  10. package/templates/basic/app/pages/home/home.vue +0 -164
  11. package/templates/basic/app/router/demo.js +0 -17
  12. package/templates/basic/app/router-schema/demo.js +0 -29
  13. package/templates/basic/app/service/demo.js +0 -51
  14. package/templates/basic/app/webpack.config.js +0 -4
  15. package/templates/basic/build.js +0 -7
  16. package/templates/basic/config/config.beta.js +0 -4
  17. package/templates/basic/config/config.default.js +0 -32
  18. package/templates/basic/config/config.local.js +0 -4
  19. package/templates/basic/config/config.prod.js +0 -6
  20. package/templates/basic/package.json +0 -31
  21. package/templates/basic/server.js +0 -26
  22. package/templates/document/README.md +0 -58
  23. package/templates/document/app/extend/README.md +0 -6
  24. package/templates/document/app/middleware/README.md +0 -2
  25. package/templates/document/app/middleware.js +0 -2
  26. package/templates/document/app/pages/docs/assets/docs-logo.svg +0 -5
  27. package/templates/document/app/pages/docs/components/doc-layout.vue +0 -144
  28. package/templates/document/app/pages/docs/components/doc-navbar.vue +0 -103
  29. package/templates/document/app/pages/docs/components/doc-search.vue +0 -131
  30. package/templates/document/app/pages/docs/components/doc-sidebar.vue +0 -29
  31. package/templates/document/app/pages/docs/components/doc-toc.vue +0 -21
  32. package/templates/document/app/pages/docs/content.js +0 -28
  33. package/templates/document/app/pages/docs/docs-config.js +0 -68
  34. package/templates/document/app/pages/docs/docs.vue +0 -24
  35. package/templates/document/app/pages/docs/entry.docs.js +0 -26
  36. package/templates/document/app/pages/docs/markdown/highlight.js +0 -30
  37. package/templates/document/app/pages/docs/markdown/index.js +0 -129
  38. package/templates/document/app/pages/docs/search.js +0 -142
  39. package/templates/document/app/pages/docs/styles/docs.less +0 -1091
  40. package/templates/document/app/pages/docs/styles/vars.less +0 -87
  41. package/templates/document/app/pages/docs/theme.js +0 -47
  42. package/templates/document/app/pages/docs/utils.js +0 -58
  43. package/templates/document/app/pages/docs/views/doc-home.vue +0 -56
  44. package/templates/document/app/pages/docs/views/doc-page.vue +0 -147
  45. package/templates/document/app/webpack.config.js +0 -15
  46. package/templates/document/build.js +0 -6
  47. package/templates/document/config/config.beta.js +0 -2
  48. package/templates/document/config/config.default.js +0 -9
  49. package/templates/document/config/config.local.js +0 -2
  50. package/templates/document/config/config.prod.js +0 -2
  51. package/templates/document/docs/guide/example.md +0 -41
  52. package/templates/document/docs/guide/introduction.md +0 -31
  53. package/templates/document/package.json +0 -36
  54. package/templates/document/scripts/build-static.js +0 -129
  55. package/templates/document/server.js +0 -13
@@ -1,87 +0,0 @@
1
- // 文档站设计变量:所有颜色走 CSS 变量,暗色模式通过 body[arco-theme="dark"]
2
- // 覆盖变量实现(Arco 组件的暗色也由同一属性驱动,两边自动同步)。
3
-
4
- :root {
5
- // 基础色板
6
- --doc-bg: #ffffff;
7
- --doc-bg-soft: #f7f8fa;
8
- --doc-navbar-bg: rgba(255, 255, 255, 0.86);
9
- --doc-sidebar-bg: #ffffff;
10
- --doc-text-1: #1d2129;
11
- --doc-text-2: #4e5969;
12
- --doc-text-3: #86909c;
13
- --doc-border: #e5e6eb;
14
- --doc-brand: #165dff;
15
- --doc-brand-soft: rgba(22, 93, 255, 0.08);
16
- --doc-mark-bg: rgba(22, 93, 255, 0.16);
17
-
18
- // 代码块
19
- --doc-code-bg: #f7f8fa;
20
- --doc-code-bar-bg: #f0f1f3;
21
- --doc-code-border: #e5e6eb;
22
- --doc-code-text: #24292f;
23
- --doc-inline-code-bg: rgba(22, 93, 255, 0.06);
24
- --doc-inline-code-text: #165dff;
25
-
26
- // highlight.js 语法高亮(亮色:接近 GitHub Light)
27
- --doc-hl-keyword: #cf222e;
28
- --doc-hl-built-in: #0550ae;
29
- --doc-hl-type: #953800;
30
- --doc-hl-literal: #0550ae;
31
- --doc-hl-number: #0550ae;
32
- --doc-hl-string: #0a3069;
33
- --doc-hl-comment: #6e7781;
34
- --doc-hl-function: #8250df;
35
- --doc-hl-tag: #116329;
36
- --doc-hl-attr: #0550ae;
37
- --doc-hl-variable: #953800;
38
- --doc-hl-meta: #6e7781;
39
- --doc-hl-section: #0550ae;
40
- --doc-hl-selector: #6639ba;
41
- --doc-hl-addition-bg: #dafbe1;
42
- --doc-hl-deletion-bg: #ffebe9;
43
-
44
- // 阴影
45
- --doc-shadow: 0 4px 10px rgba(0, 0, 0, 0.06);
46
- }
47
-
48
- body[arco-theme="dark"] {
49
- --doc-bg: #0d0e10;
50
- --doc-bg-soft: #17181a;
51
- --doc-navbar-bg: rgba(23, 24, 26, 0.86);
52
- --doc-sidebar-bg: #0d0e10;
53
- --doc-text-1: #f2f3f5;
54
- --doc-text-2: #c2c7cc;
55
- --doc-text-3: #8a9099;
56
- --doc-border: #333335;
57
- --doc-brand: #3c7eff;
58
- --doc-brand-soft: rgba(60, 126, 255, 0.16);
59
- --doc-mark-bg: rgba(60, 126, 255, 0.32);
60
-
61
- --doc-code-bg: #17181a;
62
- --doc-code-bar-bg: #1f2022;
63
- --doc-code-border: #333335;
64
- --doc-code-text: #e6e8eb;
65
- --doc-inline-code-bg: rgba(60, 126, 255, 0.14);
66
- --doc-inline-code-text: #84a8ff;
67
-
68
- // 暗色语法高亮(接近 GitHub Dark)
69
- --doc-hl-keyword: #ff7b72;
70
- --doc-hl-built-in: #79c0ff;
71
- --doc-hl-type: #ffa657;
72
- --doc-hl-literal: #79c0ff;
73
- --doc-hl-number: #79c0ff;
74
- --doc-hl-string: #a5d6ff;
75
- --doc-hl-comment: #8b949e;
76
- --doc-hl-function: #d2a8ff;
77
- --doc-hl-tag: #7ee787;
78
- --doc-hl-attr: #79c0ff;
79
- --doc-hl-variable: #ffa657;
80
- --doc-hl-meta: #8b949e;
81
- --doc-hl-section: #79c0ff;
82
- --doc-hl-selector: #d2a8ff;
83
- --doc-hl-addition-bg: rgba(46, 160, 67, 0.28);
84
- --doc-hl-deletion-bg: rgba(248, 81, 73, 0.24);
85
-
86
- --doc-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
87
- }
@@ -1,47 +0,0 @@
1
- // 主题(亮 / 暗)管理。Arco Design 通过 body 上的 arco-theme 属性切换暗色,
2
- // 文档站自己的样式全部走 CSS 变量(见 styles/vars.less),跟随该属性变化。
3
- const STORAGE_KEY = "lumfall-docs-theme";
4
-
5
- function apply(mode) {
6
- if (mode === "dark") {
7
- document.body.setAttribute("arco-theme", "dark");
8
- } else {
9
- document.body.removeAttribute("arco-theme");
10
- }
11
- }
12
-
13
- export function isDark() {
14
- return document.body.getAttribute("arco-theme") === "dark";
15
- }
16
-
17
- /** 进站时调用:优先用户上次选择,否则跟随系统偏好 */
18
- export function initTheme() {
19
- let saved = null;
20
- try {
21
- saved = localStorage.getItem(STORAGE_KEY);
22
- } catch (e) {
23
- // localStorage 不可用(如隐私模式)时只跟随系统
24
- }
25
- if (saved === "dark" || saved === "light") {
26
- apply(saved);
27
- return;
28
- }
29
- if (
30
- window.matchMedia &&
31
- window.matchMedia("(prefers-color-scheme: dark)").matches
32
- ) {
33
- apply("dark");
34
- }
35
- }
36
-
37
- /** 切换主题并持久化,返回切换后的模式 */
38
- export function toggleTheme() {
39
- const next = isDark() ? "light" : "dark";
40
- apply(next);
41
- try {
42
- localStorage.setItem(STORAGE_KEY, next);
43
- } catch (e) {
44
- // 忽略持久化失败
45
- }
46
- return next;
47
- }
@@ -1,58 +0,0 @@
1
- /**
2
- * 标题 → url slug。保留中文等 unicode 字母数字,空格转 -,
3
- * 与 markdown 渲染器的标题 id 规则保持一致(搜索跳转锚点也用它)。
4
- */
5
- export function slugify(text) {
6
- return String(text)
7
- .trim()
8
- .toLowerCase()
9
- .replace(/[^\p{L}\p{N}\s-]/gu, "")
10
- .replace(/\s+/g, "-");
11
- }
12
-
13
- /** "/view/docs/guide/intro" → "guide/intro" */
14
- export function pathOfDoc(viewPath) {
15
- return String(viewPath || "")
16
- .replace(/^\/view\/docs\/?/, "")
17
- .replace(/\/+$/, "");
18
- }
19
-
20
- /**
21
- * 解析文档内相对链接(xxx.md / ./xxx.md / ../xxx.md,可带 #锚点)。
22
- * 返回 { path, hash };不是 .md 链接时返回 null(外链、页内锚点走默认规则)。
23
- * @param {string} currentDocPath 当前文档路径,如 "guide/getting-started"
24
- * @param {string} href markdown 里的 href
25
- */
26
- export function resolveDocHref(currentDocPath, href) {
27
- const [raw = "", hash = ""] = String(href).split("#");
28
- if (!/\.md$/.test(raw)) {
29
- return null;
30
- }
31
-
32
- const baseParts = String(currentDocPath).split("/").slice(0, -1);
33
- raw
34
- .replace(/^\.\//, "")
35
- .split("/")
36
- .forEach((seg) => {
37
- if (seg === "..") {
38
- baseParts.pop();
39
- } else if (seg && seg !== ".") {
40
- baseParts.push(seg);
41
- }
42
- });
43
-
44
- const resolved = baseParts.join("/").replace(/\.md$/, "");
45
- return {
46
- path: `/view/docs/${resolved}`,
47
- hash: hash ? `#${hash}` : "",
48
- };
49
- }
50
-
51
- /** HTML 转义(搜索高亮等场景) */
52
- export function escapeHtml(text) {
53
- return String(text)
54
- .replace(/&/g, "&")
55
- .replace(/</g, "&lt;")
56
- .replace(/>/g, "&gt;")
57
- .replace(/"/g, "&quot;");
58
- }
@@ -1,56 +0,0 @@
1
- <template>
2
- <DocLayout :sidebar="false">
3
- <div class="doc-home">
4
- <section class="doc-hero">
5
- <h1 class="doc-hero-name">{{ hero.name }}</h1>
6
- <p class="doc-hero-tagline">{{ hero.tagline }}</p>
7
- <p class="doc-hero-text">{{ hero.text }}</p>
8
- <div class="doc-hero-actions">
9
- <template v-for="action in hero.actions" :key="action.text">
10
- <router-link
11
- v-if="action.path"
12
- :to="action.path"
13
- class="doc-hero-btn"
14
- :class="action.theme || ''"
15
- >
16
- {{ action.text }}
17
- </router-link>
18
- <a
19
- v-else
20
- :href="action.link"
21
- target="_blank"
22
- rel="noopener noreferrer"
23
- class="doc-hero-btn"
24
- :class="action.theme || ''"
25
- >
26
- {{ action.text }}
27
- </a>
28
- </template>
29
- </div>
30
- </section>
31
-
32
- <section class="doc-features">
33
- <div v-for="feature in hero.features" :key="feature.title" class="doc-feature-card">
34
- <div class="doc-feature-icon">{{ feature.icon }}</div>
35
- <h3 class="doc-feature-title">{{ feature.title }}</h3>
36
- <p class="doc-feature-details">{{ feature.details }}</p>
37
- </div>
38
- </section>
39
-
40
- <footer v-if="footerText" class="doc-home-footer">{{ footerText }}</footer>
41
- </div>
42
- </DocLayout>
43
- </template>
44
-
45
- <script setup>
46
- import { computed } from "vue";
47
- import DocLayout from "../components/doc-layout.vue";
48
- import docsConfig from "../docs-config";
49
-
50
- const hero = docsConfig.hero;
51
- const footerText = computed(
52
- () => docsConfig.footer.text || docsConfig.footer.copyright || ""
53
- );
54
-
55
- document.title = docsConfig.site.description;
56
- </script>
@@ -1,147 +0,0 @@
1
- <template>
2
- <DocLayout :sidebar="true">
3
- <template v-if="source !== undefined">
4
- <div class="doc-content-wrap">
5
- <article class="doc-markdown" v-html="rendered.html"></article>
6
-
7
- <div class="doc-pager">
8
- <router-link v-if="prev" :to="prev.path" class="doc-pager-item prev">
9
- <span class="doc-pager-label">上一篇</span>
10
- <span class="doc-pager-title"><icon-left /> {{ prev.text }}</span>
11
- </router-link>
12
- <span v-else class="doc-pager-item placeholder"></span>
13
-
14
- <router-link v-if="next" :to="next.path" class="doc-pager-item next">
15
- <span class="doc-pager-label">下一篇</span>
16
- <span class="doc-pager-title">{{ next.text }} <icon-right /></span>
17
- </router-link>
18
- </div>
19
-
20
- <div v-if="footer.text" class="doc-content-footer">{{ footer.text }}</div>
21
- </div>
22
-
23
- <DocToc class="doc-toc-rail" :items="rendered.toc" :active-id="activeHeadingId" />
24
- </template>
25
-
26
- <div v-else class="doc-content-wrap">
27
- <div class="doc-not-found">
28
- <h1>404</h1>
29
- <p>没有找到文档:<code>/{{ docPath }}</code></p>
30
- <p>
31
- 检查 docs/ 目录下是否存在对应的 markdown 文件,以及
32
- docs-config.js 的侧边栏路径是否正确。
33
- </p>
34
- <router-link to="/view/docs" class="doc-not-found-home">返回首页</router-link>
35
- </div>
36
- </div>
37
- </DocLayout>
38
- </template>
39
-
40
- <script setup>
41
- import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from "vue";
42
- import { useRoute } from "vue-router";
43
- import DocLayout from "../components/doc-layout.vue";
44
- import DocToc from "../components/doc-toc.vue";
45
- import { renderMarkdown } from "../markdown/index";
46
- import { getDocSource } from "../content";
47
- import docsConfig from "../docs-config";
48
- import { pathOfDoc } from "../utils";
49
-
50
- const route = useRoute();
51
-
52
- const docPath = computed(() => pathOfDoc(decodeURIComponent(route.params.path || "")));
53
- const source = computed(() => getDocSource(docPath.value));
54
- const rendered = computed(() =>
55
- source.value === undefined
56
- ? { html: "", toc: [] }
57
- : renderMarkdown(source.value, docPath.value)
58
- );
59
- const footer = docsConfig.footer;
60
-
61
- const activeHeadingId = ref("");
62
-
63
- // ---- 上一篇 / 下一篇:按侧边栏顺序 ----
64
- const flatItems = docsConfig.sidebar.flatMap((group) =>
65
- (group.items || []).map((item) => ({
66
- text: item.text,
67
- path: item.path,
68
- docPath: pathOfDoc(item.path),
69
- }))
70
- );
71
-
72
- const currentIndex = computed(() =>
73
- flatItems.findIndex((item) => item.docPath === docPath.value)
74
- );
75
- const prev = computed(() =>
76
- currentIndex.value > 0 ? flatItems[currentIndex.value - 1] : null
77
- );
78
- const next = computed(() =>
79
- currentIndex.value >= 0 && currentIndex.value < flatItems.length - 1
80
- ? flatItems[currentIndex.value + 1]
81
- : null
82
- );
83
-
84
- // ---- 标题与描述 ----
85
- watch(
86
- docPath,
87
- (path) => {
88
- const sidebarItem = flatItems.find((item) => item.docPath === path);
89
- document.title = sidebarItem
90
- ? `${sidebarItem.text} · ${docsConfig.site.title}`
91
- : docsConfig.site.description;
92
- },
93
- { immediate: true }
94
- );
95
-
96
- // ---- 路由变化:滚动到锚点或页首 ----
97
- watch(
98
- docPath,
99
- () => {
100
- activeHeadingId.value = "";
101
- nextTick(() => {
102
- if (route.hash) {
103
- const el = document.getElementById(decodeURIComponent(route.hash.slice(1)));
104
- if (el) {
105
- el.scrollIntoView();
106
- return;
107
- }
108
- }
109
- window.scrollTo(0, 0);
110
- });
111
- },
112
- { immediate: true }
113
- );
114
-
115
- // ---- TOC 滚动联动(滚动监听当前所在小节)----
116
- let ticking = false;
117
-
118
- const updateActiveHeading = () => {
119
- ticking = false;
120
- const headings = document.querySelectorAll(
121
- ".doc-markdown h2[id], .doc-markdown h3[id]"
122
- );
123
- let current = "";
124
- headings.forEach((el) => {
125
- if (el.getBoundingClientRect().top <= 96) {
126
- current = el.id;
127
- }
128
- });
129
- activeHeadingId.value = current;
130
- };
131
-
132
- const onScroll = () => {
133
- if (!ticking) {
134
- ticking = true;
135
- window.requestAnimationFrame(updateActiveHeading);
136
- }
137
- };
138
-
139
- onMounted(() => {
140
- window.addEventListener("scroll", onScroll, { passive: true });
141
- updateActiveHeading();
142
- });
143
-
144
- onBeforeUnmount(() => {
145
- window.removeEventListener("scroll", onScroll);
146
- });
147
- </script>
@@ -1,15 +0,0 @@
1
- // 业务 Webpack 扩展配置:与框架基础配置 webpack-merge.merge.smart 合并。
2
- //
3
- // 文档站把项目根目录 docs/ 下的 markdown 按原始文本打进产物
4
- // (页面里通过 require.context 读取),因此在这里声明 asset/source 规则。
5
- module.exports = {
6
- module: {
7
- rules: [
8
- {
9
- test: /\.md$/,
10
- // webpack 5 内置资源模块:把文件内容作为字符串导出(default export)
11
- type: "asset/source",
12
- },
13
- ],
14
- },
15
- };
@@ -1,6 +0,0 @@
1
- const { frontendBuild } = require("lumfall");
2
-
3
- // 环境由 _ENV 决定(不是 NODE_ENV):
4
- // _ENV=local 启动 Webpack dev server(HMR 热更新,默认 127.0.0.1:9002)
5
- // _ENV=prod 产物构建到 app/public/dist/prod/,同时产出各页面 .tpl 模板
6
- frontendBuild(process.env._ENV);
@@ -1,2 +0,0 @@
1
- // _ENV=beta 时加载,覆盖 config.default.js 的同名键(浅合并)
2
- module.exports = {};
@@ -1,9 +0,0 @@
1
- // 所有环境共享的基础配置。合并顺序(后者覆盖前者同名键):
2
- // 框架 config.default -> 业务 config.default -> 框架 config.<env> -> 业务 config.<env>
3
- module.exports = {
4
- name: "lumfall-document",
5
- apiBasePath: "/api",
6
-
7
- // 文档站是纯静态站点,默认不暴露 /api 路由;security 的默认值
8
- // (签名关闭 + projectKey 开启)保持框架默认即可,见框架文档「安全策略」
9
- };
@@ -1,2 +0,0 @@
1
- // _ENV=local 时加载,覆盖 config.default.js 的同名键(浅合并)
2
- module.exports = {};
@@ -1,2 +0,0 @@
1
- // _ENV=prod 时加载,覆盖 config.default.js 的同名键(浅合并)
2
- module.exports = {};
@@ -1,41 +0,0 @@
1
- # 示例页面
2
-
3
- 这个页面演示文档站支持的 markdown 能力,确认效果后可以删除本文件
4
- (记得同步删掉 `docs-config.js` 侧栏里的对应条目)。
5
-
6
- ## 提示块
7
-
8
- ::: tip 提示
9
- 这是一个 tip 提示块,用于展示正向提示。
10
- :::
11
-
12
- ::: warning 注意
13
- 这是一个 warning 提示块,用于展示需要注意的内容。
14
- :::
15
-
16
- ## 代码高亮
17
-
18
- 标注语言即可高亮,右上角有一键复制:
19
-
20
- ```js
21
- const { serviceStart } = require("lumfall");
22
-
23
- const app = serviceStart({
24
- name: "my-docs",
25
- homePath: "/view/docs",
26
- });
27
- ```
28
-
29
- ## 表格
30
-
31
- | 能力 | 说明 |
32
- | --- | --- |
33
- | 页面目录(TOC) | 正文 h2/h3 自动生成,滚动联动高亮 |
34
- | 站内搜索 | `Ctrl/Cmd + K` 或 `/` 唤起,标题与正文加权匹配,无需后端 |
35
- | 相对链接 | `[返回介绍](./introduction.md)` 自动转站内路由,支持锚点 |
36
- | 亮暗主题 | 跟随系统 + 手动切换,主题色由 CSS 变量驱动 |
37
-
38
- ## 锚点
39
-
40
- 每个标题悬停会出现 `#` 锚点链接,可以被其他文档引用:
41
- [跳到代码高亮](./example.md#代码高亮)。
@@ -1,31 +0,0 @@
1
- # 开始使用
2
-
3
- 这是由 create-lumfall 生成的**空文档站模板**——站点框架(导航、侧栏、目录、搜索、
4
- 代码高亮、亮暗主题)已全部就绪,你只需要写内容。
5
-
6
- ## 放入你的内容
7
-
8
- 1. 把 `docs/` 目录下的 markdown 换成你的文档(目录结构随意,两级以内体验最佳)
9
- 2. 修改 `app/pages/docs/docs-config.js`:站点名、导航、侧栏、首页 hero、页脚
10
- 3. 本地预览:`pnpm start:dev`,访问 <http://localhost:3000/view/docs>
11
-
12
- ## 侧边栏与文件对应
13
-
14
- 侧边栏条目的 `path` 对应 `docs/` 下的 markdown 文件:
15
-
16
- | 配置项 | 对应文件 |
17
- | --- | --- |
18
- | `doc("guide/introduction")` | `docs/guide/introduction.md` |
19
- | `doc("guide/example")` | `docs/guide/example.md` |
20
-
21
- ## 发布
22
-
23
- ```sh
24
- pnpm build:static # 产出 dist-static/(含 vercel.json)
25
- cd dist-static && vercel --prod
26
- ```
27
-
28
- ## 下一步
29
-
30
- - 看 [示例页面](./example.md) 了解文档站支持的 markdown 能力,确认后可删除
31
- - 完整功能说明见 lumfall-document 项目 README
@@ -1,36 +0,0 @@
1
- {
2
- "name": "lumfall-document",
3
- "version": "1.0.0",
4
- "description": "基于 lumfall 的技术文档站模板:导航、侧栏、目录、搜索、暗色模式,对标 VitePress 的使用体验",
5
- "main": "server.js",
6
- "scripts": {
7
- "dev": "_ENV='local' nodemon --exitcrash server.js",
8
- "beta": "_ENV='beta' nodemon --exitcrash server.js",
9
- "prod": "_ENV='prod' node server.js",
10
- "build:dev": "_ENV='local' node --max_old_space_size=4096 ./build.js",
11
- "build:prod": "_ENV='prod' node ./build.js",
12
- "start:dev": "concurrently --kill-others-on-fail -n webpack,server -c cyan,green \"pnpm build:dev\" \"pnpm dev\"",
13
- "start:prod": "pnpm build:prod && pnpm prod",
14
- "build:static": "node ./scripts/build-static.js"
15
- },
16
- "dependencies": {
17
- "lumfall": "^1.2.1",
18
- "highlight.js": "^11.9.0",
19
- "markdown-it": "^14.0.0",
20
- "markdown-it-container": "^4.0.0"
21
- },
22
- "devDependencies": {
23
- "concurrently": "^10.0.5",
24
- "nodemon": "^3.1.14"
25
- },
26
- "packageManager": "pnpm@10.30.0",
27
- "keywords": [
28
- "lumfall",
29
- "docs",
30
- "documentation",
31
- "vitepress",
32
- "koa",
33
- "vue"
34
- ],
35
- "license": "ISC"
36
- }
@@ -1,129 +0,0 @@
1
- /**
2
- * 静态站点构建脚本:产出可直接托管在 Vercel / Netlify / Nginx 的纯静态目录。
3
- *
4
- * 用法:pnpm build:static (等价于 _ENV=prod node build.js + 组装 dist-static/)
5
- *
6
- * 文档站是纯前端 SPA:markdown 在构建期打进 bundle,运行时不调用任何接口、
7
- * 不读取服务端注入的 window.__LUMFALL__,因此可以脱离 Koa 以静态文件部署。
8
- *
9
- * 产物结构(dist-static/):
10
- * index.html "/" 的重定向页 → /view/docs
11
- * app.html 应用外壳(由 entry.docs.tpl 转换,占位符替换为静态值)
12
- * vercel.json SPA rewrite(/view/docs/* → app.html)+ 长缓存头
13
- * dist/prod/... 构建产物(保持 /dist/prod/ 绝对路径可解析)
14
- */
15
- const { spawnSync } = require("child_process");
16
- const fs = require("fs");
17
- const path = require("path");
18
-
19
- const rootDir = process.cwd();
20
- const distDir = path.join(rootDir, "app", "public", "dist");
21
- const outDir = path.join(rootDir, "dist-static");
22
-
23
- // 1. 复用生产构建:子进程执行 build.js(_ENV=prod),进程退出即构建完成
24
- console.log("[static] running production build (build.js) ...\n");
25
- const build = spawnSync("node", ["build.js"], {
26
- stdio: "inherit",
27
- env: { ...process.env, _ENV: "prod" },
28
- });
29
- if (build.status !== 0) {
30
- process.exitCode = build.status || 1;
31
- return;
32
- }
33
-
34
- // 2. 组装输出目录(保留 .vercel/ 项目链接目录,否则 vercel link 需要重做。
35
- // 注意必须在 rmSync 之前把文件内容读进内存——rm 后同名路径会被重建,按路径保存无效)
36
- const linkDir = path.join(outDir, ".vercel");
37
- const savedLinkFiles = fs.existsSync(linkDir)
38
- ? fs.readdirSync(linkDir).map((name) => ({
39
- name,
40
- content: fs.readFileSync(path.join(linkDir, name)),
41
- }))
42
- : null;
43
- fs.rmSync(outDir, { recursive: true, force: true });
44
- fs.mkdirSync(path.join(outDir, "dist"), { recursive: true });
45
- if (savedLinkFiles) {
46
- fs.mkdirSync(linkDir, { recursive: true });
47
- for (const { name, content } of savedLinkFiles) {
48
- fs.writeFileSync(path.join(linkDir, name), content);
49
- }
50
- }
51
- fs.cpSync(path.join(distDir, "prod"), path.join(outDir, "dist", "prod"), {
52
- recursive: true,
53
- });
54
-
55
- // 3. entry.docs.tpl → app.html:把服务端渲染的 __LUMFALL__ 占位换成静态值。
56
- // 文档站前端不读这些值,保留空对象是为了模板使用方将来加 curl 调接口时行为一致。
57
- // 同时剥掉 {# ... #} nunjucks 注释——Koa 渲染时会剥离,静态导出不剥会变成
58
- // 页面顶部的可见文本(浏览器会把 head 里的游离文本挪进 body 渲染)。
59
- // lumfall >= 1.2.0 起页面模板按模式分目录,prod 构建产物在 dist/prod/ 下。
60
- const tplPath = path.join(distDir, "prod", "entry.docs.tpl");
61
- const tpl = fs
62
- .readFileSync(tplPath, "utf8")
63
- .replace(/\{#[\s\S]*?#\}/g, "");
64
- const marker = "window.__LUMFALL__";
65
- const start = tpl.indexOf(marker);
66
- if (start < 0) {
67
- throw new Error(`[static] ${marker} not found in entry.docs.tpl`);
68
- }
69
- const scriptEnd = tpl.indexOf("</script>", start);
70
- if (scriptEnd < 0) {
71
- throw new Error("[static] </script> not found after __LUMFALL__ block");
72
- }
73
-
74
- const staticGlobals = `window.__LUMFALL__ = {
75
- name: "lumfall-document",
76
- env: "static",
77
- options: {},
78
- projectKey: "",
79
- };`;
80
- const appHtml = tpl.slice(0, start) + staticGlobals + tpl.slice(scriptEnd);
81
- fs.writeFileSync(path.join(outDir, "app.html"), appHtml);
82
-
83
- // 4. "/" 重定向页:静态托管下没有 302 兜底路由,用 meta refresh + JS 跳到 /view/docs
84
- fs.writeFileSync(
85
- path.join(outDir, "index.html"),
86
- `<!DOCTYPE html>
87
- <html lang="zh-CN">
88
- <head>
89
- <meta charset="UTF-8" />
90
- <title>Lumfall 文档</title>
91
- <meta http-equiv="refresh" content="0; url=/view/docs" />
92
- <script>location.replace("/view/docs");</script>
93
- </head>
94
- <body></body>
95
- </html>
96
- `
97
- );
98
-
99
- // 5. SPA 路由规则:/view/docs/* 深链接重写到 app.html(文件优先,静态资源不受影响)
100
- fs.writeFileSync(
101
- path.join(outDir, "vercel.json"),
102
- JSON.stringify(
103
- {
104
- rewrites: [
105
- { source: "/view/docs", destination: "/app.html" },
106
- { source: "/view/docs/:path*", destination: "/app.html" },
107
- ],
108
- headers: [
109
- {
110
- // 构建产物带内容 hash,可永久缓存;app.html 不缓存保证发版即生效
111
- source: "/dist/prod/(.*)",
112
- headers: [
113
- { key: "Cache-Control", value: "public, max-age=31536000, immutable" },
114
- ],
115
- },
116
- {
117
- source: "/app.html",
118
- headers: [{ key: "Cache-Control", value: "no-cache" }],
119
- },
120
- ],
121
- },
122
- null,
123
- 2
124
- )
125
- );
126
-
127
- console.log(`\n[static] done → ${path.relative(rootDir, outDir)}`);
128
- console.log("[static] deploy: cd dist-static && vercel --prod");
129
- console.log("[static] (或 Vercel 项目设置 Build Command: pnpm build:static, Output Directory: dist-static)");