poops 1.2.3 → 1.3.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/README.md +395 -70
- package/lib/copy.js +5 -3
- package/lib/images.js +57 -0
- package/lib/markup/collections.js +39 -17
- package/lib/markup/engines/liquid.js +34 -33
- package/lib/markup/engines/nunjucks.js +44 -36
- package/lib/markup/helpers.js +164 -37
- package/lib/markup/image-cache.js +114 -0
- package/lib/markup/indexer.js +158 -2
- package/lib/markup/renderer.js +60 -0
- package/lib/markups.js +199 -49
- package/lib/postcss.js +16 -30
- package/lib/reactor.js +11 -29
- package/lib/scripts.js +37 -34
- package/lib/styles.js +17 -31
- package/lib/utils/helpers.js +20 -16
- package/lib/utils/log.js +15 -1
- package/lib/utils/minify.js +41 -0
- package/package.json +7 -3
- package/poops.js +120 -32
package/lib/markups.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { pathExists, pathIsDirectory, readDataFile, mkDir, buildTime } from './utils/helpers.js'
|
|
2
|
-
import { replaceOutExtensions, getRelativePathPrefix,
|
|
1
|
+
import { pathExists, pathIsDirectory, readDataFile, mkDir, buildTime, toPosix } from './utils/helpers.js'
|
|
2
|
+
import { replaceOutExtensions, getRelativePathPrefix, getPageUrlRelativeToOutput, parseFrontMatter, clearFrontMatterCache, wordcount } from './markup/helpers.js'
|
|
3
3
|
import { collectionAutoDiscovery, getCollectionDataBasedOnConfig, buildCollectionPaginationData, generateCollectionPaginationPages } from './markup/collections.js'
|
|
4
|
-
import { generateIndexFiles } from './markup/indexer.js'
|
|
4
|
+
import { generateIndexFiles, buildNavTree } from './markup/indexer.js'
|
|
5
5
|
import NunjucksEngine from './markup/engines/nunjucks.js'
|
|
6
6
|
import LiquidEngine from './markup/engines/liquid.js'
|
|
7
7
|
import fs from 'node:fs'
|
|
8
8
|
import { globSync } from 'glob'
|
|
9
9
|
import path from 'node:path'
|
|
10
|
+
import { createRequire } from 'node:module'
|
|
11
|
+
import { pathToFileURL } from 'node:url'
|
|
10
12
|
import log from './utils/log.js'
|
|
11
13
|
|
|
12
14
|
const ENGINES = {
|
|
@@ -22,8 +24,8 @@ export default class Markups {
|
|
|
22
24
|
if (!moduleConfig || !moduleConfig.in) return
|
|
23
25
|
if (!moduleConfig.options) moduleConfig.options = {}
|
|
24
26
|
|
|
25
|
-
// Determine engine
|
|
26
|
-
|
|
27
|
+
// Determine engine — resolved in init(), builtin name or importable module
|
|
28
|
+
this.engineName = moduleConfig.engine || moduleConfig.options.engine || 'nunjucks'
|
|
27
29
|
this.logTag = 'markup'
|
|
28
30
|
|
|
29
31
|
// Normalize config
|
|
@@ -35,23 +37,54 @@ export default class Markups {
|
|
|
35
37
|
this.includePaths = moduleConfig.includePaths || moduleConfig.options.includePaths || []
|
|
36
38
|
this.searchIndexConfig = moduleConfig.options.searchIndex || moduleConfig.searchIndex
|
|
37
39
|
this.sitemapConfig = moduleConfig.options.sitemap || moduleConfig.sitemap
|
|
40
|
+
this.navConfig = moduleConfig.options.nav || moduleConfig.nav
|
|
38
41
|
this.baseURL = moduleConfig.baseURL || moduleConfig.options.baseURL || null
|
|
39
|
-
this.dataFiles = []
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
this.autoescape = moduleConfig.autoescape || moduleConfig.options.autoescape || false
|
|
44
|
+
this.dataConfig = moduleConfig.data || moduleConfig.options.data
|
|
45
|
+
|
|
46
|
+
if (!moduleConfig.out) {
|
|
47
|
+
moduleConfig.out = this.markupOut
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Engine instantiation is async because non-builtin engines are loaded via
|
|
52
|
+
// dynamic import. Idempotent; compile() calls it lazily, so a failed engine
|
|
53
|
+
// resolution logs once per compile and the module stays inert.
|
|
54
|
+
async init() {
|
|
55
|
+
if (!this.markupIn || this.engine) return
|
|
56
|
+
|
|
57
|
+
let EngineClass = ENGINES[this.engineName]
|
|
43
58
|
if (!EngineClass) {
|
|
44
|
-
|
|
59
|
+
try {
|
|
60
|
+
// Relative/absolute specifiers resolve against cwd (the user's
|
|
61
|
+
// project), bare specifiers against the module graph (node_modules)
|
|
62
|
+
const spec = /^[./]/.test(this.engineName) || path.isAbsolute(this.engineName)
|
|
63
|
+
? pathToFileURL(path.resolve(process.cwd(), this.engineName)).href
|
|
64
|
+
: this.engineName
|
|
65
|
+
EngineClass = (await import(spec)).default
|
|
66
|
+
} catch {
|
|
67
|
+
// Bare specifier not reachable from poops's own module graph (e.g.
|
|
68
|
+
// poops installed via a file: link) — resolve from the user's project
|
|
69
|
+
try {
|
|
70
|
+
const projectRequire = createRequire(path.join(process.cwd(), 'package.json'))
|
|
71
|
+
EngineClass = (await import(pathToFileURL(projectRequire.resolve(this.engineName)).href)).default
|
|
72
|
+
} catch { /* handled below */ }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (typeof EngineClass !== 'function') {
|
|
77
|
+
log({ tag: 'error', text: `Unknown markup engine: ${this.engineName}` })
|
|
45
78
|
return
|
|
46
79
|
}
|
|
47
80
|
|
|
48
|
-
const templatesDir = path.
|
|
81
|
+
const templatesDir = path.resolve(process.cwd(), this.markupIn)
|
|
49
82
|
this.engine = new EngineClass(templatesDir, this.includePaths, {
|
|
50
|
-
autoescape:
|
|
83
|
+
autoescape: this.autoescape
|
|
51
84
|
})
|
|
52
85
|
|
|
53
86
|
this.engine.registerFilters({ timeDateFormat: this.timeDateFormat, markupOut: this.markupOut })
|
|
54
|
-
this.engine.registerTags(() => path.
|
|
87
|
+
this.engine.registerTags(() => path.resolve(process.cwd(), this.markupOut))
|
|
55
88
|
|
|
56
89
|
// Load global variables
|
|
57
90
|
const pkgPath = path.join(process.cwd(), 'package.json')
|
|
@@ -71,12 +104,7 @@ export default class Markups {
|
|
|
71
104
|
}
|
|
72
105
|
}
|
|
73
106
|
|
|
74
|
-
|
|
75
|
-
this.loadDataFiles(data)
|
|
76
|
-
|
|
77
|
-
if (!moduleConfig.out) {
|
|
78
|
-
moduleConfig.out = this.markupOut
|
|
79
|
-
}
|
|
107
|
+
this.loadDataFiles(this.dataConfig)
|
|
80
108
|
}
|
|
81
109
|
|
|
82
110
|
loadDataFiles(files) {
|
|
@@ -90,36 +118,79 @@ export default class Markups {
|
|
|
90
118
|
const dataDir = pathIsDirectory(this.markupIn) ? this.markupIn : path.dirname(this.markupIn)
|
|
91
119
|
const resolved = []
|
|
92
120
|
for (const file of files) {
|
|
93
|
-
const fullPath = path.
|
|
121
|
+
const fullPath = path.resolve(process.cwd(), dataDir, file)
|
|
94
122
|
if (pathIsDirectory(fullPath)) {
|
|
95
|
-
const dirFiles = globSync(path.join(fullPath, '**/*.+(json|yml|yaml)'))
|
|
123
|
+
const dirFiles = globSync(toPosix(path.join(fullPath, '**/*.+(json|yml|yaml)')))
|
|
96
124
|
for (const f of dirFiles) {
|
|
97
|
-
resolved.push(path.relative(path.
|
|
125
|
+
resolved.push(path.relative(path.resolve(process.cwd(), dataDir), f))
|
|
98
126
|
}
|
|
99
127
|
} else {
|
|
100
128
|
resolved.push(file)
|
|
101
129
|
}
|
|
102
130
|
}
|
|
103
131
|
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
const loadedKeys = new Set()
|
|
106
133
|
for (const dataFile of resolved) {
|
|
107
134
|
try {
|
|
108
|
-
const data = readDataFile(path.
|
|
135
|
+
const data = readDataFile(path.resolve(process.cwd(), dataDir, dataFile))
|
|
109
136
|
const globalKeyName = path.basename(dataFile, path.extname(dataFile)).replace(/[.\-\s]/g, '_')
|
|
110
137
|
this.engine.setGlobal(globalKeyName, data)
|
|
138
|
+
loadedKeys.add(globalKeyName)
|
|
111
139
|
} catch (err) {
|
|
112
140
|
log({ tag: 'error', text: 'Data file not found:', link: dataFile })
|
|
113
141
|
continue
|
|
114
142
|
}
|
|
115
143
|
}
|
|
144
|
+
|
|
145
|
+
// A deleted data file must also drop its global, or stale data renders forever
|
|
146
|
+
if (this.dataGlobalKeys) {
|
|
147
|
+
for (const key of this.dataGlobalKeys) {
|
|
148
|
+
if (!loadedKeys.has(key)) this.engine.removeGlobal(key)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this.dataGlobalKeys = loadedKeys
|
|
116
152
|
}
|
|
117
153
|
|
|
118
154
|
reloadDataFiles() {
|
|
119
|
-
this.loadDataFiles(this.
|
|
155
|
+
if (this.engine) this.loadDataFiles(this.dataConfig)
|
|
120
156
|
return Promise.resolve()
|
|
121
157
|
}
|
|
122
158
|
|
|
159
|
+
// Engines can override output extension mapping (e.g. a template format
|
|
160
|
+
// whose extension isn't in the default map); falls back to the shared helper
|
|
161
|
+
mapOutputPath(outputPath) {
|
|
162
|
+
if (this.engine && typeof this.engine.replaceOutExtensions === 'function') {
|
|
163
|
+
return this.engine.replaceOutExtensions(outputPath)
|
|
164
|
+
}
|
|
165
|
+
return replaceOutExtensions(outputPath)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Maps a deleted source path to its build output and removes it.
|
|
169
|
+
// compile() only globs existing sources, so it can never clean up
|
|
170
|
+
// after a deletion — this is the only place stale output gets removed.
|
|
171
|
+
removeOutput(sourcePath) {
|
|
172
|
+
if (!this.markupIn) return
|
|
173
|
+
const markupIn = path.resolve(process.cwd(), this.markupIn)
|
|
174
|
+
const rel = path.relative(markupIn, path.resolve(process.cwd(), sourcePath))
|
|
175
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return
|
|
176
|
+
|
|
177
|
+
const mapped = path.resolve(process.cwd(), this.markupOut, rel)
|
|
178
|
+
|
|
179
|
+
// rel === '' with a directory output would be the whole out dir —
|
|
180
|
+
// never remove that, it also holds css/js from other modules
|
|
181
|
+
if (rel !== '' && fs.existsSync(mapped) && fs.statSync(mapped).isDirectory()) {
|
|
182
|
+
fs.rmSync(mapped, { recursive: true })
|
|
183
|
+
log({ tag: this.logTag, text: 'Removed:', link: path.relative(process.cwd(), mapped) })
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const outFile = this.mapOutputPath(mapped)
|
|
188
|
+
if (fs.existsSync(outFile) && !fs.statSync(outFile).isDirectory()) {
|
|
189
|
+
fs.unlinkSync(outFile)
|
|
190
|
+
log({ tag: this.logTag, text: 'Removed:', link: path.relative(process.cwd(), outFile) })
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
123
194
|
generateMarkupGlobPattern(excludes) {
|
|
124
195
|
let markupDefaultExcludes = ['node_modules', '.git', '.svn', '.hg']
|
|
125
196
|
|
|
@@ -170,12 +241,72 @@ export default class Markups {
|
|
|
170
241
|
return { result, frontMatter }
|
|
171
242
|
}
|
|
172
243
|
|
|
244
|
+
getMarkupFiles(markupIn) {
|
|
245
|
+
// glob patterns must use `/` — on Windows `\` is an escape character
|
|
246
|
+
return [
|
|
247
|
+
...globSync(toPosix(path.join(markupIn, this.generateMarkupGlobPattern(this.includePaths)))),
|
|
248
|
+
...globSync(toPosix(path.join(markupIn, `*.+(${this.engine.markupExtensions})`)))
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// True for collection index templates that compileDirectory skips —
|
|
253
|
+
// pagination renders them instead
|
|
254
|
+
isCollectionIndexOverride(relativePathParts, collectionData) {
|
|
255
|
+
return relativePathParts.length > 1 &&
|
|
256
|
+
collectionData[relativePathParts[0]] &&
|
|
257
|
+
relativePathParts[1].startsWith('index.') &&
|
|
258
|
+
this.engine.indexableExtensions.has(path.extname(relativePathParts[1])) &&
|
|
259
|
+
collectionData[relativePathParts[0]].items.length > 0
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Pre-pass: builds the nav tree from front matter alone (no rendering) and
|
|
263
|
+
// exposes it as the `nav` template global, so sidebars can render during the
|
|
264
|
+
// same compile instead of reading the previous build's nav.json.
|
|
265
|
+
// parseFrontMatter caches by mtime, so the compile pass re-reads nothing.
|
|
266
|
+
buildNavGlobal(markupIn, collectionData) {
|
|
267
|
+
if (!this.navConfig) return
|
|
268
|
+
|
|
269
|
+
const entries = []
|
|
270
|
+
for (const collectionName of Object.keys(collectionData)) {
|
|
271
|
+
entries.push({ url: collectionName, title: collectionName, isIndex: true })
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const files = pathIsDirectory(markupIn) ? this.getMarkupFiles(markupIn) : [markupIn]
|
|
275
|
+
for (const file of files) {
|
|
276
|
+
if (!this.engine.indexableExtensions.has(path.extname(file))) continue
|
|
277
|
+
const relativePath = path.relative(markupIn, file)
|
|
278
|
+
const relativePathParts = relativePath.split(path.sep)
|
|
279
|
+
if (this.isCollectionIndexOverride(relativePathParts, collectionData)) continue
|
|
280
|
+
|
|
281
|
+
let frontMatter
|
|
282
|
+
try {
|
|
283
|
+
frontMatter = parseFrontMatter(file).frontMatter
|
|
284
|
+
} catch (err) {
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
if (frontMatter.published === false) continue
|
|
288
|
+
if (!frontMatter.title) frontMatter.title = path.basename(file, path.extname(file))
|
|
289
|
+
|
|
290
|
+
const fileCollection = relativePathParts.length > 1 && collectionData[relativePathParts[0]]
|
|
291
|
+
? relativePathParts[0]
|
|
292
|
+
: null
|
|
293
|
+
if (fileCollection && !frontMatter.collection) frontMatter.collection = fileCollection
|
|
294
|
+
|
|
295
|
+
const outPath = this.mapOutputPath(path.resolve(process.cwd(), this.markupOut, relativePath))
|
|
296
|
+
entries.push({
|
|
297
|
+
...frontMatter,
|
|
298
|
+
url: getPageUrlRelativeToOutput(outPath, this.markupOut),
|
|
299
|
+
isIndex: false
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const navOptions = typeof this.navConfig === 'object' ? this.navConfig : {}
|
|
304
|
+
this.engine.setGlobal('nav', buildNavTree(entries, navOptions))
|
|
305
|
+
}
|
|
306
|
+
|
|
173
307
|
async compileDirectory(markupIn, collectionData, pageEntries) {
|
|
174
308
|
const markupStart = performance.now()
|
|
175
|
-
const markupFiles =
|
|
176
|
-
...globSync(path.join(markupIn, this.generateMarkupGlobPattern(this.includePaths))),
|
|
177
|
-
...globSync(path.join(markupIn, `*.+(${this.engine.markupExtensions})`))
|
|
178
|
-
]
|
|
309
|
+
const markupFiles = this.getMarkupFiles(markupIn)
|
|
179
310
|
const compilePromises = []
|
|
180
311
|
const indexableExtensions = this.engine.indexableExtensions
|
|
181
312
|
|
|
@@ -183,15 +314,14 @@ export default class Markups {
|
|
|
183
314
|
const relativePath = path.relative(markupIn, file)
|
|
184
315
|
const relativePathParts = relativePath.split(path.sep)
|
|
185
316
|
|
|
186
|
-
if (relativePathParts
|
|
187
|
-
collectionData[relativePathParts[0]] &&
|
|
188
|
-
relativePathParts[1].startsWith('index.') && indexableExtensions.has(path.extname(relativePathParts[1])) &&
|
|
189
|
-
collectionData[relativePathParts[0]].items.length > 0) {
|
|
317
|
+
if (this.isCollectionIndexOverride(relativePathParts, collectionData)) {
|
|
190
318
|
continue
|
|
191
319
|
}
|
|
192
320
|
|
|
193
|
-
|
|
194
|
-
|
|
321
|
+
// Map before deriving dir/prefix: engines may relocate output (e.g.
|
|
322
|
+
// Shopify's templates/ flattening), and links must match the final path.
|
|
323
|
+
const markupOut = this.mapOutputPath(path.resolve(process.cwd(), this.markupOut, relativePath))
|
|
324
|
+
const fromPath = path.resolve(process.cwd(), this.markupOut)
|
|
195
325
|
const markupOutDir = path.dirname(markupOut)
|
|
196
326
|
|
|
197
327
|
mkDir(markupOutDir)
|
|
@@ -199,7 +329,9 @@ export default class Markups {
|
|
|
199
329
|
const fileContext = {
|
|
200
330
|
...collectionData,
|
|
201
331
|
relativePathPrefix: getRelativePathPrefix(markupOutDir, fromPath, this.baseURL),
|
|
202
|
-
|
|
332
|
+
// output-relative so page.url matches nav.json urls (getPageUrlRelativeToOutput),
|
|
333
|
+
// otherwise `item.url == page.url` sidebar/active checks never fire
|
|
334
|
+
_url: getPageUrlRelativeToOutput(markupOut, this.markupOut)
|
|
203
335
|
}
|
|
204
336
|
|
|
205
337
|
const shouldIndex = pageEntries && indexableExtensions.has(path.extname(file))
|
|
@@ -209,14 +341,12 @@ export default class Markups {
|
|
|
209
341
|
|
|
210
342
|
const compilePromise = this.compileEntry(file, fileContext).then(({ result, frontMatter, skipped }) => {
|
|
211
343
|
if (skipped) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
log({ tag: this.logTag, text: 'Removed unpublished:', link: path.relative(process.cwd(), outFile) })
|
|
344
|
+
if (fs.existsSync(markupOut)) {
|
|
345
|
+
fs.unlinkSync(markupOut)
|
|
346
|
+
log({ tag: this.logTag, text: 'Removed unpublished:', link: path.relative(process.cwd(), markupOut) })
|
|
216
347
|
}
|
|
217
348
|
return
|
|
218
349
|
}
|
|
219
|
-
markupOut = replaceOutExtensions(markupOut)
|
|
220
350
|
fs.writeFileSync(markupOut, result)
|
|
221
351
|
|
|
222
352
|
if (shouldIndex && frontMatter.published !== false) {
|
|
@@ -249,7 +379,7 @@ export default class Markups {
|
|
|
249
379
|
|
|
250
380
|
async compileSingleFile(markupIn, collectionData, pageEntries) {
|
|
251
381
|
const markupStart = performance.now()
|
|
252
|
-
let markupOut = path.
|
|
382
|
+
let markupOut = path.resolve(process.cwd(), this.markupOut)
|
|
253
383
|
const markupOutDir = path.dirname(markupOut)
|
|
254
384
|
const indexableExtensions = this.engine.indexableExtensions
|
|
255
385
|
mkDir(markupOutDir)
|
|
@@ -257,14 +387,15 @@ export default class Markups {
|
|
|
257
387
|
const fileContext = {
|
|
258
388
|
...collectionData,
|
|
259
389
|
relativePathPrefix: getRelativePathPrefix(markupOutDir, null, this.baseURL),
|
|
260
|
-
|
|
390
|
+
// output-relative so page.url matches nav.json/index urls, same as compileDirectory
|
|
391
|
+
_url: getPageUrlRelativeToOutput(this.mapOutputPath(markupOut), this.markupOut)
|
|
261
392
|
}
|
|
262
393
|
|
|
263
394
|
const shouldIndex = pageEntries && indexableExtensions.has(path.extname(markupIn))
|
|
264
395
|
|
|
265
396
|
try {
|
|
266
397
|
const { result, frontMatter, skipped } = await this.compileEntry(markupIn, fileContext)
|
|
267
|
-
markupOut =
|
|
398
|
+
markupOut = this.mapOutputPath(markupOut)
|
|
268
399
|
|
|
269
400
|
if (skipped) {
|
|
270
401
|
if (fs.existsSync(markupOut)) {
|
|
@@ -287,9 +418,9 @@ export default class Markups {
|
|
|
287
418
|
}
|
|
288
419
|
|
|
289
420
|
const markupEnd = performance.now()
|
|
290
|
-
log({ tag: this.logTag, text: 'Compiled:', link: path.relative(process.cwd(), path.
|
|
421
|
+
log({ tag: this.logTag, text: 'Compiled:', link: path.relative(process.cwd(), path.resolve(process.cwd(), this.markupOut, path.basename(markupIn))), time: buildTime(markupStart, markupEnd) })
|
|
291
422
|
} catch (err) {
|
|
292
|
-
log({ tag: this.logTag, error: true, text: 'Failed compiling:', link: path.relative(process.cwd(), path.
|
|
423
|
+
log({ tag: this.logTag, error: true, text: 'Failed compiling:', link: path.relative(process.cwd(), path.resolve(process.cwd(), this.markupOut, path.basename(markupIn))) })
|
|
293
424
|
console.error(err)
|
|
294
425
|
throw err
|
|
295
426
|
} finally {
|
|
@@ -301,13 +432,26 @@ export default class Markups {
|
|
|
301
432
|
const moduleConfig = this.config.markup
|
|
302
433
|
if (!moduleConfig || !moduleConfig.in) return
|
|
303
434
|
|
|
435
|
+
await this.init()
|
|
436
|
+
if (!this.engine) return
|
|
437
|
+
|
|
304
438
|
if (this.config.reactorData) {
|
|
439
|
+
// A removed reactor component must also drop its injected global,
|
|
440
|
+
// same staleness rule as data files in loadDataFiles()
|
|
441
|
+
const reactorKeys = new Set(Object.keys(this.config.reactorData))
|
|
442
|
+
if (this.reactorGlobalKeys) {
|
|
443
|
+
for (const key of this.reactorGlobalKeys) {
|
|
444
|
+
if (!reactorKeys.has(key)) this.engine.removeGlobal(key)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
this.reactorGlobalKeys = reactorKeys
|
|
448
|
+
|
|
305
449
|
for (const [name, html] of Object.entries(this.config.reactorData)) {
|
|
306
450
|
this.engine.setGlobal(name, html)
|
|
307
451
|
}
|
|
308
452
|
}
|
|
309
453
|
|
|
310
|
-
const markupIn = path.
|
|
454
|
+
const markupIn = path.resolve(process.cwd(), this.markupIn)
|
|
311
455
|
|
|
312
456
|
if (!pathExists(markupIn)) {
|
|
313
457
|
log({ tag: 'error', text: 'Markup path does not exist:', link: markupIn })
|
|
@@ -319,10 +463,15 @@ export default class Markups {
|
|
|
319
463
|
...getCollectionDataBasedOnConfig(this.markupIn, this.collectionsConfig)
|
|
320
464
|
}
|
|
321
465
|
|
|
322
|
-
const shouldIndex = this.searchIndexConfig || this.sitemapConfig
|
|
466
|
+
const shouldIndex = this.searchIndexConfig || this.sitemapConfig || this.navConfig
|
|
323
467
|
const pageEntries = shouldIndex ? [] : null
|
|
324
468
|
|
|
325
469
|
buildCollectionPaginationData(collectionData)
|
|
470
|
+
|
|
471
|
+
// must precede any rendering (incl. pagination pages) so every template
|
|
472
|
+
// sees the current build's nav
|
|
473
|
+
this.buildNavGlobal(markupIn, collectionData)
|
|
474
|
+
|
|
326
475
|
const collectionPromises = generateCollectionPaginationPages(collectionData, this.markupIn, this.markupOut, this.compileEntry.bind(this), this.baseURL)
|
|
327
476
|
|
|
328
477
|
await Promise.all(collectionPromises)
|
|
@@ -352,7 +501,8 @@ export default class Markups {
|
|
|
352
501
|
if (shouldIndex && pageEntries) {
|
|
353
502
|
generateIndexFiles(pageEntries, this.markupOut, this.siteData.url, {
|
|
354
503
|
searchIndex: this.searchIndexConfig,
|
|
355
|
-
sitemap: this.sitemapConfig
|
|
504
|
+
sitemap: this.sitemapConfig,
|
|
505
|
+
nav: this.navConfig
|
|
356
506
|
})
|
|
357
507
|
}
|
|
358
508
|
}
|
package/lib/postcss.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { transform } from 'esbuild'
|
|
2
1
|
import fs from 'node:fs'
|
|
3
2
|
import {
|
|
4
3
|
pathExists,
|
|
5
4
|
mkPath,
|
|
6
|
-
insertMinSuffix,
|
|
7
5
|
buildStyleOutputFilePath,
|
|
8
6
|
fillBannerTemplate,
|
|
9
7
|
buildTime,
|
|
10
8
|
fileSize
|
|
11
9
|
} from './utils/helpers.js'
|
|
10
|
+
import minifyToFile from './utils/minify.js'
|
|
12
11
|
import log from './utils/log.js'
|
|
13
12
|
|
|
14
13
|
let postcss
|
|
@@ -65,10 +64,13 @@ export default class PostCSS {
|
|
|
65
64
|
|
|
66
65
|
this.config.postcss = Array.isArray(this.config.postcss) ? this.config.postcss : [this.config.postcss]
|
|
67
66
|
for (const entry of this.config.postcss) {
|
|
68
|
-
if (entry.in
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
if (!entry.in || !entry.out) continue
|
|
68
|
+
if (!pathExists(entry.in)) {
|
|
69
|
+
log({ tag: 'postcss', error: true, text: 'Entry does not exist:', link: entry.in })
|
|
70
|
+
continue
|
|
71
71
|
}
|
|
72
|
+
mkPath(entry.out)
|
|
73
|
+
await this.compileEntry(entry.in, entry.out, entry.options)
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -98,30 +100,14 @@ export default class PostCSS {
|
|
|
98
100
|
const end = performance.now()
|
|
99
101
|
if (!options.justMinified) log({ tag: 'postcss', text: 'Compiled:', link: outfilePath, size: fileSize(outfilePath), time: buildTime(start, end) })
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (this.banner) minified.code = this.banner + '\n' + minified.code
|
|
112
|
-
fs.writeFileSync(minPath, minified.code)
|
|
113
|
-
const minEnd = performance.now()
|
|
114
|
-
log({ tag: 'postcss', text: 'Compiled:', link: minPath, size: fileSize(minPath), time: buildTime(minStart, minEnd) })
|
|
115
|
-
} catch (err) {
|
|
116
|
-
log({ tag: 'postcss', error: true, text: 'Failed compiling:', link: minPath })
|
|
117
|
-
console.error(err)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (options.justMinified) {
|
|
121
|
-
fs.unlinkSync(outfilePath)
|
|
122
|
-
}
|
|
123
|
-
} else {
|
|
124
|
-
if (pathExists(minPath)) fs.unlinkSync(minPath)
|
|
125
|
-
}
|
|
103
|
+
await minifyToFile({
|
|
104
|
+
outfilePath,
|
|
105
|
+
loader: 'css',
|
|
106
|
+
code: css,
|
|
107
|
+
banner: this.banner,
|
|
108
|
+
tag: 'postcss',
|
|
109
|
+
options,
|
|
110
|
+
startTime: options.justMinified ? start : undefined
|
|
111
|
+
})
|
|
126
112
|
}
|
|
127
113
|
}
|
package/lib/reactor.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { build
|
|
1
|
+
import { build } from 'esbuild'
|
|
2
2
|
import { deepMerge } from 'book-of-spells'
|
|
3
3
|
import {
|
|
4
4
|
pathExists,
|
|
5
5
|
mkPath,
|
|
6
|
-
insertMinSuffix,
|
|
7
6
|
pathContainsPathSegment,
|
|
8
7
|
fillBannerTemplate,
|
|
9
8
|
buildTime,
|
|
10
9
|
fileSize
|
|
11
10
|
} from './utils/helpers.js'
|
|
11
|
+
import minifyToFile from './utils/minify.js'
|
|
12
12
|
import fs from 'node:fs'
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import { createRequire } from 'node:module'
|
|
@@ -29,9 +29,12 @@ export default class Reactor {
|
|
|
29
29
|
this.rendered = {}
|
|
30
30
|
|
|
31
31
|
for (const entry of this.config.reactor) {
|
|
32
|
-
if (entry.component
|
|
33
|
-
|
|
32
|
+
if (!entry.component || !entry.inject) continue
|
|
33
|
+
if (!pathExists(entry.component)) {
|
|
34
|
+
log({ tag: 'reactor', error: true, text: 'Component does not exist:', link: entry.component })
|
|
35
|
+
continue
|
|
34
36
|
}
|
|
37
|
+
await this.compileEntry(entry)
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
this.renderedChanged = JSON.stringify(this.rendered) !== JSON.stringify(prevRendered)
|
|
@@ -83,6 +86,9 @@ export const html = renderToString(React.createElement(Component));
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
// --- Step 2: Bundle the client entry for the browser ---
|
|
89
|
+
if (client && out && !pathExists(client)) {
|
|
90
|
+
log({ tag: 'reactor', error: true, text: 'Entry does not exist:', link: client })
|
|
91
|
+
}
|
|
86
92
|
if (client && out && pathExists(client)) {
|
|
87
93
|
mkPath(out)
|
|
88
94
|
|
|
@@ -129,31 +135,7 @@ export const html = renderToString(React.createElement(Component));
|
|
|
129
135
|
if (!options.justMinified) log({ tag: 'reactor', text: 'Compiled:', link: out, size: fileSize(out), time: buildTime(esbuildStart, esbuildEnd) })
|
|
130
136
|
if (options.sourcemap) log({ tag: 'reactor', text: 'Compiled:', link: `${out}.map` })
|
|
131
137
|
|
|
132
|
-
|
|
133
|
-
const minPath = insertMinSuffix(out)
|
|
134
|
-
try {
|
|
135
|
-
const terserStart = performance.now()
|
|
136
|
-
const minifyResult = await transform(fs.readFileSync(out, 'utf-8'), {
|
|
137
|
-
minify: true,
|
|
138
|
-
loader: 'js'
|
|
139
|
-
})
|
|
140
|
-
const terserEnd = performance.now()
|
|
141
|
-
|
|
142
|
-
if (this.banner) minifyResult.code = this.banner + '\n' + minifyResult.code
|
|
143
|
-
fs.writeFileSync(minPath, minifyResult.code)
|
|
144
|
-
log({ tag: 'reactor', text: 'Compiled:', link: minPath, size: fileSize(minPath), time: buildTime(terserStart, terserEnd) })
|
|
145
|
-
} catch (err) {
|
|
146
|
-
log({ tag: 'reactor', error: true, text: 'Failed compiling:', link: minPath })
|
|
147
|
-
console.error(err)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (options.justMinified) {
|
|
151
|
-
fs.unlinkSync(out)
|
|
152
|
-
}
|
|
153
|
-
} else {
|
|
154
|
-
const minPath = insertMinSuffix(out)
|
|
155
|
-
if (pathExists(minPath)) fs.unlinkSync(minPath)
|
|
156
|
-
}
|
|
138
|
+
await minifyToFile({ outfilePath: out, loader: 'js', banner: this.banner, tag: 'reactor', options })
|
|
157
139
|
}
|
|
158
140
|
}
|
|
159
141
|
|
package/lib/scripts.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { build
|
|
1
|
+
import { build } from 'esbuild'
|
|
2
|
+
import { globSync, hasMagic } from 'glob'
|
|
2
3
|
import { deepMerge } from 'book-of-spells'
|
|
3
4
|
import {
|
|
4
5
|
pathExists,
|
|
5
6
|
mkPath,
|
|
6
7
|
pathForFile,
|
|
7
|
-
insertMinSuffix,
|
|
8
|
-
buildScriptOutputFilePath,
|
|
9
8
|
fillBannerTemplate,
|
|
10
9
|
buildTime,
|
|
11
10
|
fileSize
|
|
12
11
|
} from './utils/helpers.js'
|
|
13
|
-
import
|
|
12
|
+
import minifyToFile from './utils/minify.js'
|
|
14
13
|
import log from './utils/log.js'
|
|
15
14
|
|
|
16
15
|
export default class Scripts {
|
|
@@ -24,10 +23,30 @@ export default class Scripts {
|
|
|
24
23
|
this.config.scripts = Array.isArray(this.config.scripts) ? this.config.scripts : [this.config.scripts]
|
|
25
24
|
|
|
26
25
|
for (const scriptEntry of this.config.scripts) {
|
|
27
|
-
if (scriptEntry.in
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
if (!scriptEntry.in || !scriptEntry.out) continue
|
|
27
|
+
// `in` may be an array of entry points — pathExists on an array throws
|
|
28
|
+
const configured = Array.isArray(scriptEntry.in) ? scriptEntry.in : [scriptEntry.in]
|
|
29
|
+
const entryPoints = []
|
|
30
|
+
let missing = false
|
|
31
|
+
for (const entry of configured) {
|
|
32
|
+
if (hasMagic(entry)) {
|
|
33
|
+
// Globs must use `/` even on Windows; sort for deterministic build order
|
|
34
|
+
const matches = globSync(entry, { posix: true }).sort()
|
|
35
|
+
if (!matches.length) {
|
|
36
|
+
log({ tag: 'script', error: true, text: 'Entry does not exist:', link: entry })
|
|
37
|
+
missing = true
|
|
38
|
+
}
|
|
39
|
+
entryPoints.push(...matches)
|
|
40
|
+
} else if (!pathExists(entry)) {
|
|
41
|
+
log({ tag: 'script', error: true, text: 'Entry does not exist:', link: entry })
|
|
42
|
+
missing = true
|
|
43
|
+
} else {
|
|
44
|
+
entryPoints.push(entry)
|
|
45
|
+
}
|
|
30
46
|
}
|
|
47
|
+
if (missing) continue
|
|
48
|
+
mkPath(scriptEntry.out)
|
|
49
|
+
await this.compileEntry(entryPoints, scriptEntry.out, scriptEntry.options)
|
|
31
50
|
}
|
|
32
51
|
}
|
|
33
52
|
|
|
@@ -73,9 +92,16 @@ export default class Scripts {
|
|
|
73
92
|
|
|
74
93
|
deepMerge(opts, optionsClone) // ability to pass other esbuild options `node_modules/esbuild/lib/main.d.ts`
|
|
75
94
|
|
|
95
|
+
// Multi-dir entry points nest output under their common ancestor (esbuild's
|
|
96
|
+
// outbase), so output paths can't be derived from basenames — read them
|
|
97
|
+
// from the metafile instead. Keys are relative to absWorkingDir.
|
|
98
|
+
opts.metafile = true
|
|
99
|
+
opts.absWorkingDir = process.cwd()
|
|
100
|
+
|
|
76
101
|
const esbuildStart = performance.now()
|
|
102
|
+
let result
|
|
77
103
|
try {
|
|
78
|
-
await build(opts)
|
|
104
|
+
result = await build(opts)
|
|
79
105
|
} catch (err) {
|
|
80
106
|
log({ tag, error: true, text: 'Failed compiling:', link: outfilePath })
|
|
81
107
|
console.error(err)
|
|
@@ -83,36 +109,13 @@ export default class Scripts {
|
|
|
83
109
|
}
|
|
84
110
|
const esbuildEnd = performance.now()
|
|
85
111
|
|
|
86
|
-
for (const
|
|
87
|
-
|
|
88
|
-
const minPath = insertMinSuffix(newOutFilePath)
|
|
112
|
+
for (const [newOutFilePath, output] of Object.entries(result.metafile.outputs)) {
|
|
113
|
+
if (!output.entryPoint) continue // sourcemaps, chunks
|
|
89
114
|
|
|
90
115
|
if (!options.justMinified) log({ tag, text: 'Compiled:', link: newOutFilePath, size: fileSize(newOutFilePath), time: buildTime(esbuildStart, esbuildEnd) })
|
|
91
116
|
if (options.sourcemap) log({ tag, text: 'Compiled:', link: `${newOutFilePath}.map` })
|
|
92
117
|
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const terserStart = performance.now()
|
|
96
|
-
const minifyResult = await transform(fs.readFileSync(newOutFilePath, 'utf-8'), {
|
|
97
|
-
minify: true,
|
|
98
|
-
loader: 'js'
|
|
99
|
-
})
|
|
100
|
-
const terserEnd = performance.now()
|
|
101
|
-
|
|
102
|
-
if (this.banner) minifyResult.code = this.banner + '\n' + minifyResult.code
|
|
103
|
-
fs.writeFileSync(minPath, minifyResult.code)
|
|
104
|
-
log({ tag, text: 'Compiled:', link: minPath, size: fileSize(minPath), time: buildTime(terserStart, terserEnd) })
|
|
105
|
-
} catch (err) {
|
|
106
|
-
log({ tag, error: true, text: 'Failed compiling:', link: minPath })
|
|
107
|
-
console.error(err)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (options.justMinified) {
|
|
111
|
-
fs.unlinkSync(newOutFilePath)
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
if (pathExists(minPath)) fs.unlinkSync(minPath)
|
|
115
|
-
}
|
|
118
|
+
await minifyToFile({ outfilePath: newOutFilePath, loader: 'js', banner: this.banner, tag, options })
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
121
|
}
|