@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slybridges/kiss",
3
- "version": "0.8.5",
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.2",
15
+ "browser-sync": "^3.0.3",
16
16
  "chalk": "^4.1.2",
17
- "cheerio": "^1.0.0-rc.12",
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.3",
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.5.0",
33
- "eslint": "^9.5.0",
32
+ "@eslint/js": "^9.11.1",
33
+ "eslint": "^9.11.1",
34
34
  "eslint-config-prettier": "^9.1.0",
35
- "globals": "^15.6.0",
36
- "prettier": "^3.2.5"
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 { getParentId, relativeToAbsoluteAttributes } = require("./helpers")
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 build = async (options = {}, config = null) => {
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 context = {
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
- if (config.hooks.loadLibs.length > 0) {
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 (config.hooks.preLoad.length > 0) {
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
- global.logger.section(
40
- `Loading content from '${config.dirs.content}' directory`,
41
- )
42
- context.pages = await loadContent(config, context)
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 (config.hooks.postLoad.length > 0) {
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
- global.logger.section("Computing dynamic page context")
50
- context = computeAllPagesData(context, config)
94
+ if (buildFlags.dynamicData) {
95
+ global.logger.section("Computing dynamic page context")
96
+ context = computeAllPagesData(context, config, buildFlags)
97
+ }
51
98
 
52
- global.logger.section("Computing data views")
53
- context = computeDataViews(context, config)
99
+ if (buildFlags.dataViews) {
100
+ global.logger.section("Computing data views")
101
+ context = computeDataViews(context, config)
102
+ }
54
103
 
55
- global.logger.section(`Applying transforms`)
56
- context = await applyTransforms(context, config)
104
+ if (buildFlags.transform) {
105
+ global.logger.section(`Applying transforms`)
106
+ context = await applyTransforms(context, config, buildFlags)
107
+ }
57
108
 
58
- global.logger.section(`Writing site to '${config.dirs.public}' directory`)
59
- await writeStaticSite(context, config)
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 (config.hooks.postWrite.length > 0) {
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(`Static build completed!`)
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
- // page transforms
131
- const message =
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
- for (let [id, page] of Object.entries(context.pages)) {
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: null,
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
- //_.forEach(data, (value, key) => {
190
- if (typeof key === "string" && key.endsWith("_no_cascade")) {
191
- // this is an override key, computing original key value
192
- key = key.split("_no_cascade")[0]
193
- } else if (
194
- Object.prototype.hasOwnProperty.call(data, key + "_no_cascade")
195
- ) {
196
- // there is a data override attribute, bail out.
197
- continue
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(value, config, context, options)
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
- _.forEach(context.pages, (page, key) => {
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
- global.logger.error(
253
- ```Could not compute all data in ${config.defaults.maxComputingRounds} rounds. Check for circular
254
- dependencies or increase the 'maxComputingRounds' value.```,
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 = (pathname, options, pages, config) => {
314
- const parentName = path.dirname(pathname)
315
- const parentNameId = getParentId(pathname, config)
316
- if (!parentNameId || pages[parentNameId]) {
317
- // parent already in the collection
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(parentName)
322
- if (parentBasename !== parentName) {
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(parentName, options, pages, config)
541
+ pages = directoryCollectionLoader(
542
+ parentPath,
543
+ options,
544
+ pages,
545
+ config,
546
+ buildFlags,
547
+ )
328
548
  }
329
549
  let parent = baseLoader(
330
- parentName,
331
- { isDirectory: true, collectionGroup: "directory" },
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
- [parentNameId]: parent,
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
- let pages = {}
615
+ const loadContent = async (config, context, buildFlags) => {
616
+ const { incremental, buildPageIds } = buildFlags
617
+ let pages = context.pages
353
618
  let files = []
354
- // We first need to fetch all files that match all loaders
355
- // So that we can sort them in the right order before loading them
356
- // This is essential for the data cascade to work properly
357
- config.loaders.forEach(
358
- ({ handler, namespace, source, ...loaderOptions }, idx) => {
359
- if (source && source !== "file") {
360
- return
361
- }
362
- const allOptions = getOptions(config, namespace, loaderOptions)
363
- const { match, matchOptions = {}, ...options } = allOptions
364
- if ("active" in options && !options.active) {
365
- global.logger.log(`[${handler.name}]: loader not active. Skipping.`)
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
- let fgOptions = {
369
- cwd: config.dirs.content,
370
- markDirectories: true,
371
- stats: true,
372
- ...matchOptions,
373
- }
374
- let extraOptions = []
375
- if (namespace !== handler.name) {
376
- extraOptions.push(`namespace: '${namespace}'`)
377
- }
378
- if (loaderOptions && Object.keys(loaderOptions).length > 0) {
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
- fg.sync(match, fgOptions).map((file) => ({ ...file, loaderIdx: idx })),
647
+ getFiles(page._meta.loaderId, config, page._meta.inputPath, true),
386
648
  )
387
- },
388
- )
389
- global.logger.info(
390
- `Found ${files.length} files. Sorting them for the data cascade.`,
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.loaderIdx]
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
- // load parent folders, if any
406
- pages = directoryCollectionLoader(pathname, options, pages, config)
407
- // load stats
408
- page = {
409
- _meta: {
410
- fileCreated: file.stats.ctime,
411
- fileModified: file.stats.mtime,
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(pathname, options, page, pages, config)
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(pathname, options, page, context, config)
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 '${page._meta.inputPath}'`)
715
+ global.logger.log(`- [${handler.name}] loaded '${file.path}'`)
422
716
  } catch (err) {
423
717
  global.logger.error(
424
- `- [${handler.name}] Error loading '${_.get(
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
- // computed loaders
433
- _.filter(config.loaders, (loader) => loader.source === "computed").forEach(
434
- (loader) => {
435
- const { description, handler, ...options } = loader
436
- const message =
437
- description || `Loading computed pages using ${handler.name}`
438
- global.logger.info(message)
439
- pages = handler(pages, options, config)
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
- global.logger.info("Writing individual pages and images")
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
- _.map(context.pages, async (page) => {
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({ slug, _meta }, config, { pages })
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
- // rebuild on file changes
55
- fileWatcher.on(
56
- "all",
57
- _.debounce((event, file) => rebuild(options, event, file), 500),
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
- process.exit(code)
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
- console.info(`\nChange detected: [${event}] ${file}`)
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((p) => p.permalink === permalink)
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
- console.log(Object.keys(pages))
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
  })
@@ -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 { getPageId, getParentId, getParentPage } = require("../helpers")
4
+ const { computePageId, computeParentId, getParentPage } = require("../helpers")
5
5
 
6
- const baseLoader = (inputPath, options, page, pages, config) => {
7
- let parentId = getParentId(inputPath, config)
8
- let basename = path.basename(inputPath)
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 ? getParentPage(pages, 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 || getPageId(inputPath, config)
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
 
@@ -1,7 +1,7 @@
1
1
  const path = require("path")
2
2
 
3
- const jsLoader = (id, options, page) => {
4
- const filePath = path.resolve(page._meta.inputPath)
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 = (path, options, page, pages, config) => {
5
- let fileData = readJsonSync(path)
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 = (id, options, page, _, config) => {
4
- const fileData = textLoader(id, options, page)
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,6 +1,6 @@
1
1
  const _ = require("lodash")
2
2
 
3
- const staticLoader = (id, options, { permalink, _meta }) => {
3
+ const staticLoader = (inputPath, options, { permalink, _meta }) => {
4
4
  return {
5
5
  permalink,
6
6
  _meta: {
@@ -1,8 +1,8 @@
1
1
  const frontMatter = require("front-matter")
2
2
  const { readFileSync } = require("fs-extra")
3
3
 
4
- const textLoader = (id, _, page) => {
5
- const fileContent = readFileSync(page._meta.inputPath, "utf8")
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
- for (let [id, page] of Object.entries(context.pages)) {
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(`Page '${id}': image '${src}' is a URL. Skipping.`)
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
- `Page '${id}': image '${src}' is handled by another loader. Skipping.`,
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
- `Page '${id}': image '${url.pathname}' is handled by another loader. Skipping.`,
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) => p._meta.isPost && p.permalink && !p.excludeFromCollection,
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
- `[${this.name}]: cannot format published date '${publishedDate}' to RFC3339 for page '${page.permalink}': ${err}.`,
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
- `[${this.name}]: cannot format updated date '${updatedDate}' to RFC3339 for page '${page.permalink}': ${err}.`,
71
+ `[rssContextWriter]: cannot format updated date '${updatedDate}' to RFC3339 for page '${page.permalink}': ${err}.`,
72
72
  )
73
73
  updatedDate = null
74
74
  }