diff --git a/README.md b/README.md index 008f8fb..4c552d7 100644 --- a/README.md +++ b/README.md @@ -710,11 +710,11 @@ When you run `domstack --eject`, it will: It is recomended to eject early in your project so that you can customize the root layout as you see fit, and de-couple yourself from potential unwanted changes in the default layout as new versions of DOMStack are released. -### `--copy` directories +### `--copy` directories and files -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. +You can specify directories or individual files 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. Individual files (such as a `_redirects` or `sw.js`) are copied to the root of the destination. Conflicting files are not detected or reported and will cause undefined behavior. -Copy folders must live **outside** of the `dest` directory. Copy directories can be in the src directory allowing for nested builds. In this case they are added to the ignore glob and ignored by the rest of `domstack`. +Copy paths must live **outside** of the `dest` directory. Copy directories can be in the src directory allowing for nested builds. In this case they are added to the ignore glob and ignored by the rest of `domstack`. This is useful when you have legacy or archived site content that you want to include in your site, but don't want `domstack` 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 domstack build is sometimes easier to reason about when it's kept in a separate folder and isn't processed in any way. diff --git a/bin.js b/bin.js index 560ef5d..ac31707 100755 --- a/bin.js +++ b/bin.js @@ -81,7 +81,7 @@ const options = { }, copy: { type: 'string', - help: 'path to directories to copy into dist; can be used multiple times', + help: 'path to directories or individual files to copy into dist; can be used multiple times', multiple: true }, help: { diff --git a/index.js b/index.js index 3a062db..91a810f 100644 --- a/index.js +++ b/index.js @@ -247,7 +247,7 @@ export class DomStack { await this.#rebuildMaps(siteData) // ── Copy watchers & browser-sync ───────────────────────────────────── - const copyDirs = getCopyDirs(this.opts.copy) + const copyDirs = await getCopyDirs(this.opts.copy) this.#cpxWatchers = [ cpx.watch(getCopyGlob(this.#src), this.#dest, { ignore: this.opts.ignore }), diff --git a/lib/build-copy/index.js b/lib/build-copy/index.js index 5b81b9e..c8c483d 100644 --- a/lib/build-copy/index.js +++ b/lib/build-copy/index.js @@ -5,6 +5,7 @@ // @ts-expect-error import cpx from 'cpx2' import { join } from 'node:path' +import { stat } from 'node:fs/promises' const copy = cpx.copy /** @@ -15,11 +16,22 @@ const copy = cpx.copy /** * @param {string[]} copy - * @return {string[]} + * @return {Promise} */ -export function getCopyDirs (copy = []) { - const copyGlobs = copy?.map((dir) => join(dir, '**')) - return copyGlobs +export async function getCopyDirs (copy = []) { + const globs = await Promise.all(copy.map(async (entry) => { + try { + const stats = await stat(entry) + if (stats.isDirectory()) { + return join(entry, '**') + } + } catch { + // Path not accessible yet — treat as directory glob + return join(entry, '**') + } + return entry + })) + return globs } /** @@ -36,22 +48,22 @@ export async function buildCopy (_src, dest, _siteData, opts) { warnings: [], } - const copyDirs = getCopyDirs(opts?.copy) + const copyGlobs = await getCopyDirs(opts?.copy) - const copyTasks = copyDirs.map((copyDir) => { - return copy(copyDir, dest) + const copyTasks = copyGlobs.map((copyGlob) => { + return copy(copyGlob, dest) }) const settled = await Promise.allSettled(copyTasks) for (const [index, result] of Object.entries(settled)) { // @ts-expect-error - const copyDir = copyDirs[index] + const copyGlob = copyGlobs[index] if (result.status === 'rejected') { - const buildError = new Error('Error copying copy folders', { cause: result.reason }) + const buildError = new Error(`Error copying copy path: ${copyGlob}`, { cause: result.reason }) results.errors.push(buildError) } else { - results.report[copyDir] = result.value + results.report[copyGlob] = result.value } } return results diff --git a/lib/build-copy/index.test.js b/lib/build-copy/index.test.js index 4651210..595a0ca 100644 --- a/lib/build-copy/index.test.js +++ b/lib/build-copy/index.test.js @@ -1,11 +1,41 @@ import { test } from 'node:test' import assert from 'node:assert' +import { mkdtemp, writeFile, rm } from 'node:fs/promises' +import { join } from 'node:path' +import os from 'node:os' import { getCopyDirs } from './index.js' test.describe('build-copy', () => { - test('getCopyDirs returns correct src/dest pairs', async () => { - const copyDirs = getCopyDirs(['fixtures']) + test('getCopyDirs appends ** for non-existent paths', async () => { + const dir = await mkdtemp(join(os.tmpdir(), 'domstack-copy-test-')) + const missingPath = join(dir, 'does-not-exist') + try { + const copyDirs = await getCopyDirs([missingPath]) + assert.deepStrictEqual(copyDirs, [join(missingPath, '**')]) + } finally { + await rm(dir, { recursive: true, force: true }) + } + }) + + test('getCopyDirs returns file path as-is for existing files', async () => { + const dir = await mkdtemp(join(os.tmpdir(), 'domstack-copy-test-')) + const file = join(dir, 'sw.js') + try { + await writeFile(file, 'self.addEventListener("fetch", () => {})') + const result = await getCopyDirs([file]) + assert.deepStrictEqual(result, [file]) + } finally { + await rm(dir, { recursive: true, force: true }) + } + }) - assert.deepStrictEqual(copyDirs, ['fixtures/**']) + test('getCopyDirs appends ** for existing directories', async () => { + const dir = await mkdtemp(join(os.tmpdir(), 'domstack-copy-test-')) + try { + const result = await getCopyDirs([dir]) + assert.deepStrictEqual(result, [join(dir, '**')]) + } finally { + await rm(dir, { recursive: true, force: true }) + } }) })