Skip to content

Commit 696ed20

Browse files
committed
Sketch out a directory copy feature
1 parent 488c7d7 commit 696ed20

File tree

15 files changed

+175
-5
lines changed

15 files changed

+175
-5
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,36 @@ These imports will include the `root.layout.js` layout assets into the `blog.lay
587587

588588
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.
589589

590+
### 📁 `.copy` directories
591+
592+
If a directory with the `.copy` suffix is found in your `src` directory—like `oldsite.copy/`—its contents will be copied **as-is** into the `dest` directory during build. No processing or transformation is applied to any files inside. That means `.html`, `.js`, `.css`, and all other files are left untouched.
593+
594+
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.
595+
596+
For example:
597+
598+
```
599+
src/
600+
├── oldsite.copy/
601+
│ ├── client.js
602+
│ ├── hello.html
603+
│ └── styles/
604+
│ └── globals.css
605+
```
606+
607+
After build:
608+
609+
```
610+
public/
611+
├── oldsite/
612+
│ ├── client.js
613+
│ ├── hello.html
614+
│ └── styles/
615+
│ └── globals.css
616+
```
617+
618+
You can have multiple `.copy` directories in `src`—they’ll all be handled this way.
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:

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const DEFAULT_IGNORES = /** @type {const} */ ([
5656
'package-lock.json',
5757
'pnpm-lock.yaml',
5858
'yarn.lock',
59+
'*.copy/*'
5960
])
6061

6162
/**

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

lib/build-copy/index.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import tap from 'tap'
2+
import { getCopyDirs } from './index.js'
3+
import { resolve } from 'node:path'
4+
5+
const __dirname = import.meta.dirname
6+
7+
tap.test('getCopyDirs returns correct src/dest pairs', async (t) => {
8+
const src = resolve(__dirname, './fixtures')
9+
const copyDirs = await getCopyDirs(src)
10+
11+
// console.log({ copyDirs })
12+
13+
const expected = [
14+
{ src: 'foo.copy/**/*', dest: 'foo/' },
15+
{ src: 'bing/another.copy/**/*', dest: 'bing/another/' }
16+
]
17+
18+
// Sort by src to avoid order mismatch
19+
t.strictSame(
20+
copyDirs.sort((a, b) => a.src.localeCompare(b.src)),
21+
expected.sort((a, b) => a.src.localeCompare(b.src)),
22+
'The copyDirs should contain the correct src/dest mappings'
23+
)
24+
})

lib/builder.js

Lines changed: 9 additions & 0 deletions
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'
@@ -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
*/

lib/identify-pages.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const shaper = ({
8484
* @typedef TemplateInfo
8585
* @property {WalkerFile} templateFile - The template file info.
8686
* @property {string} path - The the path of the parent dir of the template
87-
* @property {string} outputName - The derived output name of the template file. Might be overriden.
87+
* @property {string} outputName - The derived output name of the template file. Might be overridden.
8888
*/
8989

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

301301
templates.push({
302302
templateFile: fileInfo,
303303
path: dir,
304-
outputName: temlateFileName,
304+
outputName: templateFileName,
305305
})
306306
}
307307

@@ -341,7 +341,7 @@ export async function identifyPages (src, opts = {}) {
341341
if (esbuildSettingsNames.some(name => basename(fileName) === name)) {
342342
if (esbuildSettings) {
343343
warnings.push({
344-
code: 'TOP_BUN_WARNING_DUPLICATE_ESBUILD_SETINGS',
344+
code: 'TOP_BUN_WARNING_DUPLICATE_ESBUILD_SETTINGS',
345345
message: `Skipping ${fileInfo.relname}. Duplicate esbuild options ${fileName} to ${esbuildSettings.filepath}`,
346346
})
347347
} else {

0 commit comments

Comments
 (0)