Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 22 additions & 19 deletions bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { readPackage } from 'read-pkg'
import { addPackageDependencies } from 'write-package'

import { copyFile } from './lib/helpers/copy-file.js'
import { DomStack } from './index.js'
import { DomStack, createLogger } from './index.js'
import { DomStackAggregateError } from './lib/helpers/domstack-aggregate-error.js'
import { generateTreeData } from './lib/helpers/generate-tree-data.js'
import { askYesNo } from './lib/helpers/cli-prompt.js'
Expand Down Expand Up @@ -201,6 +201,8 @@ domstack eject actions:

/** @type {DomStackOpts} */
const opts = {}
const logger = createLogger('info')
opts.logger = logger

if (argv['ignore']) opts.ignore = String(argv['ignore']).split(',')
if (argv['target']) opts.target = String(argv['target']).split(',')
Expand All @@ -219,11 +221,10 @@ domstack eject actions:

async function quit () {
if (domStack.watching) {
const results = await domStack.stopWatching()
console.log(results)
console.log('watching stopped')
await domStack.stopWatching()
logger.info('Watching stopped')
}
console.log('\nquitting cleanly')
logger.info('Quitting cleanly')
process.exit(0)
}

Expand Down Expand Up @@ -258,22 +259,24 @@ domstack eject actions:
process.exit(1)
}
} else {
const initialResults = await domStack.watch({
await domStack.watch({
serve: !argv['watch-only'],
onInitialBuild: (initialResults) => {
console.log(tree(generateTreeData(cwd, src, dest, initialResults)))
if (initialResults?.warnings?.length > 0) {
console.log(
'\nThere were build warnings:\n'
)
}
for (const warning of initialResults?.warnings) {
if ('message' in warning) {
console.log(` ${warning.message}`)
} else {
console.warn(warning)
}
}
},
})
console.log(tree(generateTreeData(cwd, src, dest, initialResults)))
if (initialResults?.warnings?.length > 0) {
console.log(
'\nThere were build warnings:\n'
)
}
for (const warning of initialResults?.warnings) {
if ('message' in warning) {
console.log(` ${warning.message}`)
} else {
console.warn(warning)
}
}
}
}

Expand Down
8 changes: 1 addition & 7 deletions examples/default-layout/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,11 @@
"start": "npm run watch",
"build": "npm run clean && domstack",
"clean": "rm -rf public && mkdir -p public",
"watch": "npm run clean && run-p watch:*",
"watch:serve": "browser-sync start --server 'public' --files 'public'",
"watch:domstack": "npm run build -- --watch"
"watch": "npm run clean && domstack --watch"
},
"dependencies": {
"@domstack/static": "file:../../."
},
"devDependencies": {
"browser-sync": "^2.26.7",
"npm-run-all2": "^6.0.0"
},
"keywords": [],
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io/)",
"license": "MIT"
Expand Down
8 changes: 1 addition & 7 deletions examples/string-layouts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,11 @@
"start": "npm run watch",
"build": "npm run clean && domstack",
"clean": "rm -rf public && mkdir -p public",
"watch": "npm run clean && run-p watch:*",
"watch:serve": "browser-sync start --server 'public' --files 'public'",
"watch:domstack": "npm run build -- --watch"
"watch": "npm run clean && domstack --watch"
},
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io/)",
"license": "MIT",
"dependencies": {
"@domstack/static": "file:../../."
},
"devDependencies": {
"browser-sync": "^2.26.7",
"npm-run-all2": "^6.0.0"
}
}
107 changes: 61 additions & 46 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* @import { GlobalDataFunction, AsyncGlobalDataFunction, WorkerBuildStepResult, GlobalDataFunctionParams } from './lib/build-pages/index.js'
* @import { BuildOptions, BuildContext } from 'esbuild'
* @import { PageInfo, TemplateInfo } from './lib/identify-pages.js'
* @import { BsInstance } from '@domstack/sync'
*/
import { once } from 'events'
import assert from 'node:assert'
Expand All @@ -21,7 +22,7 @@ import makeArray from 'make-array'
import ignore from 'ignore'
import { watch as cpxWatch } from 'cpx2'
import { inspect } from 'util'
import browserSync from 'browser-sync'
import { createLogger as createSyncLogger, createServer } from '@domstack/sync'
import { find } from '@11ty/dependency-tree-typescript'

import { getCopyGlob } from './lib/build-static/index.js'
Expand Down Expand Up @@ -50,6 +51,13 @@ import { ensureDest } from './lib/helpers/ensure-dest.js'
import { DomStackAggregateError } from './lib/helpers/domstack-aggregate-error.js'

export { PageData } from './lib/build-pages/page-data.js'
export { wrapPinoLogger } from '@domstack/sync'

const LOG_PREFIX = '[domstack]'

export function createLogger (level = 'info', streams = {}, options = {}) {
return createSyncLogger(level, streams, { prefix: LOG_PREFIX, ...options })
}

