fossbook 0.2.15 → 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 -588
- package/bin/fossbook.js +72 -61
- package/lib/deploy.js +1 -1
- package/lib/index.js +49 -28
- 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 +77 -29
- package/package.json +49 -48
package/bin/fossbook.js
CHANGED
|
@@ -60,74 +60,85 @@ if (hasFlag("-h", "--help") || !command) {
|
|
|
60
60
|
|
|
61
61
|
const configPath = getOption("-c", "--config");
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
case "serve": {
|
|
76
|
-
const config = loadConfig(configPath);
|
|
77
|
-
const outputOverride = getOption("-o", "--output");
|
|
78
|
-
if (outputOverride) config.dev.outdir = outputOverride;
|
|
79
|
-
config.includeDrafts = hasFlag("--include-drafts");
|
|
80
|
-
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
|
+
}
|
|
81
75
|
|
|
82
|
-
|
|
83
|
-
|
|
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;
|
|
84
82
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
83
|
+
const { build } = require("../lib/index");
|
|
84
|
+
await build(config);
|
|
89
85
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
console.error('Error: Please provide a post title. Usage: fossbook new "My Post Title"');
|
|
94
|
-
process.exit(1);
|
|
86
|
+
const { serve } = require("../lib/server");
|
|
87
|
+
serve(config, Number(port));
|
|
88
|
+
break;
|
|
95
89
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
|
99
114
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
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;
|
|
107
121
|
}
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
122
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
133
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
message: getOption("-m", "--message"),
|
|
123
|
-
noWait: hasFlag("--no-wait"),
|
|
124
|
-
draft: hasFlag("--draft"),
|
|
125
|
-
});
|
|
126
|
-
break;
|
|
134
|
+
default:
|
|
135
|
+
console.error(`Unknown command: ${command}`);
|
|
136
|
+
printHelp();
|
|
137
|
+
process.exit(1);
|
|
127
138
|
}
|
|
128
|
-
|
|
129
|
-
default:
|
|
130
|
-
console.error(`Unknown command: ${command}`);
|
|
131
|
-
printHelp();
|
|
132
|
-
process.exit(1);
|
|
133
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,9 +42,10 @@ function createPostTranslationIndex(languageConfigs) {
|
|
|
35
42
|
|
|
36
43
|
const translations = [];
|
|
37
44
|
for (const config of languageConfigs) {
|
|
38
|
-
const sourceFile =
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
const sourceFile =
|
|
46
|
+
config.language === config.defaultLanguage
|
|
47
|
+
? "index.md"
|
|
48
|
+
: `index.${config.language}.md`;
|
|
41
49
|
const sourcePath = path.join(postsDir, entry.name, sourceFile);
|
|
42
50
|
if (!fs.existsSync(sourcePath)) continue;
|
|
43
51
|
|
|
@@ -65,18 +73,21 @@ function createAboutTranslations(languageConfigs) {
|
|
|
65
73
|
|
|
66
74
|
const contentDir = languageConfigs[0].dev.content;
|
|
67
75
|
return languageConfigs.flatMap((config) => {
|
|
68
|
-
const sourceFile =
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
const sourceFile =
|
|
77
|
+
config.language === config.defaultLanguage
|
|
78
|
+
? "about.md"
|
|
79
|
+
: `about.${config.language}.md`;
|
|
71
80
|
if (!fs.existsSync(path.join(contentDir, sourceFile))) return [];
|
|
72
81
|
|
|
73
|
-
return [
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
];
|
|
80
91
|
});
|
|
81
92
|
}
|
|
82
93
|
|
|
@@ -110,16 +121,16 @@ function createTagListTranslations(languageConfigs) {
|
|
|
110
121
|
}));
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
function buildLanguage(config) {
|
|
124
|
+
async function buildLanguage(config) {
|
|
114
125
|
fs.mkdirSync(config.dev.outdir, { recursive: true });
|
|
115
126
|
|
|
116
127
|
// Create post pages in output directory
|
|
117
128
|
const posts = new Posts(config);
|
|
118
129
|
const postObjects = posts.createPostObjects();
|
|
119
130
|
|
|
120
|
-
|
|
121
|
-
post.generateContent("post.html");
|
|
122
|
-
}
|
|
131
|
+
for (const post of postObjects) {
|
|
132
|
+
await post.generateContent("post.html");
|
|
133
|
+
}
|
|
123
134
|
|
|
124
135
|
// Create home page and pagination in output/page directory
|
|
125
136
|
const homePagination = new HomePagination(config);
|
|
@@ -137,31 +148,38 @@ function buildLanguage(config) {
|
|
|
137
148
|
tagPages.generateContent(postObjects);
|
|
138
149
|
|
|
139
150
|
// Create about page in output/about directory
|
|
140
|
-
const aboutFile =
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
const aboutFile =
|
|
152
|
+
config.language === config.defaultLanguage
|
|
153
|
+
? "about.md"
|
|
154
|
+
: `about.${config.language}.md`;
|
|
143
155
|
const aboutPath = path.resolve(config.dev.content, aboutFile);
|
|
144
156
|
if (fs.existsSync(aboutPath)) {
|
|
145
157
|
const aboutPage = new Page(config);
|
|
146
158
|
aboutPage.readSource(aboutPath, "about");
|
|
147
159
|
aboutPage.translations = config.aboutTranslations || [];
|
|
148
|
-
aboutPage.generateContent("page.html", "about");
|
|
160
|
+
await aboutPage.generateContent("page.html", "about");
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
// Copy static directory to output directory
|
|
152
164
|
const staticImagesDir = path.join(config.dev.staticDir, "images");
|
|
153
165
|
if (fs.existsSync(staticImagesDir)) {
|
|
154
|
-
copyDirectoryRecursive(
|
|
166
|
+
await copyDirectoryRecursive(
|
|
167
|
+
staticImagesDir,
|
|
168
|
+
path.join(config.dev.outdir, "images"),
|
|
169
|
+
config,
|
|
170
|
+
);
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
// Copy theme assets to output directory
|
|
158
174
|
const themeAssetsDir = path.join(config.themePath, "assets");
|
|
159
175
|
if (fs.existsSync(themeAssetsDir)) {
|
|
160
|
-
copyDirectoryRecursive(themeAssetsDir, config.dev.outdir);
|
|
176
|
+
await copyDirectoryRecursive(themeAssetsDir, config.dev.outdir, config);
|
|
161
177
|
}
|
|
162
178
|
}
|
|
163
179
|
|
|
164
|
-
function build(config) {
|
|
180
|
+
async function build(config) {
|
|
181
|
+
beginImageBuild();
|
|
182
|
+
|
|
165
183
|
// Remove the output directory
|
|
166
184
|
if (fs.existsSync(config.dev.outdir))
|
|
167
185
|
fs.rmSync(config.dev.outdir, { recursive: true });
|
|
@@ -183,7 +201,10 @@ function build(config) {
|
|
|
183
201
|
languageConfig.allPostsTranslations = allPostsTranslations;
|
|
184
202
|
languageConfig.tagListTranslations = tagListTranslations;
|
|
185
203
|
});
|
|
186
|
-
languageConfigs
|
|
204
|
+
for (const languageConfig of languageConfigs) {
|
|
205
|
+
await buildLanguage(languageConfig);
|
|
206
|
+
}
|
|
207
|
+
await finishImageBuild(config);
|
|
187
208
|
|
|
188
209
|
// Create CNAME file for GitHub Pages
|
|
189
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
|
}
|