Skip to content

Commit b9873c3

Browse files
authored
Merge pull request #178 from bcomnes/directory-copy
Add a folder copy flag
2 parents 488c7d7 + 3b20757 commit b9873c3

File tree

18 files changed

+204
-26
lines changed

18 files changed

+204
-26
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Usage: top-bun [options]
3333
--dest, -d path to build destination directory (default: "public")
3434
--ignore, -i comma separated gitignore style ignore string
3535
--drafts Build draft pages with the `.draft.{md,js,html}` page suffix.
36+
--copy Path to directories to copy into dist; can be used multiple times
3637
--target, -t comma separated target strings for esbuild
3738
--noEsbuildMeta skip writing the esbuild metafile to disk
3839
--watch-only watch and build the src directory without serving
@@ -587,6 +588,38 @@ These imports will include the `root.layout.js` layout assets into the `blog.lay
587588

588589
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.
589590

591+
### 📁 `--copy` directories
592+
593+
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.
594+
595+
Copy folders must live **outside** of the `src` directory.
596+
597+
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.
598+
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.
599+
600+
For example:
601+
602+
```
603+
src/...
604+
oldsite/
605+
├── client.js
606+
├── hello.html
607+
└── styles/
608+
└── globals.css
609+
```
610+
611+
After build:
612+
613+
```
614+
src/...
615+
oldsite/...
616+
public/
617+
├── client.js
618+
├── hello.html
619+
└── styles/
620+
└── globals.css
621+
```
622+
590623
## Templates
591624

592625
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:

bin.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ const options = {
8080
type: 'boolean',
8181
help: 'watch and build the src folder without serving',
8282
},
83+
copy: {
84+
type: 'string',
85+
help: 'path to directories to copy into dist; can be used multiple times',
86+
multiple: true
87+
},
8388
help: {
8489
type: 'boolean',
8590
short: 'h',
@@ -202,6 +207,11 @@ top-bun eject actions:
202207
if (argv['target']) opts.target = String(argv['target']).split(',')
203208
if (argv['noEsbuildMeta']) opts.metafile = false
204209
if (argv['drafts']) opts.buildDrafts = true
210+
if (argv['copy']) {
211+
const copyPaths = Array.isArray(argv['copy']) ? argv['copy'] : [argv['copy']]
212+
// @ts-expect-error
213+
opts.copy = copyPaths.map(p => resolve(cwd, p))
214+
}
205215

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

index.js

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { once } from 'events'
22
import assert from 'node:assert'
33
import chokidar from 'chokidar'
4-
import { basename, relative } from 'path'
4+
import { basename, relative, resolve } from 'node:path'
55
// @ts-ignore
66
import makeArray from 'make-array'
77
import ignore from 'ignore'
@@ -11,6 +11,7 @@ import { inspect } from 'util'
1111
import browserSync from 'browser-sync'
1212

1313
import { getCopyGlob } from './lib/build-static/index.js'
14+
import { getCopyDirs } from './lib/build-copy/index.js'
1415
import { builder } from './lib/builder.js'
1516
import { TopBunAggregateError } from './lib/helpers/top-bun-aggregate-error.js'
1617

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

7273
/**
@@ -83,14 +84,29 @@ export class TopBun {
8384
this.#src = src
8485
this.#dest = dest
8586

87+
const copyDirs = opts?.copy ?? []
88+
8689
this.opts = {
8790
...opts,
8891
ignore: [
8992
...DEFAULT_IGNORES,
9093
basename(dest),
94+
...copyDirs.map(dir => basename(dir)),
9195
...makeArray(opts.ignore),
9296
],
9397
}
98+
99+
if (copyDirs && copyDirs.length > 0) {
100+
const absDest = resolve(this.#dest)
101+
for (const copyDir of copyDirs) {
102+
// Copy dirs can be in the src dir (nested builds), but not in the dest dir.
103+
const absCopyDir = resolve(copyDir)
104+
const relToDest = relative(absDest, absCopyDir)
105+
if (relToDest === '' || !relToDest.startsWith('..')) {
106+
throw new Error(`copyDir ${copyDir} is within the dest directory`)
107+
}
108+
}
109+
}
94110
}
95111

96112
get watching () {
@@ -126,7 +142,12 @@ export class TopBun {
126142
report = err.results
127143
}
128144

129-
this.#cpxWatcher = cpx.watch(getCopyGlob(this.#src), this.#dest, { ignore: this.opts.ignore })
145+
const copyDirs = getCopyDirs(this.opts.copy)
146+
147+
this.#cpxWatchers = [
148+
cpx.watch(getCopyGlob(this.#src), this.#dest, { ignore: this.opts.ignore }),
149+
...copyDirs.map(copyDir => cpx.watch(copyDir, this.#dest))
150+
]
130151
if (serve) {
131152
const bs = browserSync.create()
132153
this.#browserSyncServer = bs
@@ -136,20 +157,22 @@ export class TopBun {
136157
})
137158
}
138159

139-
this.#cpxWatcher.on('watch-ready', () => {
140-
console.log('Copy watcher ready')
160+
this.#cpxWatchers.forEach(w => {
161+
w.on('watch-ready', () => {
162+
console.log('Copy watcher ready')
141163

142-
this.#cpxWatcher.on('copy', (/** @type{{ srcPath: string, dstPath: string }} */e) => {
143-
console.log(`Copy ${e.srcPath} to ${e.dstPath}`)
144-
})
164+
w.on('copy', (/** @type{{ srcPath: string, dstPath: string }} */e) => {
165+
console.log(`Copy ${e.srcPath} to ${e.dstPath}`)
166+
})
145167

146-
this.#cpxWatcher.on('remove', (/** @type{{ path: string }} */e) => {
147-
console.log(`Remove ${e.path}`)
148-
})
149-
})
168+
w.on('remove', (/** @type{{ path: string }} */e) => {
169+
console.log(`Remove ${e.path}`)
170+
})
150171

151-
this.#cpxWatcher.on('watch-error', (/** @type{Error} */err) => {
152-
console.log(`Copy error: ${err.message}`)
172+
w.on('watch-error', (/** @type{Error} */err) => {
173+
console.log(`Copy error: ${err.message}`)
174+
})
175+
})
153176
})
154177

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

206229
async stopWatching () {
207-
if ((!this.watching || !this.#cpxWatcher)) throw new Error('Not watching')
208-
if (this.#watcher) await this.#watcher.close()
209-
this.#cpxWatcher.close()
230+
if ((!this.watching || !this.#cpxWatchers)) throw new Error('Not watching')
231+
if (this.#watcher) this.#watcher.close()
232+
this.#cpxWatchers.forEach(w => {
233+
w.close()
234+
})
210235
this.#watcher = null
211-
this.#cpxWatcher = null
236+
this.#cpxWatchers = null
212237
this.#browserSyncServer?.exit() // This will kill the process
213238
this.#browserSyncServer = null
214239
}

lib/build-copy/fixtures/bing/another.copy/.keep

Whitespace-only changes.

lib/build-copy/fixtures/foo.copy/.keep

Whitespace-only changes.

lib/build-copy/fixtures/foo.copy/bar/baz.copy/.keep

Whitespace-only changes.

lib/build-copy/index.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// @ts-ignore
2+
import cpx from 'cpx2'
3+
import { join } from 'node:path'
4+
const copy = cpx.copy
5+
6+
/**
7+
* @typedef {Awaited<ReturnType<typeof copy>>} CopyBuilderReport
8+
*/
9+
10+
/**
11+
* @typedef {import('../builder.js').BuildStepResult<'static', CopyBuilderReport>} CopyBuildStepResult
12+
*/
13+
14+
/**
15+
* @typedef {import('../builder.js').BuildStep<'static', CopyBuilderReport>} CopyBuildStep
16+
*/
17+
18+
/**
19+
* @param {string[]} copy
20+
* @return {string[]}
21+
*/
22+
export function getCopyDirs (copy = []) {
23+
const copyGlobs = copy?.map((dir) => join(dir, '**'))
24+
return copyGlobs
25+
}
26+
27+
/**
28+
* run CPX2 on src folder
29+
*
30+
* @type {CopyBuildStep}
31+
*/
32+
export async function buildCopy (_src, dest, _siteData, opts) {
33+
/** @type {CopyBuildStepResult} */
34+
const results = {
35+
type: 'static',
36+
report: {},
37+
errors: [],
38+
warnings: [],
39+
}
40+
41+
const copyDirs = getCopyDirs(opts?.copy)
42+
43+
const copyTasks = copyDirs.map((copyDir) => {
44+
return copy(copyDir, dest)
45+
})
46+
47+
const settled = await Promise.allSettled(copyTasks)
48+
49+
for (const [index, result] of Object.entries(settled)) {
50+
// @ts-expect-error
51+
const copyDir = copyDirs[index]
52+
if (result.status === 'rejected') {
53+
const buildError = new Error('Error copying copy folders', { cause: result.reason })
54+
results.errors.push(buildError)
55+
} else {
56+
results.report[copyDir] = result.value
57+
}
58+
}
59+
return results
60+
}

lib/build-copy/index.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import tap from 'tap'
2+
import { getCopyDirs } from './index.js'
3+
4+
tap.test('getCopyDirs returns correct src/dest pairs', async (t) => {
5+
const copyDirs = getCopyDirs(['fixtures'])
6+
7+
t.strictSame(copyDirs, ['fixtures/**'])
8+
})

lib/builder.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { buildPages } from './build-pages/index.js'
22
import { identifyPages } from './identify-pages.js'
33
import { buildStatic } from './build-static/index.js'
4+
import { buildCopy } from './build-copy/index.js'
45
import { buildEsbuild } from './build-esbuild/index.js'
56
import { TopBunAggregateError } from './helpers/top-bun-aggregate-error.js'
67
import { ensureDest } from './helpers/ensure-dest.js'
@@ -40,12 +41,12 @@ import { ensureDest } from './helpers/ensure-dest.js'
4041

4142
/**
4243
* @typedef TopBunOpts
43-
* @property {string[]|undefined} [ignore] - Array of file/folder patterns to ignore.
4444
* @property {boolean|undefined} [static=true] - Enable/disable static file processing
4545
* @property {boolean|undefined} [metafile=true] - Enable/disable the writing of the esbuild metadata file.
4646
* @property {string[]|undefined} [ignore=[]] - Array of ignore strings
4747
* @property {string[]|undefined} [target=[]] - Array of target strings to pass to esbuild
4848
* @property {boolean|undefined} [buildDrafts=false] - Build draft files with the published:false variable
49+
* @property {string[]|undefined} [copy=[]] - Array of paths to copy their contents into the dest directory
4950
*/
5051

5152
/**
@@ -57,13 +58,15 @@ import { ensureDest } from './helpers/ensure-dest.js'
5758
* @typedef {import('./build-esbuild/index.js').EsBuildStepResults} EsBuildStepResults
5859
* @typedef {import('./build-pages/index.js').PageBuildStepResult} PageBuildStepResult
5960
* @typedef {import('./build-static/index.js').StaticBuildStepResult} StaticBuildStepResult
61+
* @typedef {import('./build-copy/index.js').CopyBuildStepResult} CopyBuildStepResult
6062
*/
6163

6264
/**
6365
* @typedef Results
6466
* @property {SiteData} siteData
6567
* @property {EsBuildStepResults} esbuildResults
6668
* @property {StaticBuildStepResult} [staticResults]
69+
* @property {CopyBuildStepResult} [copyResults]
6770
* @property {PageBuildStepResult} [pageBuildResults]
6871
* @property {BuildStepWarnings} warnings
6972
*/
@@ -112,11 +115,13 @@ export async function builder (src, dest, opts) {
112115
const [
113116
esbuildResults,
114117
staticResults,
118+
copyResults,
115119
] = await Promise.all([
116120
buildEsbuild(src, dest, siteData, opts),
117121
opts.static
118122
? buildStatic(src, dest, siteData, opts)
119123
: Promise.resolve(),
124+
buildCopy(src, dest, siteData, opts),
120125
])
121126

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

143+
errors.push(...copyResults.errors)
144+
warnings.push(...copyResults.warnings)
145+
results.copyResults = copyResults
146+
138147
if (errors.length > 0) {
139148
const preBuildError = new TopBunAggregateError(errors, 'Prebuild finished but there were errors.', results)
140149
throw preBuildError

lib/helpers/top-bun-warning.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* 'TOP_BUN_WARNING_UNKNOWN_PAGE_BUILDER' |
99
* 'TOP_BUN_WARNING_DUPLICATE_GLOBAL_STYLE' |
1010
* 'TOP_BUN_WARNING_DUPLICATE_GLOBAL_CLIENT' |
11-
* 'TOP_BUN_WARNING_DUPLICATE_ESBUILD_SETINGS' |
11+
* 'TOP_BUN_WARNING_DUPLICATE_ESBUILD_SETTINGS' |
1212
* 'TOP_BUN_WARNING_DUPLICATE_GLOBAL_VARS'
1313
* } TopBunWarningCode
1414
*/

0 commit comments

Comments
 (0)