/**
* @typedef {BuildOptions} BuildOptions
Expand Down Expand Up @@ -154,9 +162,10 @@ export class DomStack {
/** @type {Readonly<CurrentOpts & { ignore: string[] }>} */ opts
/** @type {FSWatcher?} */ #watcher = null
/** @type {any[]?} */ #cpxWatchers = null
/** @type {browserSync.BrowserSyncInstance?} */ #browserSyncServer = null
/** @type {BsInstance?} */ #syncServer = null
/** @type {BuildContext?} */ #esbuildContext = null
/** @type {SiteData?} */ #siteData = null
/** @type {ReturnType<typeof createLogger>} */ #logger

// Watch maps (rebuilt after every full rebuild)
/** @type {Map<string, Set<string>>} depFilepath → Set<layoutName> */
Expand Down Expand Up @@ -192,17 +201,19 @@ export class DomStack {
this.#src = src
this.#dest = dest

const copyDirs = opts?.copy ?? []
const { logger, ...buildOpts } = opts
const copyDirs = buildOpts?.copy ?? []

this.opts = {
...opts,
this.opts = /** @type {Readonly<CurrentOpts & { ignore: string[] }>} */ ({
...buildOpts,
ignore: [
...DEFAULT_IGNORES,
basename(dest),
...copyDirs.map(dir => basename(dir)),
...makeArray(opts.ignore),
...makeArray(buildOpts.ignore),
],
}
})
this.#logger = logger ?? createLogger('info')

if (copyDirs && copyDirs.length > 0) {
const absDest = resolve(this.#dest)
Expand All @@ -229,10 +240,12 @@ export class DomStack {
* Build and watch a domstack build
* @param {object} [params]
* @param {boolean} params.serve
* @param {(results: Results) => void | Promise<void>} [params.onInitialBuild]
* @return {Promise<Results>}
*/
async watch ({
serve,
onInitialBuild,
} = {
serve: true,
}) {
Expand Down Expand Up @@ -268,7 +281,7 @@ export class DomStack {
pageBuildResults,
}
buildLogger(report)
console.log('Initial JS, CSS and Page Build Complete')
this.#logger.info('Initial JS, CSS and page build complete')
} catch (err) {
errorLogger(err)
if (!(err instanceof DomStackAggregateError)) throw new Error('Non-aggregate error thrown', { cause: err })
Expand All @@ -278,38 +291,29 @@ export class DomStack {
// Build watch maps after initial build
await this.#rebuildMaps(siteData)

// ── Copy watchers & browser-sync ─────────────────────────────────────
// ── Copy watchers & dev server ───────────────────────────────────────
const copyDirs = getCopyDirs(this.opts.copy)

this.#cpxWatchers = [
cpxWatch(getCopyGlob(this.#src), this.#dest, { ignore: this.opts.ignore }),
...copyDirs.map(copyDir => cpxWatch(copyDir, this.#dest))
]
if (serve) {
const bs = browserSync.create()
this.#browserSyncServer = bs
bs.watch(basename(this.#dest), { ignoreInitial: true }).on('change', bs.reload)
bs.init({
server: this.#dest,
})
}

this.#cpxWatchers.forEach(w => {
w.on('watch-ready', () => {
console.log('Copy watcher ready')

w.on('copy', (/** @type{{ srcPath: string, dstPath: string }} */e) => {
console.log(`Copy ${e.srcPath} to ${e.dstPath}`)
})
const copyWatchersReady = this.#cpxWatchers.map(async w => {
w.on('copy', (/** @type{{ srcPath: string, dstPath: string }} */e) => {
this.#logger.info({ srcPath: e.srcPath, dstPath: e.dstPath }, 'Copy file')
})

w.on('remove', (/** @type{{ path: string }} */e) => {
console.log(`Remove ${e.path}`)
})
w.on('remove', (/** @type{{ path: string }} */e) => {
this.#logger.info({ path: e.path }, 'Remove file')
})

w.on('watch-error', (/** @type{Error} */err) => {
console.log(`Copy error: ${err.message}`)
})
w.on('watch-error', (/** @type{Error} */err) => {
this.#logger.error({ err }, 'Copy error')
})

await once(w, 'watch-ready')
this.#logger.info('Copy watcher ready')
})

// ── Chokidar watcher ─────────────────────────────────────────────────
Expand Down Expand Up @@ -340,7 +344,20 @@ export class DomStack {

this.#watcher = watcher

await once(watcher, 'ready')
await Promise.all([
...copyWatchersReady,
once(watcher, 'ready'),
])

await onInitialBuild?.(report)

if (serve) {
this.#syncServer = await createServer({
server: this.#dest,
files: basename(this.#dest),
logger: this.#logger.child({ component: 'sync' }, { prefix: '[domstack-sync]' }),
})
}

const enqueue = (/** @type {() => Promise<void>} */ fn) => {
this.#buildLock = this.#buildLock.then(() => fn().catch(errorLogger))
Expand All @@ -367,7 +384,7 @@ export class DomStack {
* Used for structural changes (add/unlink), global.vars.*, esbuild.settings.*.
*/
async #fullRebuild () {
console.log('Triggering full rebuild...')
this.#logger.info('Triggering full rebuild')
// Dispose the old esbuild context
if (this.#esbuildContext) {
await this.#esbuildContext.dispose()
Expand All @@ -377,8 +394,7 @@ export class DomStack {
const siteData = await identifyPages(this.#src, this.opts)

if (siteData.errors.length > 0) {
console.error('identifyPages errors:')
for (const err of siteData.errors) console.error(' ', err.message)
this.#logger.error({ errors: siteData.errors.map(err => err.message) }, 'identifyPages errors')
return
}

Expand Down Expand Up @@ -415,13 +431,12 @@ export class DomStack {
)

if (isEsbuildEntry) {
console.log(`"${changedBasename}" ${event}, restarting esbuild...`)
this.#logger.info({ file: changedBasename, event }, 'Restarting esbuild')

// Re-identify pages to discover the new/removed entry point
const siteData = await identifyPages(this.#src, this.opts)
if (siteData.errors.length > 0) {
console.error('identifyPages errors:')
for (const err of siteData.errors) console.error(' ', err.message)
this.#logger.error({ errors: siteData.errors.map(err => err.message) }, 'identifyPages errors')
return
}

Expand Down Expand Up @@ -476,7 +491,7 @@ export class DomStack {
await this.#rebuildMaps(siteData)
} else {
// Non-esbuild file: structural change (page, layout, template, config, etc.)
console.log(`"${changedBasename}" ${event}, triggering full rebuild...`)
this.#logger.info({ file: changedBasename, event }, 'Triggering full rebuild')
return this.#fullRebuild()
}
}
Expand Down Expand Up @@ -640,19 +655,19 @@ export class DomStack {

// 2. global.vars.* → full rebuild (esbuild restart + all pages)
if (globalVarsNames.some(n => changedBasename === n)) {
console.log(`"${changedBasename}" changed, triggering full rebuild...`)
this.#logger.info({ file: changedBasename }, 'Triggering full rebuild')
return this.#fullRebuild()
}

// 3. global.data.* → full page rebuild (no esbuild restart)
if (globalDataNames.some(n => changedBasename === n)) {
console.log(`"${changedBasename}" changed, rebuilding all pages...`)
this.#logger.info({ file: changedBasename }, 'Rebuilding all pages')
return this.#runPageBuild(siteData)
}

// 4. esbuild.settings.* → full rebuild
if (esbuildSettingsNames.some(n => changedBasename === n)) {
console.log(`"${changedBasename}" changed, triggering full rebuild...`)
this.#logger.info({ file: changedBasename }, 'Triggering full rebuild')
return this.#fullRebuild()
}

Expand All @@ -667,7 +682,7 @@ export class DomStack {
// esbuild's own watcher handles these. Stable filenames mean page HTML doesn't
// change, so no page rebuild is needed.
if (this.#esbuildEntryPoints.has(changedPath)) {
console.log(`"${changedBasename}" changed, esbuild will handle rebundling.`)
this.#logger.info({ file: changedBasename }, 'Esbuild will handle rebundling')
return
}

Expand All @@ -681,7 +696,7 @@ export class DomStack {
const pageFilterPaths = Array.from(affectedPages).map(p => p.pageFile.filepath)
return this.#runPageBuild(siteData, pageFilterPaths, [])
}
console.log(`"${changedBasename}" changed but no pages use layout "${layoutName}", skipping.`)
this.#logger.info({ file: changedBasename, layout: layoutName }, 'Layout changed but no pages use it; skipping')
return
}
// Not a registered layout — fall through to dep checks
Expand Down Expand Up @@ -741,7 +756,7 @@ export class DomStack {
}

// 13. No matching rule — skip.
console.log(`"${changedBasename}" changed but did not match any rebuild rule, skipping.`)
this.#logger.info({ file: changedBasename }, 'Changed file did not match any rebuild rule; skipping')
}

async stopWatching () {
Expand All @@ -756,8 +771,8 @@ export class DomStack {
await this.#esbuildContext.dispose()
this.#esbuildContext = null
}
this.#browserSyncServer?.exit() // This will kill the process
this.#browserSyncServer = null
await this.#syncServer?.exit()
this.#syncServer = null
}

/**
Expand Down
1 change: 1 addition & 0 deletions lib/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { ensureDest } from './helpers/ensure-dest.js'
* @property {string[]|undefined} [target=[]] - Array of target strings to pass to esbuild
* @property {boolean|undefined} [buildDrafts=false] - Build draft files with the published:false variable
* @property {string[]|undefined} [copy=[]] - Array of paths to copy their contents into the dest directory
* @property {import('@domstack/sync').Logger|undefined} [logger] - Logger used for build/watch output
*/

/**
Expand Down
Loading