@slybridges/kiss 0.8.5 → 0.9.0
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/package.json +8 -8
- package/src/build.js +450 -142
- package/src/cli.js +5 -0
- package/src/data/computePermalink.js +1 -1
- package/src/data/initialPageData.js +3 -0
- package/src/devServer.js +54 -9
- package/src/helpers.js +58 -25
- package/src/libs/loadNunjucks.js +7 -2
- package/src/loaders/baseLoader.js +25 -7
- package/src/loaders/jsLoader.js +2 -2
- package/src/loaders/jsonLoader.js +2 -2
- package/src/loaders/markdownLoader.js +2 -2
- package/src/loaders/staticLoader.js +1 -1
- package/src/loaders/textLoader.js +2 -2
- package/src/transforms/imageContextTransform.js +10 -6
- package/src/views/computeCollectionDataView.js +5 -1
- package/src/writers/rssContextWriter.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slybridges/kiss",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Keep It Simple and Static site generator",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"author": "Sylvestre Dupont",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"browser-sync": "^3.0.
|
|
15
|
+
"browser-sync": "^3.0.3",
|
|
16
16
|
"chalk": "^4.1.2",
|
|
17
|
-
"cheerio": "^1.0.0
|
|
17
|
+
"cheerio": "^1.0.0",
|
|
18
18
|
"chokidar": "^3.6.0",
|
|
19
19
|
"date-fns": "^3.6.0",
|
|
20
20
|
"fast-glob": "^3.3.2",
|
|
@@ -23,17 +23,17 @@
|
|
|
23
23
|
"lodash": "^4.17.21",
|
|
24
24
|
"marked": "^12.0.1",
|
|
25
25
|
"nunjucks": "^3.2.4",
|
|
26
|
-
"sharp": "^0.33.
|
|
26
|
+
"sharp": "^0.33.5",
|
|
27
27
|
"slugify": "^1.6.6",
|
|
28
28
|
"xml": "^1.0.1",
|
|
29
29
|
"yargs": "^17.7.2"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@eslint/js": "^9.
|
|
33
|
-
"eslint": "^9.
|
|
32
|
+
"@eslint/js": "^9.11.1",
|
|
33
|
+
"eslint": "^9.11.1",
|
|
34
34
|
"eslint-config-prettier": "^9.1.0",
|
|
35
|
-
"globals": "^15.
|
|
36
|
-
"prettier": "^3.
|
|
35
|
+
"globals": "^15.9.0",
|
|
36
|
+
"prettier": "^3.3.3"
|
|
37
37
|
},
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
package/src/build.js
CHANGED
|
@@ -5,62 +5,115 @@ const fg = require("fast-glob")
|
|
|
5
5
|
const path = require("path")
|
|
6
6
|
|
|
7
7
|
const { loadConfig } = require("./config")
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
computeParentId,
|
|
10
|
+
getBuildEntries,
|
|
11
|
+
getPageFromInputPath,
|
|
12
|
+
relativeToAbsoluteAttributes,
|
|
13
|
+
} = require("./helpers")
|
|
9
14
|
const { baseLoader } = require("./loaders")
|
|
10
15
|
const { setGlobalLogger } = require("./logger")
|
|
11
16
|
|
|
12
|
-
const
|
|
17
|
+
const hasOwnProperty = Object.prototype.hasOwnProperty
|
|
18
|
+
|
|
19
|
+
const build = async (options = {}, lastBuild = {}, version = 0) => {
|
|
13
20
|
console.time("Build time")
|
|
14
21
|
|
|
15
22
|
const { configFile, unsafeBuild, verbosity, watchMode } = options
|
|
16
23
|
|
|
17
24
|
setGlobalLogger(verbosity)
|
|
18
25
|
|
|
19
|
-
let
|
|
20
|
-
pages: {},
|
|
21
|
-
}
|
|
26
|
+
let { config } = lastBuild
|
|
22
27
|
|
|
23
|
-
global.logger.section("Loading config and initial context")
|
|
24
28
|
if (!config) {
|
|
29
|
+
global.logger.section("Loading config and initial context")
|
|
30
|
+
config = loadConfig({ configFile })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// config file can either be passed as option or be the default in the last build config
|
|
34
|
+
const actualConfigFile = configFile || config?.configFile
|
|
35
|
+
if (options.incremental && options.file === actualConfigFile) {
|
|
36
|
+
global.logger.section("Reloading config and initial context")
|
|
25
37
|
config = loadConfig({ configFile })
|
|
38
|
+
// clearing last build contect in case of config change
|
|
39
|
+
lastBuild.context = null
|
|
26
40
|
}
|
|
27
|
-
context = { ...context, ...config.context }
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
const buildFlags = computeBuildFlags(
|
|
43
|
+
options,
|
|
44
|
+
config,
|
|
45
|
+
lastBuild.context,
|
|
46
|
+
version,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
let context = lastBuild.context || { pages: {}, ...config.context }
|
|
50
|
+
|
|
51
|
+
if (options.incremental) {
|
|
52
|
+
// compute the file ids that will need to be updated
|
|
53
|
+
buildFlags.buildPageIds = computeBuildPageIDs(context, buildFlags)
|
|
54
|
+
if (buildFlags.buildPageIds.length > 0) {
|
|
55
|
+
global.logger.info(
|
|
56
|
+
"Change impacts pages with IDs",
|
|
57
|
+
buildFlags.buildPageIds,
|
|
58
|
+
)
|
|
59
|
+
} else if (!lastBuild.context) {
|
|
60
|
+
global.logger.info(
|
|
61
|
+
"Incremental build mode. Performing initial full build...",
|
|
62
|
+
)
|
|
63
|
+
} else {
|
|
64
|
+
global.logger.info(
|
|
65
|
+
"Incremental rebuild not possible. Performing full rebuild...",
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (buildFlags.loadLibs) {
|
|
30
71
|
global.logger.section("Running loadLibs hooks")
|
|
31
|
-
config = runConfigHooks(config, "loadLibs")
|
|
72
|
+
config = runConfigHooks(config, "loadLibs", null, buildFlags)
|
|
32
73
|
}
|
|
33
74
|
|
|
34
|
-
if (
|
|
75
|
+
if (buildFlags.preLoad) {
|
|
35
76
|
global.logger.section("Running preLoad hooks")
|
|
36
|
-
context = runConfigHooks(config, "preLoad", context)
|
|
77
|
+
context = runConfigHooks(config, "preLoad", context, buildFlags)
|
|
37
78
|
}
|
|
38
79
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
80
|
+
if (buildFlags.content) {
|
|
81
|
+
if (options.incremental && buildFlags.buildPageIds.length > 0) {
|
|
82
|
+
global.logger.section("Reloading content")
|
|
83
|
+
} else {
|
|
84
|
+
global.logger.section(`Loading content from '${config.dirs.content}'`)
|
|
85
|
+
}
|
|
86
|
+
context.pages = await loadContent(config, context, buildFlags)
|
|
87
|
+
}
|
|
43
88
|
|
|
44
|
-
if (
|
|
89
|
+
if (buildFlags.postLoad) {
|
|
45
90
|
global.logger.section("Running postLoad hooks")
|
|
46
|
-
context = runConfigHooks(config, "postLoad", context)
|
|
91
|
+
context = runConfigHooks(config, "postLoad", context, buildFlags)
|
|
47
92
|
}
|
|
48
93
|
|
|
49
|
-
|
|
50
|
-
|
|
94
|
+
if (buildFlags.dynamicData) {
|
|
95
|
+
global.logger.section("Computing dynamic page context")
|
|
96
|
+
context = computeAllPagesData(context, config, buildFlags)
|
|
97
|
+
}
|
|
51
98
|
|
|
52
|
-
|
|
53
|
-
|
|
99
|
+
if (buildFlags.dataViews) {
|
|
100
|
+
global.logger.section("Computing data views")
|
|
101
|
+
context = computeDataViews(context, config)
|
|
102
|
+
}
|
|
54
103
|
|
|
55
|
-
|
|
56
|
-
|
|
104
|
+
if (buildFlags.transform) {
|
|
105
|
+
global.logger.section(`Applying transforms`)
|
|
106
|
+
context = await applyTransforms(context, config, buildFlags)
|
|
107
|
+
}
|
|
57
108
|
|
|
58
|
-
|
|
59
|
-
|
|
109
|
+
if (buildFlags.write) {
|
|
110
|
+
global.logger.section(`Writing site to '${config.dirs.public}' directory`)
|
|
111
|
+
await writeStaticSite(context, config, buildFlags)
|
|
112
|
+
}
|
|
60
113
|
|
|
61
|
-
if (
|
|
114
|
+
if (buildFlags.postWrite) {
|
|
62
115
|
global.logger.section("Running postWrite hooks")
|
|
63
|
-
runConfigHooks(config, "postWrite", context)
|
|
116
|
+
runConfigHooks(config, "postWrite", context, buildFlags)
|
|
64
117
|
}
|
|
65
118
|
|
|
66
119
|
global.logger.section("Build complete")
|
|
@@ -84,16 +137,19 @@ const build = async (options = {}, config = null) => {
|
|
|
84
137
|
} else if (warningCount > 0) {
|
|
85
138
|
global.logger.warn(`${warningCount} warning(s) found.`)
|
|
86
139
|
} else {
|
|
87
|
-
global.logger.success(`
|
|
140
|
+
global.logger.success(`Build completed!`)
|
|
88
141
|
}
|
|
89
142
|
console.timeEnd("Build time")
|
|
143
|
+
|
|
144
|
+
return { context, config }
|
|
90
145
|
}
|
|
91
146
|
|
|
92
147
|
module.exports = build
|
|
93
148
|
|
|
94
149
|
/** Private **/
|
|
95
150
|
|
|
96
|
-
const applyTransforms = async (context, config) => {
|
|
151
|
+
const applyTransforms = async (context, config, buildFlags) => {
|
|
152
|
+
const { buildPageIds, incremental } = buildFlags
|
|
97
153
|
if (!config.transforms || config.transforms.length === 0) {
|
|
98
154
|
global.logger.info(`No transform registered.`)
|
|
99
155
|
return context
|
|
@@ -119,7 +175,7 @@ const applyTransforms = async (context, config) => {
|
|
|
119
175
|
options.description || `Transforming context using ${handler.name}`
|
|
120
176
|
global.logger.info(message)
|
|
121
177
|
try {
|
|
122
|
-
context = await handler(context, options, config)
|
|
178
|
+
context = await handler(context, options, config, buildFlags)
|
|
123
179
|
} catch (err) {
|
|
124
180
|
global.logger.error(
|
|
125
181
|
`[${handler.name}] Error during transform:\n`,
|
|
@@ -127,12 +183,21 @@ const applyTransforms = async (context, config) => {
|
|
|
127
183
|
)
|
|
128
184
|
}
|
|
129
185
|
} else {
|
|
130
|
-
|
|
131
|
-
|
|
186
|
+
const entries = getBuildEntries(context, buildFlags).filter(
|
|
187
|
+
([, page]) => !outputType || outputType === page._meta.outputType,
|
|
188
|
+
)
|
|
189
|
+
if (entries.length === 0) {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
let message =
|
|
132
193
|
options.description ||
|
|
133
|
-
`Transforming ${outputType || "all"} pages using ${handler.name}`
|
|
194
|
+
`Transforming ${outputType || "all"} pages using '${handler.name}'`
|
|
195
|
+
if (incremental && buildPageIds.length > 0) {
|
|
196
|
+
message += ` (incremental)`
|
|
197
|
+
}
|
|
134
198
|
global.logger.info(message)
|
|
135
|
-
|
|
199
|
+
// page transforms
|
|
200
|
+
for (let [id, page] of entries) {
|
|
136
201
|
if (outputType && page._meta.outputType !== outputType) {
|
|
137
202
|
continue
|
|
138
203
|
}
|
|
@@ -151,6 +216,140 @@ const applyTransforms = async (context, config) => {
|
|
|
151
216
|
return context
|
|
152
217
|
}
|
|
153
218
|
|
|
219
|
+
const computeBuildFlags = (options, config, lastContext, version) => {
|
|
220
|
+
let flags = {
|
|
221
|
+
incremental: options.incremental,
|
|
222
|
+
config: true,
|
|
223
|
+
loadLibs: config.hooks.loadLibs.length > 0,
|
|
224
|
+
preLoad: config.hooks.preLoad.length > 0,
|
|
225
|
+
content: true,
|
|
226
|
+
postLoad: config.hooks.postLoad.length > 0,
|
|
227
|
+
dynamicData: true,
|
|
228
|
+
dataViews: true,
|
|
229
|
+
transform: true,
|
|
230
|
+
write: true,
|
|
231
|
+
postWrite: config.hooks.postWrite.length > 0,
|
|
232
|
+
version: version,
|
|
233
|
+
}
|
|
234
|
+
if (!options.incremental || !lastContext || !options.file) {
|
|
235
|
+
// full build
|
|
236
|
+
return flags
|
|
237
|
+
}
|
|
238
|
+
// incremental build mode
|
|
239
|
+
if (options.event === "addDir") {
|
|
240
|
+
// skip directory changes
|
|
241
|
+
return {
|
|
242
|
+
incremental: options.incremental,
|
|
243
|
+
config: false,
|
|
244
|
+
loadLibs: false,
|
|
245
|
+
preLoad: false,
|
|
246
|
+
content: false,
|
|
247
|
+
postLoad: false,
|
|
248
|
+
dynamicData: false,
|
|
249
|
+
dataViews: false,
|
|
250
|
+
transform: false,
|
|
251
|
+
write: false,
|
|
252
|
+
postWrite: false,
|
|
253
|
+
version: version,
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// only 'change' event is supported for now in incremental mode
|
|
257
|
+
// 'add', 'unlink' and 'unlinkDir' will trigger a full rebuild
|
|
258
|
+
if (options.event !== "change") {
|
|
259
|
+
return flags
|
|
260
|
+
}
|
|
261
|
+
flags.file = options.file
|
|
262
|
+
flags.config = false
|
|
263
|
+
flags.content = false
|
|
264
|
+
flags.dynamicData = false
|
|
265
|
+
flags.dataViews = false
|
|
266
|
+
if (options.file.startsWith(config.dirs.content)) {
|
|
267
|
+
// Content change: we need to reload the changed content, recompute the data,
|
|
268
|
+
// do the transforms and write the files
|
|
269
|
+
flags.content = true
|
|
270
|
+
flags.contentFile = options.file
|
|
271
|
+
flags.dynamicData = true
|
|
272
|
+
flags.dataViews = true
|
|
273
|
+
} else if (options.file.startsWith(config.dirs.template)) {
|
|
274
|
+
// Template change: no need to reload the content or recompute the data.
|
|
275
|
+
// We only need to perform the transforms and write the files
|
|
276
|
+
// Templates are always related to the template directory
|
|
277
|
+
flags.templateFile = path.relative(config.dirs.template, options.file)
|
|
278
|
+
}
|
|
279
|
+
// compute hook flags
|
|
280
|
+
flags.loadLibs = computeIncrementalHookBuildFlag(
|
|
281
|
+
config.hooks.loadLibs,
|
|
282
|
+
options.file,
|
|
283
|
+
lastContext,
|
|
284
|
+
)
|
|
285
|
+
flags.preLoad = computeIncrementalHookBuildFlag(
|
|
286
|
+
config.hooks.preLoad,
|
|
287
|
+
options.file,
|
|
288
|
+
lastContext,
|
|
289
|
+
)
|
|
290
|
+
flags.postLoad = computeIncrementalHookBuildFlag(
|
|
291
|
+
config.hooks.postLoad,
|
|
292
|
+
options.file,
|
|
293
|
+
lastContext,
|
|
294
|
+
)
|
|
295
|
+
flags.postWrite = computeIncrementalHookBuildFlag(
|
|
296
|
+
config.hooks.postWrite,
|
|
297
|
+
options.file,
|
|
298
|
+
lastContext,
|
|
299
|
+
)
|
|
300
|
+
// other change: full rebuild
|
|
301
|
+
return flags
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// return the list of ids impacted by a file change which are
|
|
305
|
+
// the file itself, its ascendants and descendants in case of a content file change
|
|
306
|
+
// or all pages using a template in case of a template file change
|
|
307
|
+
// if there is no file change, return an empty list
|
|
308
|
+
const computeBuildPageIDs = (context, buildFlags) => {
|
|
309
|
+
let buildPageIds = []
|
|
310
|
+
if (buildFlags.contentFile) {
|
|
311
|
+
const page = getPageFromInputPath(buildFlags.contentFile, context.pages)
|
|
312
|
+
if (!page) {
|
|
313
|
+
global.logger.warn(
|
|
314
|
+
`Could not find existing page for '${buildFlags.contentFile}'.`,
|
|
315
|
+
)
|
|
316
|
+
return []
|
|
317
|
+
}
|
|
318
|
+
// ascendants
|
|
319
|
+
buildPageIds = buildPageIds.concat(page._meta.ascendants)
|
|
320
|
+
// current page
|
|
321
|
+
buildPageIds.push(page._meta.id)
|
|
322
|
+
// descendants
|
|
323
|
+
buildPageIds = buildPageIds.concat(page._meta.descendants)
|
|
324
|
+
} else if (buildFlags.templateFile) {
|
|
325
|
+
// mark all pages using that template for rebuild
|
|
326
|
+
// Note: if the template is a sub-template of another template
|
|
327
|
+
// (e.g. that is included() in a main template),
|
|
328
|
+
// this will not work and we'll rebuild everything
|
|
329
|
+
buildPageIds = Object.values(context.pages)
|
|
330
|
+
.filter((page) => page.layout === buildFlags.templateFile)
|
|
331
|
+
.map((page) => page._meta.id)
|
|
332
|
+
}
|
|
333
|
+
return buildPageIds
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const computeIncrementalHookBuildFlag = (hooks, file, context) => {
|
|
337
|
+
if (!hooks || hooks.length === 0) {
|
|
338
|
+
return false
|
|
339
|
+
}
|
|
340
|
+
// check if some of the hook have a incrementalRebuild attribute
|
|
341
|
+
// if they do call the function and return true if any of the function returns true
|
|
342
|
+
return hooks.some((hook) => {
|
|
343
|
+
if (typeof hook === "function") {
|
|
344
|
+
return true
|
|
345
|
+
}
|
|
346
|
+
if (typeof hook.incrementalRebuild === "function") {
|
|
347
|
+
return hook.incrementalRebuild(file, context)
|
|
348
|
+
}
|
|
349
|
+
return false
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
154
353
|
// load content derived from existing pages
|
|
155
354
|
const computeDataViews = (context, config) => {
|
|
156
355
|
config.dataViews.forEach((view) => {
|
|
@@ -170,32 +369,28 @@ const computeDataViews = (context, config) => {
|
|
|
170
369
|
return context
|
|
171
370
|
}
|
|
172
371
|
|
|
173
|
-
const computePageData = (data, config, context, options = {}) => {
|
|
372
|
+
const computePageData = (data, config, context, buildFlags, options = {}) => {
|
|
174
373
|
let computed = {
|
|
175
|
-
data:
|
|
374
|
+
data: _.isArray(data) ? [...data] : { ...data },
|
|
176
375
|
pendingCount: 0,
|
|
177
376
|
}
|
|
178
|
-
if (_.isArray(data)) {
|
|
179
|
-
computed.data = [...data]
|
|
180
|
-
} else {
|
|
181
|
-
computed.data = { ...data }
|
|
182
|
-
}
|
|
183
377
|
if (!options.topLevelData) {
|
|
184
378
|
// for recursive call
|
|
185
379
|
options.topLevelData = data
|
|
186
380
|
}
|
|
187
381
|
for (let key in data) {
|
|
188
382
|
let value = data[key]
|
|
189
|
-
|
|
190
|
-
if (typeof key === "string"
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
383
|
+
|
|
384
|
+
if (typeof key === "string") {
|
|
385
|
+
if (key.endsWith("_no_cascade")) {
|
|
386
|
+
// this is an override key, computing original key value
|
|
387
|
+
key = key.split("_no_cascade")[0]
|
|
388
|
+
} else if (hasOwnProperty.call(data, key + "_no_cascade")) {
|
|
389
|
+
// there is a data override attribute, bail out.
|
|
390
|
+
continue
|
|
391
|
+
}
|
|
198
392
|
}
|
|
393
|
+
|
|
199
394
|
if (typeof value === "function") {
|
|
200
395
|
// it's a function we need to compute the result
|
|
201
396
|
let currentPending = 0
|
|
@@ -219,7 +414,13 @@ const computePageData = (data, config, context, options = {}) => {
|
|
|
219
414
|
}
|
|
220
415
|
} else if (_.isPlainObject(value) || _.isArray(value)) {
|
|
221
416
|
// it's an object: we need to see if there is data to compute inside
|
|
222
|
-
let subComputed = computePageData(
|
|
417
|
+
let subComputed = computePageData(
|
|
418
|
+
value,
|
|
419
|
+
config,
|
|
420
|
+
context,
|
|
421
|
+
buildFlags,
|
|
422
|
+
options,
|
|
423
|
+
)
|
|
223
424
|
computed.data[key] = subComputed.data
|
|
224
425
|
computed.pendingCount += subComputed.pendingCount
|
|
225
426
|
} else {
|
|
@@ -229,16 +430,17 @@ const computePageData = (data, config, context, options = {}) => {
|
|
|
229
430
|
return computed
|
|
230
431
|
}
|
|
231
432
|
|
|
232
|
-
const computeAllPagesData = (context, config) => {
|
|
433
|
+
const computeAllPagesData = (context, config, buildFlags) => {
|
|
233
434
|
let pendingTotal = 0
|
|
234
435
|
let round = 1
|
|
235
436
|
let computed = {}
|
|
236
437
|
|
|
237
438
|
while (round === 1 || pendingTotal > 0) {
|
|
439
|
+
let entries = getBuildEntries(context, buildFlags)
|
|
238
440
|
pendingTotal = 0
|
|
239
|
-
|
|
441
|
+
entries.forEach(([key, page]) => {
|
|
240
442
|
try {
|
|
241
|
-
computed = computePageData(page, config, context)
|
|
443
|
+
computed = computePageData(page, config, context, buildFlags)
|
|
242
444
|
} catch (err) {
|
|
243
445
|
global.logger.error(
|
|
244
446
|
`[computePageData] Error during computing page data for page id '${page._meta.id}'\n`,
|
|
@@ -249,10 +451,9 @@ const computeAllPagesData = (context, config) => {
|
|
|
249
451
|
pendingTotal += computed.pendingCount
|
|
250
452
|
})
|
|
251
453
|
if (pendingTotal > 0 && round + 1 > config.defaults.maxComputingRounds) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
)
|
|
454
|
+
let message = `Could not compute all data in ${config.defaults.maxComputingRounds} rounds.`
|
|
455
|
+
message += ` Check for circular dependencies or increase the 'config.defaults.maxComputingRounds' value.`
|
|
456
|
+
global.logger.error(message)
|
|
256
457
|
break
|
|
257
458
|
}
|
|
258
459
|
if (pendingTotal > 0) {
|
|
@@ -310,32 +511,55 @@ const countPendingDependencies = (page, pages, deps = []) => {
|
|
|
310
511
|
return pendingCount
|
|
311
512
|
}
|
|
312
513
|
|
|
313
|
-
const directoryCollectionLoader = (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
514
|
+
const directoryCollectionLoader = (
|
|
515
|
+
pathname,
|
|
516
|
+
options,
|
|
517
|
+
pages,
|
|
518
|
+
config,
|
|
519
|
+
buildFlags,
|
|
520
|
+
) => {
|
|
521
|
+
const parentPath = path.dirname(pathname)
|
|
522
|
+
const parentId = computeParentId(pathname, config)
|
|
523
|
+
if (!parentId) {
|
|
524
|
+
// reached the top parent
|
|
525
|
+
return pages
|
|
526
|
+
}
|
|
527
|
+
if (
|
|
528
|
+
pages[parentId] &&
|
|
529
|
+
pages[parentId]._meta.buildVersion === buildFlags.version
|
|
530
|
+
) {
|
|
531
|
+
// parent already in the collection and versions match
|
|
318
532
|
return pages
|
|
319
533
|
}
|
|
320
534
|
let isTopLevel = true
|
|
321
|
-
const parentBasename = path.basename(
|
|
322
|
-
if (parentBasename !==
|
|
535
|
+
const parentBasename = path.basename(parentPath)
|
|
536
|
+
if (parentBasename !== parentPath) {
|
|
323
537
|
// there is a parent of the parent folder
|
|
324
538
|
// according to our top down data cascade approach,
|
|
325
539
|
// we need to compute this one first
|
|
326
540
|
isTopLevel = false
|
|
327
|
-
pages = directoryCollectionLoader(
|
|
541
|
+
pages = directoryCollectionLoader(
|
|
542
|
+
parentPath,
|
|
543
|
+
options,
|
|
544
|
+
pages,
|
|
545
|
+
config,
|
|
546
|
+
buildFlags,
|
|
547
|
+
)
|
|
328
548
|
}
|
|
329
549
|
let parent = baseLoader(
|
|
330
|
-
|
|
331
|
-
{
|
|
550
|
+
parentPath,
|
|
551
|
+
{
|
|
552
|
+
isDirectory: true,
|
|
553
|
+
collectionGroup: "directory",
|
|
554
|
+
buildVersion: buildFlags.version,
|
|
555
|
+
},
|
|
332
556
|
isTopLevel ? config.defaults.pageData : {},
|
|
333
557
|
pages,
|
|
334
558
|
config,
|
|
335
559
|
)
|
|
336
560
|
|
|
337
561
|
return _.merge({}, pages, {
|
|
338
|
-
[
|
|
562
|
+
[parentId]: parent,
|
|
339
563
|
})
|
|
340
564
|
}
|
|
341
565
|
|
|
@@ -343,106 +567,176 @@ const isComputableValue = (value) =>
|
|
|
343
567
|
typeof value === "function" ||
|
|
344
568
|
(_.isPlainObject(value) && value._kissCheckDependencies)
|
|
345
569
|
|
|
570
|
+
const getFiles = (
|
|
571
|
+
loaderId,
|
|
572
|
+
config,
|
|
573
|
+
match = null,
|
|
574
|
+
contentPathInMatch = false,
|
|
575
|
+
) => {
|
|
576
|
+
const loader = config.loaders[loaderId]
|
|
577
|
+
if (!loader) {
|
|
578
|
+
global.logger.error(`Loader with id ${loaderId} not found.`)
|
|
579
|
+
return []
|
|
580
|
+
}
|
|
581
|
+
// eslint-disable-next-line no-unused-vars
|
|
582
|
+
const { handler, namespace, ...loaderOptions } = loader
|
|
583
|
+
const allOptions = getOptions(config, namespace, loaderOptions)
|
|
584
|
+
const { matchOptions = {} } = allOptions
|
|
585
|
+
if (typeof match === "string") {
|
|
586
|
+
match = [match]
|
|
587
|
+
} else if (!match) {
|
|
588
|
+
match = allOptions.match
|
|
589
|
+
}
|
|
590
|
+
const fgOptions = {
|
|
591
|
+
...matchOptions,
|
|
592
|
+
markDirectories: true,
|
|
593
|
+
stats: true,
|
|
594
|
+
}
|
|
595
|
+
if (!contentPathInMatch) {
|
|
596
|
+
fgOptions.cwd = config.dirs.content
|
|
597
|
+
}
|
|
598
|
+
return fg.sync(match, fgOptions).map((file) => ({
|
|
599
|
+
...file,
|
|
600
|
+
loaderId,
|
|
601
|
+
// not sure why but it looks like sometimes fg returns the full path in the name
|
|
602
|
+
name: file.name.includes(path.sep) ? path.basename(file.name) : file.name,
|
|
603
|
+
// make sure path always includes the content directory
|
|
604
|
+
path: contentPathInMatch
|
|
605
|
+
? file.path
|
|
606
|
+
: path.join(config.dirs.content, file.path),
|
|
607
|
+
}))
|
|
608
|
+
}
|
|
609
|
+
|
|
346
610
|
const getOptions = (config, namespace, options) => {
|
|
347
611
|
const nameSpaceOptions = _.get(config, namespace, {})
|
|
348
612
|
return { ...nameSpaceOptions, ...options }
|
|
349
613
|
}
|
|
350
614
|
|
|
351
|
-
const loadContent = async (config, context) => {
|
|
352
|
-
|
|
615
|
+
const loadContent = async (config, context, buildFlags) => {
|
|
616
|
+
const { incremental, buildPageIds } = buildFlags
|
|
617
|
+
let pages = context.pages
|
|
353
618
|
let files = []
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
619
|
+
const isIncrementalBuild = incremental && buildPageIds?.length > 0
|
|
620
|
+
|
|
621
|
+
if (isIncrementalBuild) {
|
|
622
|
+
// incremental build: one content file changed
|
|
623
|
+
// -> reload the file + all their ascendants + any descendant
|
|
624
|
+
// files here are relative to the content directory
|
|
625
|
+
|
|
626
|
+
buildPageIds.forEach((id) => {
|
|
627
|
+
const page = pages[id]
|
|
628
|
+
if (!page) {
|
|
629
|
+
global.logger.warn(
|
|
630
|
+
`[loadContent] Page with id '${id}' not found. Skipping.`,
|
|
631
|
+
)
|
|
366
632
|
return
|
|
367
633
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
extraOptions.push(`options: ${JSON.stringify(loaderOptions)}`)
|
|
634
|
+
// add to files to load
|
|
635
|
+
if (page._meta.indexInputPath) {
|
|
636
|
+
// also need to reload the index file
|
|
637
|
+
files = files.concat(
|
|
638
|
+
getFiles(
|
|
639
|
+
page._meta.indexLoaderId,
|
|
640
|
+
config,
|
|
641
|
+
page._meta.indexInputPath,
|
|
642
|
+
true,
|
|
643
|
+
),
|
|
644
|
+
)
|
|
380
645
|
}
|
|
381
|
-
global.logger.info(
|
|
382
|
-
`Listing files matching ${JSON.stringify(match)} for ${handler.name}${extraOptions.length > 0 ? ` (${extraOptions.join(", ")})` : ""}`,
|
|
383
|
-
)
|
|
384
646
|
files = files.concat(
|
|
385
|
-
|
|
647
|
+
getFiles(page._meta.loaderId, config, page._meta.inputPath, true),
|
|
386
648
|
)
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
global.logger.info(`Reloading ${files.length} files...`)
|
|
652
|
+
} else {
|
|
653
|
+
// We first need to fetch all files
|
|
654
|
+
// So that we can sort them in the right order before loading them
|
|
655
|
+
// This is essential for the data cascade to work properly
|
|
656
|
+
config.loaders.forEach(
|
|
657
|
+
({ handler, namespace, source, ...loaderOptions }, idx) => {
|
|
658
|
+
if (source && source !== "file") {
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
const options = getOptions(config, namespace, loaderOptions)
|
|
662
|
+
if ("active" in options && !options.active) {
|
|
663
|
+
global.logger.log(`[${handler.name}]: loader not active. Skipping.`)
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
let extraOptions = []
|
|
667
|
+
if (namespace !== handler.name) {
|
|
668
|
+
extraOptions.push(`namespace: '${namespace}'`)
|
|
669
|
+
}
|
|
670
|
+
if (loaderOptions && Object.keys(loaderOptions).length > 0) {
|
|
671
|
+
extraOptions.push(`options: ${JSON.stringify(loaderOptions)}`)
|
|
672
|
+
}
|
|
673
|
+
// full build: load all files matching this loader
|
|
674
|
+
global.logger.info(
|
|
675
|
+
`Listing files matching ${JSON.stringify(options.match)} for ${handler.name}${extraOptions.length > 0 ? ` (${extraOptions.join(", ")})` : ""}`,
|
|
676
|
+
)
|
|
677
|
+
files = files.concat(getFiles(idx, config))
|
|
678
|
+
},
|
|
679
|
+
)
|
|
680
|
+
global.logger.info(`Found ${files.length} files. Loading...`)
|
|
681
|
+
}
|
|
392
682
|
// sorting and loading files
|
|
393
683
|
// sort files to make sure index.* files are loaded first and post.* files last
|
|
394
684
|
// in order to respect the data cascade principle
|
|
395
685
|
files = sortFiles(files)
|
|
396
|
-
global.logger.info(`Loading files...`)
|
|
397
686
|
for (const file of files) {
|
|
398
687
|
// finding the right loader for the file
|
|
399
688
|
const { handler, namespace, ...loaderOptions } =
|
|
400
|
-
config.loaders[file.
|
|
689
|
+
config.loaders[file.loaderId]
|
|
401
690
|
const options = getOptions(config, namespace, loaderOptions)
|
|
402
|
-
let pathname = path.join(config.dirs.content, file.path)
|
|
403
691
|
let page = {}
|
|
404
692
|
try {
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
693
|
+
// initialize pages from directory structures
|
|
694
|
+
// this is so that we have a page entry from each folder, even if there are no index or post files
|
|
695
|
+
pages = directoryCollectionLoader(
|
|
696
|
+
file.path,
|
|
697
|
+
options,
|
|
698
|
+
pages,
|
|
699
|
+
config,
|
|
700
|
+
buildFlags,
|
|
701
|
+
)
|
|
414
702
|
// load base data including _meta infos and based on ParentData
|
|
415
|
-
page = baseLoader(
|
|
703
|
+
page = baseLoader(
|
|
704
|
+
file.path,
|
|
705
|
+
{ ...options, file, buildVersion: buildFlags.version },
|
|
706
|
+
page,
|
|
707
|
+
pages,
|
|
708
|
+
config,
|
|
709
|
+
)
|
|
416
710
|
// load content specific data
|
|
417
|
-
page = await handler(
|
|
711
|
+
page = await handler(file.path, options, page, context, config)
|
|
418
712
|
// relative @attributes to absolute
|
|
419
713
|
page = relativeToAbsoluteAttributes(page, options, config)
|
|
420
714
|
pages[page._meta.id] = page
|
|
421
|
-
global.logger.log(`- [${handler.name}] loaded '${
|
|
715
|
+
global.logger.log(`- [${handler.name}] loaded '${file.path}'`)
|
|
422
716
|
} catch (err) {
|
|
423
717
|
global.logger.error(
|
|
424
|
-
`- [${handler.name}] Error loading '${
|
|
425
|
-
page,
|
|
426
|
-
"_meta.inputPath",
|
|
427
|
-
)}'\n`,
|
|
718
|
+
`- [${handler.name}] Error loading '${file.path}'\n`,
|
|
428
719
|
err.stack,
|
|
429
720
|
)
|
|
430
721
|
}
|
|
431
722
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
description
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
723
|
+
if (!incremental || buildPageIds?.length === 0) {
|
|
724
|
+
// full build
|
|
725
|
+
// computed loaders
|
|
726
|
+
_.filter(config.loaders, (loader) => loader.source === "computed").forEach(
|
|
727
|
+
(loader) => {
|
|
728
|
+
const { description, handler, ...options } = loader
|
|
729
|
+
const message =
|
|
730
|
+
description || `Loading computed pages using ${handler.name}`
|
|
731
|
+
global.logger.info(message)
|
|
732
|
+
pages = handler(pages, options, config)
|
|
733
|
+
},
|
|
734
|
+
)
|
|
735
|
+
}
|
|
442
736
|
return pages
|
|
443
737
|
}
|
|
444
738
|
|
|
445
|
-
const runConfigHooks = (config, event, data) => {
|
|
739
|
+
const runConfigHooks = (config, event, data, buildFlags) => {
|
|
446
740
|
const hooks = _.get(config.hooks, event)
|
|
447
741
|
if (!hooks || hooks.length === 0) {
|
|
448
742
|
global.logger.log(`No hooks registered for ${event}`)
|
|
@@ -450,9 +744,19 @@ const runConfigHooks = (config, event, data) => {
|
|
|
450
744
|
}
|
|
451
745
|
hooks.forEach((hook) => {
|
|
452
746
|
if (typeof hook === "function") {
|
|
453
|
-
data = runHandlerHook(hook, {}, config, data)
|
|
747
|
+
data = runHandlerHook(hook, {}, config, data, buildFlags)
|
|
454
748
|
} else {
|
|
455
|
-
const { action, handler, command, ...options } = hook
|
|
749
|
+
const { action, handler, command, incrementalRebuild, ...options } = hook
|
|
750
|
+
if (buildFlags.incremental && buildFlags.file) {
|
|
751
|
+
// during an incremental rebuild, only run hooks that have an incrementalRebuild function set
|
|
752
|
+
if (
|
|
753
|
+
!incrementalRebuild ||
|
|
754
|
+
(typeof incrementalRebuild === "function" &&
|
|
755
|
+
!incrementalRebuild(buildFlags.file, data))
|
|
756
|
+
) {
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
}
|
|
456
760
|
switch (action) {
|
|
457
761
|
case "copy":
|
|
458
762
|
runCopyHook(options, config)
|
|
@@ -461,7 +765,7 @@ const runConfigHooks = (config, event, data) => {
|
|
|
461
765
|
runExecHook(command, options)
|
|
462
766
|
break
|
|
463
767
|
case "run":
|
|
464
|
-
data = runHandlerHook(handler, options, config, data)
|
|
768
|
+
data = runHandlerHook(handler, options, config, data, buildFlags)
|
|
465
769
|
break
|
|
466
770
|
default: {
|
|
467
771
|
global.logger.error(`Unknown hook action: ${action}`)
|
|
@@ -534,21 +838,23 @@ const sortFiles = (files) => {
|
|
|
534
838
|
})
|
|
535
839
|
}
|
|
536
840
|
|
|
537
|
-
const runHandlerHook = (handler, options, config, data) => {
|
|
841
|
+
const runHandlerHook = (handler, options, config, data, buildFlags) => {
|
|
538
842
|
const message = options.description || `Running ${handler.name}`
|
|
539
843
|
global.logger.info(message)
|
|
540
844
|
try {
|
|
541
|
-
return handler(options, config, data)
|
|
845
|
+
return handler(options, config, data, buildFlags)
|
|
542
846
|
} catch (err) {
|
|
543
847
|
global.logger.error(`Error in ${handler.name}:\n`, err.stack)
|
|
544
848
|
return data
|
|
545
849
|
}
|
|
546
850
|
}
|
|
547
851
|
|
|
548
|
-
const writeStaticSite = async (context, config) => {
|
|
549
|
-
|
|
852
|
+
const writeStaticSite = async (context, config, buildFlags) => {
|
|
853
|
+
const entries = getBuildEntries(context, buildFlags)
|
|
854
|
+
let message = `Writing ${entries.length} pages and images`
|
|
855
|
+
global.logger.info(message)
|
|
550
856
|
await Promise.all(
|
|
551
|
-
|
|
857
|
+
entries.map(async ([, page]) => {
|
|
552
858
|
if (page.excludeFromWrite) {
|
|
553
859
|
global.logger.log(
|
|
554
860
|
`- Page '${page._meta.id}' is marked as excludeFromWrite. Skipping.`,
|
|
@@ -557,6 +863,8 @@ const writeStaticSite = async (context, config) => {
|
|
|
557
863
|
global.logger.log(
|
|
558
864
|
`- Page '${page._meta.id}' has no permalink. Skipping.`,
|
|
559
865
|
)
|
|
866
|
+
} else if (page._meta.outputType === "SKIP") {
|
|
867
|
+
global.logger.log(`- Page '${page._meta.id}' has SKIP type. Skipping.`)
|
|
560
868
|
} else {
|
|
561
869
|
const writer = config.writers.find(
|
|
562
870
|
(writer) => writer.outputType === page._meta.outputType,
|
package/src/cli.js
CHANGED
|
@@ -27,4 +27,9 @@ yargs(hideBin(process.argv))
|
|
|
27
27
|
default: false,
|
|
28
28
|
describe: "Won't exit(1) on build errors",
|
|
29
29
|
})
|
|
30
|
+
.option("incremental", {
|
|
31
|
+
boolean: true,
|
|
32
|
+
default: false,
|
|
33
|
+
describe: "Performs incremental builds on watch (experimental)",
|
|
34
|
+
})
|
|
30
35
|
.demandCommand(1, "Enter a command").argv
|
|
@@ -36,7 +36,7 @@ const computePermalink = ({ slug, _meta }, config, { pages }) => {
|
|
|
36
36
|
const parent = pages[_meta.parent]
|
|
37
37
|
if (typeof parent.permalink === "function") {
|
|
38
38
|
// return the function itself hoping parent permalink will be computed later
|
|
39
|
-
return computePermalink
|
|
39
|
+
return computePermalink
|
|
40
40
|
}
|
|
41
41
|
if (typeof parent.permalink === "string") {
|
|
42
42
|
basePermalink = parent.permalink
|
|
@@ -45,10 +45,13 @@ const initialPageData = {
|
|
|
45
45
|
descendants: computeDescendants,
|
|
46
46
|
fileCreated: null,
|
|
47
47
|
fileModified: null,
|
|
48
|
+
indexInputPath: null,
|
|
49
|
+
indexLoaderId: null,
|
|
48
50
|
inputPath: "",
|
|
49
51
|
isCollection: computeIsCollection,
|
|
50
52
|
isDirectory: false,
|
|
51
53
|
isPost: computeIsPost,
|
|
54
|
+
loaderId: null,
|
|
52
55
|
outputPath: computeOutputPath,
|
|
53
56
|
parent: null,
|
|
54
57
|
outputType: null,
|
package/src/devServer.js
CHANGED
|
@@ -9,6 +9,11 @@ const { resetGlobalLogger } = require("./logger")
|
|
|
9
9
|
|
|
10
10
|
const build = require("./build.js")
|
|
11
11
|
|
|
12
|
+
let lastBuild = { context: null, config: null }
|
|
13
|
+
let buildVersion = 0
|
|
14
|
+
let isBuildRunning = false
|
|
15
|
+
let changeBacklog = []
|
|
16
|
+
|
|
12
17
|
const serve = (options, config) => {
|
|
13
18
|
if (!config) {
|
|
14
19
|
config = loadConfig(options)
|
|
@@ -48,14 +53,41 @@ const watch = async (options = {}, config) => {
|
|
|
48
53
|
config.configFile,
|
|
49
54
|
...config.dirs.watchExtra,
|
|
50
55
|
],
|
|
51
|
-
{ awaitWriteFinish: true },
|
|
56
|
+
{ awaitWriteFinish: true, ignoreInitial: true },
|
|
52
57
|
)
|
|
53
58
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
// initial build
|
|
60
|
+
lastBuild = await build(options)
|
|
61
|
+
|
|
62
|
+
if (options.incremental) {
|
|
63
|
+
console.info("Incremental build mode enabled")
|
|
64
|
+
// rebuild only the changed file
|
|
65
|
+
fileWatcher.on("all", async (event, file) => {
|
|
66
|
+
if (isBuildRunning) {
|
|
67
|
+
// there is already a build running, add to backlog
|
|
68
|
+
changeBacklog.push({ event, file })
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
isBuildRunning = true
|
|
72
|
+
lastBuild = await incrementalRebuild(options, lastBuild, event, file)
|
|
73
|
+
while (changeBacklog.length > 0) {
|
|
74
|
+
const change = changeBacklog.shift()
|
|
75
|
+
lastBuild = await incrementalRebuild(
|
|
76
|
+
options,
|
|
77
|
+
lastBuild,
|
|
78
|
+
change.event,
|
|
79
|
+
change.file,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
isBuildRunning = false
|
|
83
|
+
})
|
|
84
|
+
} else {
|
|
85
|
+
// rebuild all on file changes
|
|
86
|
+
fileWatcher.on(
|
|
87
|
+
"all",
|
|
88
|
+
_.debounce((event, file) => rebuild(options, event, file), 500),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
59
91
|
|
|
60
92
|
// remove chokidar watcher
|
|
61
93
|
let signals = [
|
|
@@ -71,7 +103,10 @@ const watch = async (options = {}, config) => {
|
|
|
71
103
|
signals.forEach((signal) => {
|
|
72
104
|
process.on(signal, (code) => {
|
|
73
105
|
fileWatcher.close()
|
|
74
|
-
|
|
106
|
+
if (signal === "uncaughtException") {
|
|
107
|
+
global.logger.error("kiss encountered a fatal error\n", code)
|
|
108
|
+
}
|
|
109
|
+
process.exit(1)
|
|
75
110
|
})
|
|
76
111
|
})
|
|
77
112
|
}
|
|
@@ -107,8 +142,18 @@ const preparePublicFolder = async (config) => {
|
|
|
107
142
|
}
|
|
108
143
|
|
|
109
144
|
const rebuild = (options, event, file) => {
|
|
110
|
-
|
|
145
|
+
global.logger.section(`\nChange detected: [${event}] ${file}`)
|
|
146
|
+
clearRequireCache()
|
|
147
|
+
resetGlobalLogger(options.verbosity)
|
|
148
|
+
return build(options)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const incrementalRebuild = async (options, lastBuild, event, file) => {
|
|
152
|
+
buildVersion++
|
|
153
|
+
global.logger.section(
|
|
154
|
+
`\nChange detected: [${event}] ${file}. Incremental rebuild #${buildVersion}...`,
|
|
155
|
+
)
|
|
111
156
|
clearRequireCache()
|
|
112
157
|
resetGlobalLogger(options.verbosity)
|
|
113
|
-
build(options)
|
|
158
|
+
return build({ event, file, ...options }, lastBuild, buildVersion)
|
|
114
159
|
}
|
package/src/helpers.js
CHANGED
|
@@ -6,9 +6,28 @@ const path = require("path")
|
|
|
6
6
|
// terminators: space, comma, newline, end of string, ', ",<, >, ), ], }, #
|
|
7
7
|
const AT_GENERIC_ATTRIBUTE_REGEX =
|
|
8
8
|
/@([a-zA-Z0-9-_]+):([^,\s\n\]'"<>)}#]+)(?=[,\s\n\]'"<>)}#]|$)/g
|
|
9
|
+
// TODO: try with const AT_GENERIC_ATTRIBUTE_REGEX = /@([a-zA-Z0-9_-]+):([^,\s\]'"<>)}#]+)(?=[,\s\]'"<>)}#]|$)/g;
|
|
9
10
|
|
|
10
11
|
const AT_FILE_ATTRIBUTE_REGEX =
|
|
11
12
|
/@file:([^,\s\n\]'"<>)}#]+)(?=[,\s\n\]'"<>)}#]|$)/g
|
|
13
|
+
// TODO: try with const AT_FILE_ATTRIBUTE_REGEX = /@file:([^\s,\\\]'"<>)}#]+)(?=[\s,\\\]'"<>)}#]|$)/g;
|
|
14
|
+
|
|
15
|
+
const computePageId = (inputPath, config) => {
|
|
16
|
+
let topDir = config.dirs.content
|
|
17
|
+
if (config.dirs.content.endsWith("/")) {
|
|
18
|
+
topDir = topDir.slice(0, -1)
|
|
19
|
+
}
|
|
20
|
+
// replace top dir name by "."
|
|
21
|
+
return inputPath.replace(new RegExp("^" + topDir), ".")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const computeParentId = (inputPath, config) => {
|
|
25
|
+
const parentPath = path.dirname(inputPath)
|
|
26
|
+
if (parentPath === ".") {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
return computePageId(parentPath, config)
|
|
30
|
+
}
|
|
12
31
|
|
|
13
32
|
const findCollectionById = (collections, id) => {
|
|
14
33
|
if (id === ".") {
|
|
@@ -51,6 +70,15 @@ const getAbsoluteURL = (url = "", baseURL) => {
|
|
|
51
70
|
return new URL(url, baseURL).href
|
|
52
71
|
}
|
|
53
72
|
|
|
73
|
+
const getBuildEntries = (context, buildFlags) => {
|
|
74
|
+
const { pages } = context
|
|
75
|
+
const { buildPageIds } = buildFlags
|
|
76
|
+
if (buildPageIds?.length > 0) {
|
|
77
|
+
return buildPageIds.map((id) => [id, pages[id]])
|
|
78
|
+
}
|
|
79
|
+
return Object.entries(pages)
|
|
80
|
+
}
|
|
81
|
+
|
|
54
82
|
const getChildrenPages = (page, pages, filterOptions) => {
|
|
55
83
|
const children = page._meta.children.map((c) => pages[c])
|
|
56
84
|
if (filterOptions) {
|
|
@@ -138,6 +166,15 @@ const getLocale = (context, sep = "-") => {
|
|
|
138
166
|
return ""
|
|
139
167
|
}
|
|
140
168
|
|
|
169
|
+
const getPageFromInputPath = (inputPath, pages) => {
|
|
170
|
+
const pageValues = Object.values(pages)
|
|
171
|
+
let page = pageValues.find(
|
|
172
|
+
(p) =>
|
|
173
|
+
p._meta.inputPath === inputPath || p._meta.indexInputPath === inputPath,
|
|
174
|
+
)
|
|
175
|
+
return page
|
|
176
|
+
}
|
|
177
|
+
|
|
141
178
|
// Tries to find the page corresponding to the source
|
|
142
179
|
// @attributes should have been resolved by mow
|
|
143
180
|
// Supports absolute, and relative paths
|
|
@@ -149,7 +186,13 @@ const getPageFromSource = (source, parentPage, pages, config, options = {}) => {
|
|
|
149
186
|
const permalink = getFullPath(source, parentPage.permalink, {
|
|
150
187
|
throwIfInvalid: true,
|
|
151
188
|
})
|
|
152
|
-
const page = Object.values(pages).find(
|
|
189
|
+
const page = Object.values(pages).find(
|
|
190
|
+
(p) =>
|
|
191
|
+
p.permalink === permalink ||
|
|
192
|
+
// in incremental builds,
|
|
193
|
+
// also search in derivatives in case the image source was already replaced during previous build
|
|
194
|
+
p.derivatives?.find((d) => d.permalink === permalink),
|
|
195
|
+
)
|
|
153
196
|
if (!page) {
|
|
154
197
|
if (throwIfNotFound) {
|
|
155
198
|
throw new Error(
|
|
@@ -161,29 +204,17 @@ const getPageFromSource = (source, parentPage, pages, config, options = {}) => {
|
|
|
161
204
|
return { ...page }
|
|
162
205
|
}
|
|
163
206
|
|
|
164
|
-
const getPageId = (inputPath, config) => {
|
|
165
|
-
let topDir = config.dirs.content
|
|
166
|
-
if (config.dirs.content.endsWith("/")) {
|
|
167
|
-
topDir = topDir.slice(0, -1)
|
|
168
|
-
}
|
|
169
|
-
// replace top dir name by "."
|
|
170
|
-
return inputPath.replace(new RegExp("^" + topDir), ".")
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const getParentId = (inputPath, config) => {
|
|
174
|
-
const parentPath = path.dirname(inputPath)
|
|
175
|
-
if (parentPath === ".") {
|
|
176
|
-
return null
|
|
177
|
-
}
|
|
178
|
-
return getPageId(parentPath, config)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
207
|
// returns the parent sanitizing the data for the cascade
|
|
182
|
-
const getParentPage = (pages, id) => {
|
|
208
|
+
const getParentPage = (pages, id, isPostAsking) => {
|
|
183
209
|
let parent = pages[id]
|
|
184
210
|
if (!parent) {
|
|
185
211
|
global.logger.error(`Couldn't find parent with id '${id}'`)
|
|
186
|
-
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
if (!isPostAsking) {
|
|
215
|
+
// omit attributes that shouldn't cascade, unless the page asking is a post
|
|
216
|
+
const attributesToOmit = ["_meta.indexInputPath", "_meta.indexLoaderId"]
|
|
217
|
+
parent = _.omit(parent, attributesToOmit)
|
|
187
218
|
}
|
|
188
219
|
return omitNoCascadeAttributes(parent)
|
|
189
220
|
}
|
|
@@ -310,16 +341,18 @@ const sortPages = (pages, sortBy, { skipUndefinedSort } = {}) => {
|
|
|
310
341
|
module.exports = {
|
|
311
342
|
AT_FILE_ATTRIBUTE_REGEX,
|
|
312
343
|
AT_GENERIC_ATTRIBUTE_REGEX,
|
|
344
|
+
computePageId,
|
|
345
|
+
computeParentId,
|
|
313
346
|
findCollectionById,
|
|
314
347
|
getAbsoluteURL,
|
|
348
|
+
getBuildEntries,
|
|
315
349
|
getChildrenPages,
|
|
316
350
|
getDescendantPages,
|
|
317
351
|
getFullPath,
|
|
318
352
|
getInputPath,
|
|
319
353
|
getLocale,
|
|
354
|
+
getPageFromInputPath,
|
|
320
355
|
getPageFromSource,
|
|
321
|
-
getPageId,
|
|
322
|
-
getParentId,
|
|
323
356
|
getParentPage,
|
|
324
357
|
isChild,
|
|
325
358
|
isValidURL,
|
|
@@ -333,11 +366,11 @@ module.exports = {
|
|
|
333
366
|
|
|
334
367
|
// removing any key ending with _no_cascade
|
|
335
368
|
const omitNoCascadeAttributes = (obj) => {
|
|
336
|
-
let result = {}
|
|
369
|
+
let result = _.isArray(obj) ? [] : {}
|
|
337
370
|
_.forEach(obj, (value, key) => {
|
|
338
|
-
if (_.isPlainObject(value)) {
|
|
371
|
+
if (_.isPlainObject(value) || _.isArray(value)) {
|
|
339
372
|
result[key] = omitNoCascadeAttributes(value)
|
|
340
|
-
} else if (!key.endsWith("_no_cascade")) {
|
|
373
|
+
} else if (typeof key === "number" || !key.endsWith("_no_cascade")) {
|
|
341
374
|
result[key] = value
|
|
342
375
|
}
|
|
343
376
|
})
|
package/src/libs/loadNunjucks.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
const nunjucks = require("nunjucks")
|
|
2
2
|
|
|
3
|
-
const loadNunjucks = (_, config) => {
|
|
3
|
+
const loadNunjucks = (_, config, data, buildFlags = {}) => {
|
|
4
|
+
const options = {
|
|
5
|
+
// we need to watch them so that template can be reloaded when in incremental mode
|
|
6
|
+
// (in normal mode, config is reloaded which reloads the lib and clears the cache)
|
|
7
|
+
watch: !!buildFlags.incremental,
|
|
8
|
+
}
|
|
4
9
|
// set default nunjucks template dir
|
|
5
|
-
config.libs.nunjucks = nunjucks.configure(config.dirs.template)
|
|
10
|
+
config.libs.nunjucks = nunjucks.configure(config.dirs.template, options)
|
|
6
11
|
// clear nunjucks caches (for when in watch mode)
|
|
7
12
|
// https://risanb.com/code/how-to-clear-nunjucks-cache/
|
|
8
13
|
config.libs.nunjucks.loaders.map((loader) => (loader.cache = {}))
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
const path = require("path")
|
|
2
2
|
const _ = require("lodash")
|
|
3
3
|
|
|
4
|
-
const {
|
|
4
|
+
const { computePageId, computeParentId, getParentPage } = require("../helpers")
|
|
5
5
|
|
|
6
|
-
const baseLoader = (inputPath, options, page, pages, config) => {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const baseLoader = (inputPath, options = {}, page = {}, pages, config) => {
|
|
7
|
+
const { file, buildVersion } = options
|
|
8
|
+
const inputPathObject = path.parse(inputPath)
|
|
9
|
+
let parentId = computeParentId(inputPath, config)
|
|
10
|
+
let basename = inputPathObject.base
|
|
9
11
|
if (basename === config.dirs.content) {
|
|
10
12
|
basename = ""
|
|
11
13
|
}
|
|
12
|
-
const parentData = parentId
|
|
14
|
+
const parentData = parentId
|
|
15
|
+
? getParentPage(pages, parentId, inputPathObject.name === "post")
|
|
16
|
+
: {}
|
|
13
17
|
let isDirectory = options.isDirectory || inputPath.endsWith("/")
|
|
14
|
-
let id = options.id ||
|
|
18
|
+
let id = options.id || computePageId(inputPath, config)
|
|
15
19
|
let outputType = options.outputType || "HTML"
|
|
16
|
-
let collectionGroup = options.collectionGroup
|
|
20
|
+
let collectionGroup = options.collectionGroup || "directory"
|
|
17
21
|
if (outputType === "HTML") {
|
|
18
22
|
// only files converted as HTML can override
|
|
19
23
|
// they parent entry
|
|
@@ -48,6 +52,20 @@ const baseLoader = (inputPath, options, page, pages, config) => {
|
|
|
48
52
|
outputType,
|
|
49
53
|
collectionGroup,
|
|
50
54
|
})
|
|
55
|
+
if (inputPathObject.name === "index") {
|
|
56
|
+
// save the input path in indexInputPath in case it gets overwritten later on by a post.* file
|
|
57
|
+
page._meta.indexInputPath = inputPath
|
|
58
|
+
// also need to save the loader index
|
|
59
|
+
page._meta.indexLoaderId = file?.loaderId
|
|
60
|
+
}
|
|
61
|
+
if (file) {
|
|
62
|
+
page._meta.fileCreated = file.stats.ctime
|
|
63
|
+
page._meta.fileModified = file.stats.mtime
|
|
64
|
+
page._meta.loaderId = file.loaderId
|
|
65
|
+
}
|
|
66
|
+
if (buildVersion !== undefined) {
|
|
67
|
+
page._meta.buildVersion = buildVersion
|
|
68
|
+
}
|
|
51
69
|
return page
|
|
52
70
|
}
|
|
53
71
|
|
package/src/loaders/jsLoader.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require("path")
|
|
2
2
|
|
|
3
|
-
const jsLoader = (
|
|
4
|
-
const filePath = path.resolve(
|
|
3
|
+
const jsLoader = (inputPath, options, page) => {
|
|
4
|
+
const filePath = path.resolve(inputPath)
|
|
5
5
|
const fileData = require(filePath)
|
|
6
6
|
return { ...page, ...fileData }
|
|
7
7
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const { parseISO } = require("date-fns")
|
|
2
2
|
const { readJsonSync } = require("fs-extra")
|
|
3
3
|
|
|
4
|
-
const jsonLoader = (
|
|
5
|
-
let fileData = readJsonSync(
|
|
4
|
+
const jsonLoader = (inputPath, options, page, pages, config) => {
|
|
5
|
+
let fileData = readJsonSync(inputPath)
|
|
6
6
|
const published = fileData[config.defaults.pagePublishedAttribute]
|
|
7
7
|
const updated = fileData[config.defaults.pageUpdatedAttribute]
|
|
8
8
|
if (published && typeof published === "string") {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const textLoader = require("./textLoader")
|
|
2
2
|
|
|
3
|
-
const markdownLoader = (
|
|
4
|
-
const fileData = textLoader(
|
|
3
|
+
const markdownLoader = (inputPath, options, page, _, config) => {
|
|
4
|
+
const fileData = textLoader(inputPath, options, page)
|
|
5
5
|
const content = config.libs.marked(fileData.content)
|
|
6
6
|
return { ...page, ...fileData, content }
|
|
7
7
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const frontMatter = require("front-matter")
|
|
2
2
|
const { readFileSync } = require("fs-extra")
|
|
3
3
|
|
|
4
|
-
const textLoader = (
|
|
5
|
-
const fileContent = readFileSync(
|
|
4
|
+
const textLoader = (inputPath, _, page) => {
|
|
5
|
+
const fileContent = readFileSync(inputPath, "utf8")
|
|
6
6
|
const fileData = frontMatter(fileContent)
|
|
7
7
|
const content = fileData.body
|
|
8
8
|
return { ...page, ...fileData.attributes, content }
|
|
@@ -3,7 +3,7 @@ const cheerio = require("cheerio")
|
|
|
3
3
|
const path = require("path")
|
|
4
4
|
const sharp = require("sharp")
|
|
5
5
|
|
|
6
|
-
const { getPageFromSource, isValidURL } = require("../helpers")
|
|
6
|
+
const { getBuildEntries, getPageFromSource, isValidURL } = require("../helpers")
|
|
7
7
|
|
|
8
8
|
const META_SELECTORS = [
|
|
9
9
|
"meta[property='og:image']",
|
|
@@ -11,8 +11,10 @@ const META_SELECTORS = [
|
|
|
11
11
|
"meta[property='twitter:image']",
|
|
12
12
|
]
|
|
13
13
|
|
|
14
|
-
const imageContextTransform = async (context, options, config) => {
|
|
15
|
-
|
|
14
|
+
const imageContextTransform = async (context, options, config, buildFlags) => {
|
|
15
|
+
const entries = getBuildEntries(context, buildFlags)
|
|
16
|
+
|
|
17
|
+
for (let [id, page] of entries) {
|
|
16
18
|
if (!page._html || !page.permalink) {
|
|
17
19
|
continue
|
|
18
20
|
}
|
|
@@ -253,14 +255,16 @@ const transformImageTag = async ($, img, page, context, options, config) => {
|
|
|
253
255
|
global.logger.warn(`Page '${id}': image '${src}' has no 'alt' attribute.`)
|
|
254
256
|
}
|
|
255
257
|
if (isValidURL(src)) {
|
|
256
|
-
global.logger.log(
|
|
258
|
+
global.logger.log(
|
|
259
|
+
`- [transformImageTag] Page '${id}': image '${src}' is a URL. Skipping.`,
|
|
260
|
+
)
|
|
257
261
|
return context
|
|
258
262
|
}
|
|
259
263
|
const imgPage = getPageFromSource(src, page, context.pages, config)
|
|
260
264
|
if (imgPage._meta.outputType !== "IMAGE") {
|
|
261
265
|
// image is handled by another loader. skipping.
|
|
262
266
|
global.logger.log(
|
|
263
|
-
|
|
267
|
+
`- [transformImageTag] Page '${id}': image '${src}' is handled by another loader. Skipping.`,
|
|
264
268
|
)
|
|
265
269
|
return context
|
|
266
270
|
}
|
|
@@ -304,7 +308,7 @@ const transformMetaTag = async (
|
|
|
304
308
|
if (imgPage._meta.outputType !== "IMAGE") {
|
|
305
309
|
// image is handled by another loader. skipping.
|
|
306
310
|
global.logger.log(
|
|
307
|
-
|
|
311
|
+
`- [transformMetaTag] Page '${id}': image '${url.pathname}' is handled by another loader. Skipping.`,
|
|
308
312
|
)
|
|
309
313
|
return context
|
|
310
314
|
}
|
|
@@ -41,7 +41,11 @@ module.exports = computeCollectionDataView
|
|
|
41
41
|
|
|
42
42
|
const getAllPosts = (page, pages, config) =>
|
|
43
43
|
getDescendantPages(page, pages, {
|
|
44
|
-
filterBy: (p) =>
|
|
44
|
+
filterBy: (p) =>
|
|
45
|
+
p._meta.isPost &&
|
|
46
|
+
p.permalink &&
|
|
47
|
+
!p.excludeFromCollection &&
|
|
48
|
+
!p.excludeFromWrite,
|
|
45
49
|
sortBy: page.sortCollectionBy || config.defaults.sortCollectionBy,
|
|
46
50
|
skipUndefinedSort: true,
|
|
47
51
|
})
|
|
@@ -57,7 +57,7 @@ const pageObject = (page, options, config) => {
|
|
|
57
57
|
publishedDate = formatRFC3339(publishedDate)
|
|
58
58
|
} catch (err) {
|
|
59
59
|
global.logger.warn(
|
|
60
|
-
`[
|
|
60
|
+
`[rssContextWriter]: cannot format published date '${publishedDate}' to RFC3339 for page '${page.permalink}': ${err}.`,
|
|
61
61
|
)
|
|
62
62
|
publishedDate = null
|
|
63
63
|
}
|
|
@@ -68,7 +68,7 @@ const pageObject = (page, options, config) => {
|
|
|
68
68
|
updatedDate = formatRFC3339(updatedDate)
|
|
69
69
|
} catch (err) {
|
|
70
70
|
global.logger.warn(
|
|
71
|
-
`[
|
|
71
|
+
`[rssContextWriter]: cannot format updated date '${updatedDate}' to RFC3339 for page '${page.permalink}': ${err}.`,
|
|
72
72
|
)
|
|
73
73
|
updatedDate = null
|
|
74
74
|
}
|