fossbook 0.2.14 → 0.2.16
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 +755 -576
- package/bin/fossbook.js +73 -59
- package/lib/deploy.js +2 -1
- package/lib/index.js +58 -29
- package/lib/init.js +103 -19
- package/lib/mod/config.js +43 -13
- package/lib/mod/github_cache.js +130 -0
- package/lib/mod/image.js +411 -0
- package/lib/mod/marked.js +65 -25
- package/lib/mod/page.js +78 -29
- package/lib/posts.js +1 -0
- package/package.json +49 -48
package/bin/fossbook.js
CHANGED
|
@@ -24,6 +24,7 @@ Options:
|
|
|
24
24
|
--clean Remove output directory before build (default: true)
|
|
25
25
|
--github (init) Also create a GitHub repo and push
|
|
26
26
|
--lang (new) Comma-separated translation languages, e.g. ko,ja
|
|
27
|
+
--include-drafts (build, serve) Include posts with draft: true
|
|
27
28
|
-m, --message (deploy) Custom commit message
|
|
28
29
|
--no-wait (deploy) Push without waiting for CI status
|
|
29
30
|
--draft (deploy) Commit locally without pushing
|
|
@@ -59,72 +60,85 @@ if (hasFlag("-h", "--help") || !command) {
|
|
|
59
60
|
|
|
60
61
|
const configPath = getOption("-c", "--config");
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const config = loadConfig(configPath);
|
|
75
|
-
const outputOverride = getOption("-o", "--output");
|
|
76
|
-
if (outputOverride) config.dev.outdir = outputOverride;
|
|
77
|
-
const port = getOption("-p", "--port") || 3000;
|
|
63
|
+
async function run() {
|
|
64
|
+
switch (command) {
|
|
65
|
+
case "build": {
|
|
66
|
+
const config = loadConfig(configPath);
|
|
67
|
+
const outputOverride = getOption("-o", "--output");
|
|
68
|
+
if (outputOverride) config.dev.outdir = outputOverride;
|
|
69
|
+
config.includeDrafts = hasFlag("--include-drafts");
|
|
70
|
+
|
|
71
|
+
const { build } = require("../lib/index");
|
|
72
|
+
await build(config);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
78
75
|
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
case "serve": {
|
|
77
|
+
const config = loadConfig(configPath);
|
|
78
|
+
const outputOverride = getOption("-o", "--output");
|
|
79
|
+
if (outputOverride) config.dev.outdir = outputOverride;
|
|
80
|
+
config.includeDrafts = hasFlag("--include-drafts");
|
|
81
|
+
const port = getOption("-p", "--port") || 3000;
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
83
|
+
const { build } = require("../lib/index");
|
|
84
|
+
await build(config);
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.error('Error: Please provide a post title. Usage: fossbook new "My Post Title"');
|
|
91
|
-
process.exit(1);
|
|
86
|
+
const { serve } = require("../lib/server");
|
|
87
|
+
serve(config, Number(port));
|
|
88
|
+
break;
|
|
92
89
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
|
|
91
|
+
case "new": {
|
|
92
|
+
const title = args[1];
|
|
93
|
+
if (!title) {
|
|
94
|
+
console.error(
|
|
95
|
+
'Error: Please provide a post title. Usage: fossbook new "My Post Title"',
|
|
96
|
+
);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
if (hasFlag("--lang") && !getOption("--lang")) {
|
|
100
|
+
console.error(
|
|
101
|
+
"Error: Please provide at least one language after --lang",
|
|
102
|
+
);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const config = loadConfig(configPath);
|
|
106
|
+
const { createPost } = require("../lib/new");
|
|
107
|
+
try {
|
|
108
|
+
createPost(config, title, getOption("--lang"));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(`Error: ${error.message}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
96
114
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
process.exit(1);
|
|
115
|
+
|
|
116
|
+
case "init": {
|
|
117
|
+
const { initProject } = require("../lib/init");
|
|
118
|
+
const projectName = !args[1] || args[1].startsWith("--") ? null : args[1];
|
|
119
|
+
initProject({ github: hasFlag("--github"), name: projectName });
|
|
120
|
+
break;
|
|
104
121
|
}
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
122
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
case "deploy": {
|
|
124
|
+
const config = loadConfig(configPath);
|
|
125
|
+
const { deploy } = require("../lib/deploy");
|
|
126
|
+
await deploy(config, {
|
|
127
|
+
message: getOption("-m", "--message"),
|
|
128
|
+
noWait: hasFlag("--no-wait"),
|
|
129
|
+
draft: hasFlag("--draft"),
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
114
133
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
message: getOption("-m", "--message"),
|
|
120
|
-
noWait: hasFlag("--no-wait"),
|
|
121
|
-
draft: hasFlag("--draft"),
|
|
122
|
-
});
|
|
123
|
-
break;
|
|
134
|
+
default:
|
|
135
|
+
console.error(`Unknown command: ${command}`);
|
|
136
|
+
printHelp();
|
|
137
|
+
process.exit(1);
|
|
124
138
|
}
|
|
125
|
-
|
|
126
|
-
default:
|
|
127
|
-
console.error(`Unknown command: ${command}`);
|
|
128
|
-
printHelp();
|
|
129
|
-
process.exit(1);
|
|
130
139
|
}
|
|
140
|
+
|
|
141
|
+
run().catch((error) => {
|
|
142
|
+
console.error(error);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
});
|
package/lib/deploy.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -6,8 +6,13 @@ const HomePagination = require("./home");
|
|
|
6
6
|
const AllPostsPage = require("./all_posts");
|
|
7
7
|
const Posts = require("./posts");
|
|
8
8
|
const TagPages = require("./tag");
|
|
9
|
+
const {
|
|
10
|
+
beginImageBuild,
|
|
11
|
+
finishImageBuild,
|
|
12
|
+
publishImage,
|
|
13
|
+
} = require("./mod/image");
|
|
9
14
|
|
|
10
|
-
function copyDirectoryRecursive(src, dest) {
|
|
15
|
+
async function copyDirectoryRecursive(src, dest, config) {
|
|
11
16
|
fs.mkdirSync(dest, { recursive: true });
|
|
12
17
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
13
18
|
|
|
@@ -16,9 +21,11 @@ function copyDirectoryRecursive(src, dest) {
|
|
|
16
21
|
const destPath = path.join(dest, entry.name);
|
|
17
22
|
|
|
18
23
|
if (entry.isDirectory()) {
|
|
19
|
-
copyDirectoryRecursive(srcPath, destPath);
|
|
24
|
+
await copyDirectoryRecursive(srcPath, destPath, config);
|
|
25
|
+
} else if (path.extname(entry.name).toLowerCase() === ".png") {
|
|
26
|
+
await publishImage(srcPath, destPath, config, undefined, false);
|
|
20
27
|
} else {
|
|
21
|
-
fs.
|
|
28
|
+
await fs.promises.copyFile(srcPath, destPath);
|
|
22
29
|
}
|
|
23
30
|
}
|
|
24
31
|
}
|
|
@@ -35,10 +42,16 @@ function createPostTranslationIndex(languageConfigs) {
|
|
|
35
42
|
|
|
36
43
|
const translations = [];
|
|
37
44
|
for (const config of languageConfigs) {
|
|
38
|
-
const sourceFile =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
const sourceFile =
|
|
46
|
+
config.language === config.defaultLanguage
|
|
47
|
+
? "index.md"
|
|
48
|
+
: `index.${config.language}.md`;
|
|
49
|
+
const sourcePath = path.join(postsDir, entry.name, sourceFile);
|
|
50
|
+
if (!fs.existsSync(sourcePath)) continue;
|
|
51
|
+
|
|
52
|
+
const post = new Page(config);
|
|
53
|
+
post.readSource(sourcePath, entry.name);
|
|
54
|
+
if (post.draft && !config.includeDrafts) continue;
|
|
42
55
|
|
|
43
56
|
const prefix = config.postsPath ? `${config.postsPath}/` : "";
|
|
44
57
|
translations.push({
|
|
@@ -60,18 +73,21 @@ function createAboutTranslations(languageConfigs) {
|
|
|
60
73
|
|
|
61
74
|
const contentDir = languageConfigs[0].dev.content;
|
|
62
75
|
return languageConfigs.flatMap((config) => {
|
|
63
|
-
const sourceFile =
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
const sourceFile =
|
|
77
|
+
config.language === config.defaultLanguage
|
|
78
|
+
? "about.md"
|
|
79
|
+
: `about.${config.language}.md`;
|
|
66
80
|
if (!fs.existsSync(path.join(contentDir, sourceFile))) return [];
|
|
67
81
|
|
|
68
|
-
return [
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
language: config.language,
|
|
85
|
+
languageName: config.languageName,
|
|
86
|
+
path: `${config.basePath || "/"}about/`,
|
|
87
|
+
url: `${config.blogsite}/about/`,
|
|
88
|
+
isDefault: config.language === config.defaultLanguage,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
75
91
|
});
|
|
76
92
|
}
|
|
77
93
|
|
|
@@ -105,16 +121,16 @@ function createTagListTranslations(languageConfigs) {
|
|
|
105
121
|
}));
|
|
106
122
|
}
|
|
107
123
|
|
|
108
|
-
function buildLanguage(config) {
|
|
124
|
+
async function buildLanguage(config) {
|
|
109
125
|
fs.mkdirSync(config.dev.outdir, { recursive: true });
|
|
110
126
|
|
|
111
127
|
// Create post pages in output directory
|
|
112
128
|
const posts = new Posts(config);
|
|
113
129
|
const postObjects = posts.createPostObjects();
|
|
114
130
|
|
|
115
|
-
|
|
116
|
-
post.generateContent("post.html");
|
|
117
|
-
}
|
|
131
|
+
for (const post of postObjects) {
|
|
132
|
+
await post.generateContent("post.html");
|
|
133
|
+
}
|
|
118
134
|
|
|
119
135
|
// Create home page and pagination in output/page directory
|
|
120
136
|
const homePagination = new HomePagination(config);
|
|
@@ -132,37 +148,47 @@ function buildLanguage(config) {
|
|
|
132
148
|
tagPages.generateContent(postObjects);
|
|
133
149
|
|
|
134
150
|
// Create about page in output/about directory
|
|
135
|
-
const aboutFile =
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
const aboutFile =
|
|
152
|
+
config.language === config.defaultLanguage
|
|
153
|
+
? "about.md"
|
|
154
|
+
: `about.${config.language}.md`;
|
|
138
155
|
const aboutPath = path.resolve(config.dev.content, aboutFile);
|
|
139
156
|
if (fs.existsSync(aboutPath)) {
|
|
140
157
|
const aboutPage = new Page(config);
|
|
141
158
|
aboutPage.readSource(aboutPath, "about");
|
|
142
159
|
aboutPage.translations = config.aboutTranslations || [];
|
|
143
|
-
aboutPage.generateContent("page.html", "about");
|
|
160
|
+
await aboutPage.generateContent("page.html", "about");
|
|
144
161
|
}
|
|
145
162
|
|
|
146
163
|
// Copy static directory to output directory
|
|
147
164
|
const staticImagesDir = path.join(config.dev.staticDir, "images");
|
|
148
165
|
if (fs.existsSync(staticImagesDir)) {
|
|
149
|
-
copyDirectoryRecursive(
|
|
166
|
+
await copyDirectoryRecursive(
|
|
167
|
+
staticImagesDir,
|
|
168
|
+
path.join(config.dev.outdir, "images"),
|
|
169
|
+
config,
|
|
170
|
+
);
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
// Copy theme assets to output directory
|
|
153
174
|
const themeAssetsDir = path.join(config.themePath, "assets");
|
|
154
175
|
if (fs.existsSync(themeAssetsDir)) {
|
|
155
|
-
copyDirectoryRecursive(themeAssetsDir, config.dev.outdir);
|
|
176
|
+
await copyDirectoryRecursive(themeAssetsDir, config.dev.outdir, config);
|
|
156
177
|
}
|
|
157
178
|
}
|
|
158
179
|
|
|
159
|
-
function build(config) {
|
|
180
|
+
async function build(config) {
|
|
181
|
+
beginImageBuild();
|
|
182
|
+
|
|
160
183
|
// Remove the output directory
|
|
161
184
|
if (fs.existsSync(config.dev.outdir))
|
|
162
185
|
fs.rmSync(config.dev.outdir, { recursive: true });
|
|
163
186
|
fs.mkdirSync(config.dev.outdir, { recursive: true });
|
|
164
187
|
|
|
165
188
|
const languageConfigs = config.languageConfigs || [config];
|
|
189
|
+
languageConfigs.forEach((languageConfig) => {
|
|
190
|
+
languageConfig.includeDrafts = config.includeDrafts === true;
|
|
191
|
+
});
|
|
166
192
|
const postTranslationIndex = createPostTranslationIndex(languageConfigs);
|
|
167
193
|
const aboutTranslations = createAboutTranslations(languageConfigs);
|
|
168
194
|
const homeTranslations = createHomeTranslations(languageConfigs);
|
|
@@ -175,7 +201,10 @@ function build(config) {
|
|
|
175
201
|
languageConfig.allPostsTranslations = allPostsTranslations;
|
|
176
202
|
languageConfig.tagListTranslations = tagListTranslations;
|
|
177
203
|
});
|
|
178
|
-
languageConfigs
|
|
204
|
+
for (const languageConfig of languageConfigs) {
|
|
205
|
+
await buildLanguage(languageConfig);
|
|
206
|
+
}
|
|
207
|
+
await finishImageBuild(config);
|
|
179
208
|
|
|
180
209
|
// Create CNAME file for GitHub Pages
|
|
181
210
|
if (config.githubCNAME)
|
package/lib/init.js
CHANGED
|
@@ -2,6 +2,12 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { execSync } = require("child_process");
|
|
4
4
|
const readline = require("readline");
|
|
5
|
+
const { CACHE_SCHEMA_VERSION } = require("./mod/image");
|
|
6
|
+
|
|
7
|
+
const IMAGE_CACHE_SCHEMA = `v${CACHE_SCHEMA_VERSION}`;
|
|
8
|
+
const IMAGE_CACHE_IGNORE_RULE = ".fossbook-cache/";
|
|
9
|
+
const IMAGE_CACHE_SAVE_CONDITION =
|
|
10
|
+
"steps.image-cache-restore.outputs.cache-hit != 'true' && steps.image-cache-status.outputs.new-entries == 'true'";
|
|
5
11
|
|
|
6
12
|
const DEPLOY_WORKFLOW = `name: Deploy to GitHub Pages
|
|
7
13
|
|
|
@@ -36,9 +42,41 @@ jobs:
|
|
|
36
42
|
- name: Install dependencies
|
|
37
43
|
run: npm ci
|
|
38
44
|
|
|
45
|
+
- name: Resolve Fossbook image cache
|
|
46
|
+
id: image-cache-config
|
|
47
|
+
run: node -e "require('fossbook/lib/mod/github_cache').writeGitHubCacheOutputs()"
|
|
48
|
+
|
|
49
|
+
- name: Restore Fossbook image cache
|
|
50
|
+
id: image-cache-restore
|
|
51
|
+
uses: actions/cache/restore@v4
|
|
52
|
+
continue-on-error: true
|
|
53
|
+
with:
|
|
54
|
+
path: \${{ steps.image-cache-config.outputs.cache-path }}/images
|
|
55
|
+
key: \${{ runner.os }}-\${{ runner.arch }}-fossbook-images-${IMAGE_CACHE_SCHEMA}-\${{ steps.image-cache-config.outputs.input-hash }}-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
56
|
+
restore-keys: |
|
|
57
|
+
\${{ runner.os }}-\${{ runner.arch }}-fossbook-images-${IMAGE_CACHE_SCHEMA}-\${{ steps.image-cache-config.outputs.input-hash }}-
|
|
58
|
+
\${{ runner.os }}-\${{ runner.arch }}-fossbook-images-${IMAGE_CACHE_SCHEMA}-
|
|
59
|
+
|
|
39
60
|
- name: Build site
|
|
40
61
|
run: npx fossbook build
|
|
41
62
|
|
|
63
|
+
- name: Read Fossbook image cache status
|
|
64
|
+
id: image-cache-status
|
|
65
|
+
continue-on-error: true
|
|
66
|
+
shell: bash
|
|
67
|
+
env:
|
|
68
|
+
FOSSBOOK_CACHE_STATUS: \${{ steps.image-cache-config.outputs.status-path }}
|
|
69
|
+
run: |
|
|
70
|
+
node -e "const fs=require('fs');let writes=0;try{writes=JSON.parse(fs.readFileSync(process.env.FOSSBOOK_CACHE_STATUS,'utf8')).writes||0}catch(error){console.warn(error.message)}fs.appendFileSync(process.env.GITHUB_OUTPUT, 'new-entries='+(writes>0)+'\\n')"
|
|
71
|
+
|
|
72
|
+
- name: Save Fossbook image cache
|
|
73
|
+
if: \${{ ${IMAGE_CACHE_SAVE_CONDITION} }}
|
|
74
|
+
uses: actions/cache/save@v4
|
|
75
|
+
continue-on-error: true
|
|
76
|
+
with:
|
|
77
|
+
path: \${{ steps.image-cache-config.outputs.cache-path }}/images
|
|
78
|
+
key: \${{ runner.os }}-\${{ runner.arch }}-fossbook-images-${IMAGE_CACHE_SCHEMA}-\${{ steps.image-cache-config.outputs.input-hash }}-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
79
|
+
|
|
42
80
|
- name: Setup Pages
|
|
43
81
|
uses: actions/configure-pages@v4
|
|
44
82
|
with:
|
|
@@ -70,10 +108,7 @@ function initProject(options = {}) {
|
|
|
70
108
|
}
|
|
71
109
|
|
|
72
110
|
// Create directories
|
|
73
|
-
const dirs = [
|
|
74
|
-
"content/posts",
|
|
75
|
-
"static/images",
|
|
76
|
-
];
|
|
111
|
+
const dirs = ["content/posts", "static/images"];
|
|
77
112
|
|
|
78
113
|
dirs.forEach((dir) => {
|
|
79
114
|
const dirPath = path.join(cwd, dir);
|
|
@@ -95,8 +130,13 @@ function initProject(options = {}) {
|
|
|
95
130
|
// Create .gitignore
|
|
96
131
|
const gitignorePath = path.join(cwd, ".gitignore");
|
|
97
132
|
if (!fs.existsSync(gitignorePath)) {
|
|
98
|
-
fs.writeFileSync(
|
|
133
|
+
fs.writeFileSync(
|
|
134
|
+
gitignorePath,
|
|
135
|
+
`node_modules/\npublic/\n${IMAGE_CACHE_IGNORE_RULE}\n`,
|
|
136
|
+
);
|
|
99
137
|
console.log("Created: .gitignore");
|
|
138
|
+
} else if (ensureGitignoreRule(gitignorePath, IMAGE_CACHE_IGNORE_RULE)) {
|
|
139
|
+
console.log(`Updated: .gitignore (${IMAGE_CACHE_IGNORE_RULE})`);
|
|
100
140
|
}
|
|
101
141
|
|
|
102
142
|
// Create fossbook.config.js
|
|
@@ -119,6 +159,14 @@ function initProject(options = {}) {
|
|
|
119
159
|
image: "",
|
|
120
160
|
theme: "archie",
|
|
121
161
|
|
|
162
|
+
// PNG publishing optimization
|
|
163
|
+
imageOptimization: true,
|
|
164
|
+
imageMaxWidth: 1400,
|
|
165
|
+
imageMaxWidthOverride: 1200,
|
|
166
|
+
imageWebP: true,
|
|
167
|
+
imageResponsiveWidths: [600, 800, 1000],
|
|
168
|
+
cacheDir: "./.fossbook-cache",
|
|
169
|
+
|
|
122
170
|
// Languages (optional)
|
|
123
171
|
defaultLanguage: "en",
|
|
124
172
|
defaultLanguageInSubdir: false,
|
|
@@ -192,7 +240,9 @@ Welcome to my blog!
|
|
|
192
240
|
execSync("npm install", { cwd, stdio: "inherit" });
|
|
193
241
|
console.log("Created: package-lock.json");
|
|
194
242
|
} catch {
|
|
195
|
-
console.warn(
|
|
243
|
+
console.warn(
|
|
244
|
+
"Warning: npm install failed. You may need to run it manually.",
|
|
245
|
+
);
|
|
196
246
|
}
|
|
197
247
|
}
|
|
198
248
|
|
|
@@ -203,13 +253,13 @@ Welcome to my blog!
|
|
|
203
253
|
|
|
204
254
|
console.log("\nSite initialized! Next steps:");
|
|
205
255
|
if (!options.github) {
|
|
206
|
-
console.log(
|
|
256
|
+
console.log(" 1. Edit fossbook.config.js with your site settings");
|
|
207
257
|
console.log(' 2. Run: fossbook new "My First Post"');
|
|
208
258
|
console.log(" 3. Edit your post in content/posts/");
|
|
209
259
|
console.log(" 4. Run: fossbook build");
|
|
210
260
|
console.log(" 5. Run: fossbook serve");
|
|
211
261
|
} else {
|
|
212
|
-
console.log(
|
|
262
|
+
console.log(" 1. Edit fossbook.config.js with your site settings");
|
|
213
263
|
console.log(' 2. Run: fossbook new "My First Post"');
|
|
214
264
|
console.log(" 3. Edit your post in content/posts/");
|
|
215
265
|
console.log(" 4. Run: fossbook deploy");
|
|
@@ -223,10 +273,10 @@ function initGitHub(cwd) {
|
|
|
223
273
|
} catch {
|
|
224
274
|
console.error(
|
|
225
275
|
"Error: GitHub CLI (gh) is not installed.\n" +
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
276
|
+
"Install it: https://cli.github.com/\n" +
|
|
277
|
+
" Linux: sudo apt install gh\n" +
|
|
278
|
+
" macOS: brew install gh\n" +
|
|
279
|
+
" Windows: winget install GitHub.cli",
|
|
230
280
|
);
|
|
231
281
|
process.exit(1);
|
|
232
282
|
}
|
|
@@ -256,7 +306,9 @@ function initGitHub(cwd) {
|
|
|
256
306
|
});
|
|
257
307
|
} catch {
|
|
258
308
|
// Repo may already exist — ask user what to do
|
|
259
|
-
console.warn(
|
|
309
|
+
console.warn(
|
|
310
|
+
`\nA GitHub repository named "${repoName}" already exists on your account.`,
|
|
311
|
+
);
|
|
260
312
|
const answer = promptSync("Push to the existing repo? (y/n): ");
|
|
261
313
|
if (answer.toLowerCase() !== "y") {
|
|
262
314
|
console.log("Cancelled. Your local site files are still available.");
|
|
@@ -275,11 +327,18 @@ function initGitHub(cwd) {
|
|
|
275
327
|
stdio: ["pipe", "pipe", "ignore"],
|
|
276
328
|
}).trim();
|
|
277
329
|
const remoteUrl = `git@github.com:${ghUser}/${repoName}.git`;
|
|
278
|
-
execSync(`git remote add origin ${remoteUrl}`, {
|
|
330
|
+
execSync(`git remote add origin ${remoteUrl}`, {
|
|
331
|
+
cwd,
|
|
332
|
+
stdio: "inherit",
|
|
333
|
+
});
|
|
279
334
|
console.log(`Added remote: ${remoteUrl}`);
|
|
280
335
|
} catch {
|
|
281
|
-
console.warn(
|
|
282
|
-
|
|
336
|
+
console.warn(
|
|
337
|
+
"Warning: Could not add git remote. Add it manually with:",
|
|
338
|
+
);
|
|
339
|
+
console.warn(
|
|
340
|
+
` git remote add origin git@github.com:<user>/${repoName}.git`,
|
|
341
|
+
);
|
|
283
342
|
}
|
|
284
343
|
}
|
|
285
344
|
}
|
|
@@ -288,14 +347,19 @@ function initGitHub(cwd) {
|
|
|
288
347
|
console.log("Committing and pushing to GitHub...");
|
|
289
348
|
execSync("git add -A", { cwd, stdio: "inherit" });
|
|
290
349
|
try {
|
|
291
|
-
execSync('git commit -m "Initial fossbook site"', {
|
|
350
|
+
execSync('git commit -m "Initial fossbook site"', {
|
|
351
|
+
cwd,
|
|
352
|
+
stdio: "inherit",
|
|
353
|
+
});
|
|
292
354
|
} catch {
|
|
293
355
|
// Nothing to commit
|
|
294
356
|
}
|
|
295
357
|
try {
|
|
296
358
|
execSync("git push -u origin main", { cwd, stdio: "inherit" });
|
|
297
359
|
} catch (e) {
|
|
298
|
-
console.warn(
|
|
360
|
+
console.warn(
|
|
361
|
+
"Warning: Could not push to origin. You may need to push manually.",
|
|
362
|
+
);
|
|
299
363
|
}
|
|
300
364
|
|
|
301
365
|
console.log("\nGitHub repository created and pushed!");
|
|
@@ -323,4 +387,24 @@ function promptSync(question) {
|
|
|
323
387
|
return str.trim();
|
|
324
388
|
}
|
|
325
389
|
|
|
326
|
-
|
|
390
|
+
function shouldSaveImageCache(cacheHit, newEntries) {
|
|
391
|
+
return cacheHit !== "true" && newEntries === true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function ensureGitignoreRule(gitignorePath, rule) {
|
|
395
|
+
const contents = fs.readFileSync(gitignorePath, "utf8");
|
|
396
|
+
const rules = contents.split(/\r?\n/).map((line) => line.trim());
|
|
397
|
+
if (rules.includes(rule)) return false;
|
|
398
|
+
|
|
399
|
+
const separator = contents.length > 0 && !contents.endsWith("\n") ? "\n" : "";
|
|
400
|
+
fs.appendFileSync(gitignorePath, `${separator}${rule}\n`);
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = {
|
|
405
|
+
DEPLOY_WORKFLOW,
|
|
406
|
+
IMAGE_CACHE_SCHEMA,
|
|
407
|
+
ensureGitignoreRule,
|
|
408
|
+
initProject,
|
|
409
|
+
shouldSaveImageCache,
|
|
410
|
+
};
|
package/lib/mod/config.js
CHANGED
|
@@ -19,9 +19,15 @@ const defaults = {
|
|
|
19
19
|
defaultLanguage: "en",
|
|
20
20
|
defaultLanguageInSubdir: false,
|
|
21
21
|
languages: null,
|
|
22
|
-
basePath: "",
|
|
23
|
-
postsPath: "posts",
|
|
24
|
-
comments: null,
|
|
22
|
+
basePath: "", // auto-detected for GitHub Pages project sites, e.g. "/repo-name/"
|
|
23
|
+
postsPath: "posts", // URL prefix for posts: "posts" serves articles at /posts/<slug>/; set "" for root
|
|
24
|
+
comments: null, // { provider: "utterances", repo: "user/repo", issueTerm: "pathname", theme: "github-light" }
|
|
25
|
+
imageOptimization: true,
|
|
26
|
+
imageMaxWidth: 1400,
|
|
27
|
+
imageMaxWidthOverride: 1200,
|
|
28
|
+
imageWebP: true,
|
|
29
|
+
imageResponsiveWidths: [600, 800, 1000],
|
|
30
|
+
cacheDir: "./.fossbook-cache",
|
|
25
31
|
content: "./content",
|
|
26
32
|
postsDir: "./content/posts",
|
|
27
33
|
outputDir: "./public",
|
|
@@ -44,7 +50,9 @@ function resolveThemePath(config) {
|
|
|
44
50
|
if (fs.existsSync(builtinThemePath)) {
|
|
45
51
|
return builtinThemePath;
|
|
46
52
|
}
|
|
47
|
-
console.error(
|
|
53
|
+
console.error(
|
|
54
|
+
`Theme "${themeName}" not found in ${userThemePath} or ${builtinThemePath}`,
|
|
55
|
+
);
|
|
48
56
|
process.exit(1);
|
|
49
57
|
}
|
|
50
58
|
|
|
@@ -76,6 +84,7 @@ function loadConfig(configPath) {
|
|
|
76
84
|
content: path.resolve(merged.content),
|
|
77
85
|
about: path.resolve(merged.content, "about.md"),
|
|
78
86
|
outdir: path.resolve(merged.outputDir),
|
|
87
|
+
cacheDir: path.resolve(merged.cacheDir),
|
|
79
88
|
themePath: path.dirname(themePath), // e.g. /path/to/themes
|
|
80
89
|
staticDir: path.resolve(merged.staticDir),
|
|
81
90
|
};
|
|
@@ -105,23 +114,36 @@ function loadConfig(configPath) {
|
|
|
105
114
|
|
|
106
115
|
function createLanguageConfigs(config, configPath) {
|
|
107
116
|
const configuredLanguages = config.languages;
|
|
108
|
-
const hasMultipleLanguages =
|
|
117
|
+
const hasMultipleLanguages =
|
|
118
|
+
configuredLanguages !== null && configuredLanguages !== undefined;
|
|
109
119
|
const languages = hasMultipleLanguages
|
|
110
120
|
? configuredLanguages
|
|
111
121
|
: { [config.defaultLanguage]: {} };
|
|
112
122
|
|
|
113
|
-
if (
|
|
123
|
+
if (
|
|
124
|
+
typeof languages !== "object" ||
|
|
125
|
+
Array.isArray(languages) ||
|
|
126
|
+
Object.keys(languages).length === 0
|
|
127
|
+
) {
|
|
114
128
|
throw new Error("languages must be a non-empty object");
|
|
115
129
|
}
|
|
116
|
-
if (
|
|
117
|
-
|
|
130
|
+
if (
|
|
131
|
+
!Object.prototype.hasOwnProperty.call(languages, config.defaultLanguage)
|
|
132
|
+
) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`defaultLanguage "${config.defaultLanguage}" is not defined in languages`,
|
|
135
|
+
);
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
return Object.entries(languages).map(([language, overrides]) => {
|
|
121
139
|
if (!/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/.test(language)) {
|
|
122
140
|
throw new Error(`Invalid language key "${language}"`);
|
|
123
141
|
}
|
|
124
|
-
if (
|
|
142
|
+
if (
|
|
143
|
+
!overrides ||
|
|
144
|
+
typeof overrides !== "object" ||
|
|
145
|
+
Array.isArray(overrides)
|
|
146
|
+
) {
|
|
125
147
|
throw new Error(`languages.${language} must be an object`);
|
|
126
148
|
}
|
|
127
149
|
|
|
@@ -129,7 +151,8 @@ function createLanguageConfigs(config, configPath) {
|
|
|
129
151
|
? loadLanguageConfig(configPath, language)
|
|
130
152
|
: {};
|
|
131
153
|
|
|
132
|
-
const useSubdirectory =
|
|
154
|
+
const useSubdirectory =
|
|
155
|
+
config.defaultLanguageInSubdir || language !== config.defaultLanguage;
|
|
133
156
|
const languagePrefix = useSubdirectory ? language : "";
|
|
134
157
|
const basePath = appendPathSegment(config.basePath, languagePrefix);
|
|
135
158
|
const blogsite = appendUrlSegment(config.blogsite, languagePrefix);
|
|
@@ -141,7 +164,8 @@ function createLanguageConfigs(config, configPath) {
|
|
|
141
164
|
defaultLanguage: config.defaultLanguage,
|
|
142
165
|
defaultLanguageInSubdir: config.defaultLanguageInSubdir,
|
|
143
166
|
language,
|
|
144
|
-
languageName:
|
|
167
|
+
languageName:
|
|
168
|
+
fileOverrides.languageName || overrides.languageName || language,
|
|
145
169
|
locale: fileOverrides.locale || overrides.locale || language,
|
|
146
170
|
languagePrefix,
|
|
147
171
|
basePath,
|
|
@@ -166,8 +190,14 @@ function loadLanguageConfig(configPath, language) {
|
|
|
166
190
|
if (!fs.existsSync(languageConfigPath)) return {};
|
|
167
191
|
|
|
168
192
|
const languageConfig = require(languageConfigPath);
|
|
169
|
-
if (
|
|
170
|
-
|
|
193
|
+
if (
|
|
194
|
+
!languageConfig ||
|
|
195
|
+
typeof languageConfig !== "object" ||
|
|
196
|
+
Array.isArray(languageConfig)
|
|
197
|
+
) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`${path.basename(languageConfigPath)} must export an object`,
|
|
200
|
+
);
|
|
171
201
|
}
|
|
172
202
|
return languageConfig;
|
|
173
203
|
}
|