Skip to content

Commit 1113c96

Browse files
committed
Sketch out a directory copy feature
1 parent 488c7d7 commit 1113c96

File tree

17 files changed

+181
-24
lines changed

17 files changed

+181
-24
lines changed

README.md

Lines changed: 30 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,35 @@ 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 folders to copy into your `dest` folder using the `--copy` flag. Everything in those folders will be copied as-is into the destination. Conflicting files may cause undefined behavior.
594+
595+
Copy folders should generally 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.
598+
599+
For example:
600+
601+
```
602+
src/
603+
oldsite/
604+
├── client.js
605+
├── hello.html
606+
└── styles/
607+
└── globals.css
608+
```
609+
610+
After build:
611+
612+
```
613+
public/
614+
├── client.js
615+
├── hello.html
616+
└── styles/
617+
└── globals.css
618+
```
619+
590620
## Templates
591621

592622
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: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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

@@ -56,6 +57,7 @@ const DEFAULT_IGNORES = /** @type {const} */ ([
5657
'package-lock.json',
5758
'pnpm-lock.yaml',
5859
'yarn.lock',
60+
'*.copy/*'
5961
])
6062

6163
/**
@@ -66,7 +68,7 @@ export class TopBun {
6668
/** @type {string} */ #dest = ''
6769
/** @type {Readonly<CurrentOpts & { ignore: string[] }>} */ opts
6870
/** @type {FSWatcher?} */ #watcher = null
69-
/** @type {any?} */ #cpxWatcher = null
71+
/** @type {any[]?} */ #cpxWatchers = null
7072
/** @type {browserSync.BrowserSyncInstance?} */ #browserSyncServer = null
7173

7274
/**
@@ -126,7 +128,12 @@ export class TopBun {
126128
report = err.results
127129
}
128130

129-
this.#cpxWatcher = cpx.watch(getCopyGlob(this.#src), this.#dest, { ignore: this.opts.ignore })
131+
const copyDirs = getCopyDirs(this.opts.copy)
132+
133+
this.#cpxWatchers = [
134+
cpx.watch(getCopyGlob(this.#src), this.#dest, { ignore: this.opts.ignore }),
135+
...copyDirs.map(copyDir => cpx.watch(copyDir, this.#dest))
136+
]
130137
if (serve) {
131138
const bs = browserSync.create()
132139
this.#browserSyncServer = bs
@@ -136,20 +143,22 @@ export class TopBun {
136143
})
137144
}
138145

139-
this.#cpxWatcher.on('watch-ready', () => {
140-
console.log('Copy watcher ready')
146+
this.#cpxWatchers.forEach(w => {
147+
w.on('watch-ready', () => {
148+
console.log('Copy watcher ready')
141149

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

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

151-
this.#cpxWatcher.on('watch-error', (/** @type{Error} */err) => {
152-
console.log(`Copy error: ${err.message}`)
158+
w.on('watch-error', (/** @type{Error} */err) => {
159+
console.log(`Copy error: ${err.message}`)
160+
})
161+
})
153162
})
154163

155164
const ig = ignore().add(this.opts.ignore ?? [])
@@ -204,11 +213,13 @@ export class TopBun {
204213
}
205214

206215
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()
216+
if ((!this.watching || !this.#cpxWatchers)) throw new Error('Not watching')
217+
if (this.#watcher) this.#watcher.close()
218+
this.#cpxWatchers.forEach(w => {
219+
w.close()
220+
})
210221
this.#watcher = null
211-
this.#cpxWatcher = null
222+
this.#cpxWatchers = null
212223
this.#browserSyncServer?.exit() // This will kill the process
213224
this.#browserSyncServer = null
214225
}

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.src] = 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)