Skip to content
Merged
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Usage: top-bun [options]
--dest, -d path to build destination directory (default: "public")
--ignore, -i comma separated gitignore style ignore string
--drafts Build draft pages with the `.draft.{md,js,html}` page suffix.
--copy Path to directories to copy into dist; can be used multiple times
--target, -t comma separated target strings for esbuild
--noEsbuildMeta skip writing the esbuild metafile to disk
--watch-only watch and build the src directory without serving
Expand Down Expand Up @@ -587,6 +588,38 @@ These imports will include the `root.layout.js` layout assets into the `blog.lay

All static assets in the `src` directory are copied 1:1 to the `public` directory. Any file in the `src` directory that doesn't end in `.js`, `.css`, `.html`, or `.md` is copied to the `dest` directory.

### 📁 `--copy` directories

You can specify directories to copy into your `dest` directory using the `--copy` flag. Everything in those directories will be copied as-is into the destination, including js, css, html and markdown, preserving the internal directory structure. Conflicting files are not detected or reported and will cause undefined behavior.

Copy folders must live **outside** of the `src` directory.

This is useful when you have legacy or archived site content that you want to include in your site, but don't want `top-bun` to process or modify it.
In general, static content should live in your primary `src` directory, however for merging in old static assets over your top-bun build is sometimes easier to reason about when it's kept in a separate folder and isn't processed in any way.

For example:

```
src/...
oldsite/
├── client.js
├── hello.html
└── styles/
└── globals.css
```

After build:

```
src/...
oldsite/...
public/
├── client.js
├── hello.html
└── styles/
└── globals.css
```

## Templates

Template files let you write any kind of file type to the `dest` folder while customizing the contents of that file with access to the site [Variables](#variables) object, or inject any other kind of data fetched at build time. Template files can be located anywhere and look like:
Expand Down
10 changes: 10 additions & 0 deletions bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ const options = {
type: 'boolean',
help: 'watch and build the src folder without serving',
},
copy: {
type: 'string',
help: 'path to directories to copy into dist; can be used multiple times',
multiple: true
},
help: {
type: 'boolean',
short: 'h',
Expand Down Expand Up @@ -202,6 +207,11 @@ top-bun eject actions:
if (argv['target']) opts.target = String(argv['target']).split(',')
if (argv['noEsbuildMeta']) opts.metafile = false
if (argv['drafts']) opts.buildDrafts = true
if (argv['copy']) {
const copyPaths = Array.isArray(argv['copy']) ? argv['copy'] : [argv['copy']]
// @ts-expect-error
opts.copy = copyPaths.map(p => resolve(cwd, p))
}

const topBun = new TopBun(src, dest, opts)

Expand Down
61 changes: 43 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { once } from 'events'
import assert from 'node:assert'
import chokidar from 'chokidar'
import { basename, relative } from 'path'
import { basename, relative, resolve } from 'node:path'
// @ts-ignore
import makeArray from 'make-array'
import ignore from 'ignore'
Expand All @@ -11,6 +11,7 @@ import { inspect } from 'util'
import browserSync from 'browser-sync'

import { getCopyGlob } from './lib/build-static/index.js'
import { getCopyDirs } from './lib/build-copy/index.js'
import { builder } from './lib/builder.js'
import { TopBunAggregateError } from './lib/helpers/top-bun-aggregate-error.js'

Expand Down Expand Up @@ -66,7 +67,7 @@ export class TopBun {
/** @type {string} */ #dest = ''
/** @type {Readonly<CurrentOpts & { ignore: string[] }>} */ opts
/** @type {FSWatcher?} */ #watcher = null
/** @type {any?} */ #cpxWatcher = null
/** @type {any[]?} */ #cpxWatchers = null
/** @type {browserSync.BrowserSyncInstance?} */ #browserSyncServer = null

/**
Expand All @@ -83,14 +84,29 @@ export class TopBun {
this.#src = src
this.#dest = dest

const copyDirs = opts?.copy ?? []

this.opts = {
...opts,
ignore: [
...DEFAULT_IGNORES,
basename(dest),
...copyDirs.map(dir => basename(dir)),
...makeArray(opts.ignore),
],
}

if (copyDirs && copyDirs.length > 0) {
const absDest = resolve(this.#dest)
for (const copyDir of copyDirs) {
// Copy dirs can be in the src dir (nested builds), but not in the dest dir.
const absCopyDir = resolve(copyDir)
const relToDest = relative(absDest, absCopyDir)
if (relToDest === '' || !relToDest.startsWith('..')) {
throw new Error(`copyDir ${copyDir} is within the dest directory`)
}
}
}
}

get watching () {
Expand Down Expand Up @@ -126,7 +142,12 @@ export class TopBun {
report = err.results
}

this.#cpxWatcher = cpx.watch(getCopyGlob(this.#src), this.#dest, { ignore: this.opts.ignore })
const copyDirs = getCopyDirs(this.opts.copy)

this.#cpxWatchers = [
cpx.watch(getCopyGlob(this.#src), this.#dest, { ignore: this.opts.ignore }),
...copyDirs.map(copyDir => cpx.watch(copyDir, this.#dest))
]
if (serve) {
const bs = browserSync.create()
this.#browserSyncServer = bs
Expand All @@ -136,20 +157,22 @@ export class TopBun {
})
}

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

this.#cpxWatcher.on('copy', (/** @type{{ srcPath: string, dstPath: string }} */e) => {
console.log(`Copy ${e.srcPath} to ${e.dstPath}`)
})
w.on('copy', (/** @type{{ srcPath: string, dstPath: string }} */e) => {
console.log(`Copy ${e.srcPath} to ${e.dstPath}`)
})

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

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

const ig = ignore().add(this.opts.ignore ?? [])
Expand Down Expand Up @@ -204,11 +227,13 @@ export class TopBun {
}

async stopWatching () {
if ((!this.watching || !this.#cpxWatcher)) throw new Error('Not watching')
if (this.#watcher) await this.#watcher.close()
this.#cpxWatcher.close()
if ((!this.watching || !this.#cpxWatchers)) throw new Error('Not watching')
if (this.#watcher) this.#watcher.close()
this.#cpxWatchers.forEach(w => {
w.close()
})
this.#watcher = null
this.#cpxWatcher = null
this.#cpxWatchers = null
this.#browserSyncServer?.exit() // This will kill the process
this.#browserSyncServer = null
}
Expand Down
Empty file.
Empty file.
Empty file.
60 changes: 60 additions & 0 deletions lib/build-copy/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// @ts-ignore
import cpx from 'cpx2'
import { join } from 'node:path'
const copy = cpx.copy

/**
* @typedef {Awaited<ReturnType<typeof copy>>} CopyBuilderReport
*/

/**
* @typedef {import('../builder.js').BuildStepResult<'static', CopyBuilderReport>} CopyBuildStepResult
*/

/**
* @typedef {import('../builder.js').BuildStep<'static', CopyBuilderReport>} CopyBuildStep
*/

/**
* @param {string[]} copy
* @return {string[]}
*/
export function getCopyDirs (copy = []) {
const copyGlobs = copy?.map((dir) => join(dir, '**'))
return copyGlobs
}

/**
* run CPX2 on src folder
*
* @type {CopyBuildStep}
*/
export async function buildCopy (_src, dest, _siteData, opts) {
/** @type {CopyBuildStepResult} */
const results = {
type: 'static',
report: {},
errors: [],
warnings: [],
}

const copyDirs = getCopyDirs(opts?.copy)

const copyTasks = copyDirs.map((copyDir) => {
return copy(copyDir, dest)
})

const settled = await Promise.allSettled(copyTasks)

for (const [index, result] of Object.entries(settled)) {
// @ts-expect-error
const copyDir = copyDirs[index]
if (result.status === 'rejected') {
const buildError = new Error('Error copying copy folders', { cause: result.reason })
results.errors.push(buildError)
} else {
results.report[copyDir] = result.value
}
}
return results
}
8 changes: 8 additions & 0 deletions lib/build-copy/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import tap from 'tap'
import { getCopyDirs } from './index.js'

tap.test('getCopyDirs returns correct src/dest pairs', async (t) => {
const copyDirs = getCopyDirs(['fixtures'])

t.strictSame(copyDirs, ['fixtures/**'])
})
11 changes: 10 additions & 1 deletion lib/builder.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { buildPages } from './build-pages/index.js'
import { identifyPages } from './identify-pages.js'
import { buildStatic } from './build-static/index.js'
import { buildCopy } from './build-copy/index.js'
import { buildEsbuild } from './build-esbuild/index.js'
import { TopBunAggregateError } from './helpers/top-bun-aggregate-error.js'
import { ensureDest } from './helpers/ensure-dest.js'
Expand Down Expand Up @@ -40,12 +41,12 @@ import { ensureDest } from './helpers/ensure-dest.js'

/**
* @typedef TopBunOpts
* @property {string[]|undefined} [ignore] - Array of file/folder patterns to ignore.
* @property {boolean|undefined} [static=true] - Enable/disable static file processing
* @property {boolean|undefined} [metafile=true] - Enable/disable the writing of the esbuild metadata file.
* @property {string[]|undefined} [ignore=[]] - Array of ignore strings
* @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
*/

/**
Expand All @@ -57,13 +58,15 @@ import { ensureDest } from './helpers/ensure-dest.js'
* @typedef {import('./build-esbuild/index.js').EsBuildStepResults} EsBuildStepResults
* @typedef {import('./build-pages/index.js').PageBuildStepResult} PageBuildStepResult
* @typedef {import('./build-static/index.js').StaticBuildStepResult} StaticBuildStepResult
* @typedef {import('./build-copy/index.js').CopyBuildStepResult} CopyBuildStepResult
*/

/**
* @typedef Results
* @property {SiteData} siteData
* @property {EsBuildStepResults} esbuildResults
* @property {StaticBuildStepResult} [staticResults]
* @property {CopyBuildStepResult} [copyResults]
* @property {PageBuildStepResult} [pageBuildResults]
* @property {BuildStepWarnings} warnings
*/
Expand Down Expand Up @@ -112,11 +115,13 @@ export async function builder (src, dest, opts) {
const [
esbuildResults,
staticResults,
copyResults,
] = await Promise.all([
buildEsbuild(src, dest, siteData, opts),
opts.static
? buildStatic(src, dest, siteData, opts)
: Promise.resolve(),
buildCopy(src, dest, siteData, opts),
])

/** @type {Results} */
Expand All @@ -135,6 +140,10 @@ export async function builder (src, dest, opts) {
results.staticResults = staticResults
}

errors.push(...copyResults.errors)
warnings.push(...copyResults.warnings)
results.copyResults = copyResults

if (errors.length > 0) {
const preBuildError = new TopBunAggregateError(errors, 'Prebuild finished but there were errors.', results)
throw preBuildError
Expand Down
2 changes: 1 addition & 1 deletion lib/helpers/top-bun-warning.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* 'TOP_BUN_WARNING_UNKNOWN_PAGE_BUILDER' |
* 'TOP_BUN_WARNING_DUPLICATE_GLOBAL_STYLE' |
* 'TOP_BUN_WARNING_DUPLICATE_GLOBAL_CLIENT' |
* 'TOP_BUN_WARNING_DUPLICATE_ESBUILD_SETINGS' |
* 'TOP_BUN_WARNING_DUPLICATE_ESBUILD_SETTINGS' |
* 'TOP_BUN_WARNING_DUPLICATE_GLOBAL_VARS'
* } TopBunWarningCode
*/
Expand Down
8 changes: 4 additions & 4 deletions lib/identify-pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const shaper = ({
* @typedef TemplateInfo
* @property {WalkerFile} templateFile - The template file info.
* @property {string} path - The the path of the parent dir of the template
* @property {string} outputName - The derived output name of the template file. Might be overriden.
* @property {string} outputName - The derived output name of the template file. Might be overridden.
*/

/**
Expand Down Expand Up @@ -296,12 +296,12 @@ export async function identifyPages (src, opts = {}) {
if (templateSuffixs.some(suffix => fileName.endsWith(suffix))) {
const suffix = templateSuffixs.find(suffix => fileName.endsWith(suffix))
if (!suffix) throw new Error('template suffix not found')
const temlateFileName = fileName.slice(0, -suffix.length)
const templateFileName = fileName.slice(0, -suffix.length)

templates.push({
templateFile: fileInfo,
path: dir,
outputName: temlateFileName,
outputName: templateFileName,
})
}

Expand Down Expand Up @@ -341,7 +341,7 @@ export async function identifyPages (src, opts = {}) {
if (esbuildSettingsNames.some(name => basename(fileName) === name)) {
if (esbuildSettings) {
warnings.push({
code: 'TOP_BUN_WARNING_DUPLICATE_ESBUILD_SETINGS',
code: 'TOP_BUN_WARNING_DUPLICATE_ESBUILD_SETTINGS',
message: `Skipping ${fileInfo.relname}. Duplicate esbuild options ${fileName} to ${esbuildSettings.filepath}`,
})
} else {
Expand Down
1 change: 1 addition & 0 deletions test-cases/general-features/copyfolder/oldsite/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('hello world')
11 changes: 11 additions & 0 deletions test-cases/general-features/copyfolder/oldsite/hello.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello</title>
</head>
<body>
<h1>Hello world</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
background-color: #f0f0f0;
}
1 change: 1 addition & 0 deletions test-cases/general-features/copyfolder/some.type
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hi
Loading