diff --git a/CHANGELOG.md b/CHANGELOG.md index 83bbc211..3bdf2956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ Any BREAKING CHANGE between minor versions will be documented here in upper case ### Fixed -- `Site.copy` now works as expected when given a path with a trailing slash. [#426] +- `Site.copy` now supports trailing slashes in the source path. [#426] +- Multiple `Site.copy` calls can now be used to copy one file to multiple destinations. [#429] ## [1.17.4] - 2023-05-25 ### Added @@ -2261,6 +2262,7 @@ The first version. [#418]: https://github.com/lumeland/lume/issues/418 [#419]: https://github.com/lumeland/lume/issues/419 [#426]: https://github.com/lumeland/lume/pull/426 +[#429]: https://github.com/lumeland/lume/pull/429 [1.17.4]: https://github.com/lumeland/lume/compare/v1.17.3...v1.17.4 [1.17.3]: https://github.com/lumeland/lume/compare/v1.17.2...v1.17.3 diff --git a/core/fs.ts b/core/fs.ts index 4e5a5103..7df126cd 100644 --- a/core/fs.ts +++ b/core/fs.ts @@ -12,11 +12,17 @@ export interface Options { export type Loader = (path: string) => Promise; export class Entry { + /** The name of the file/dir. */ name: string; + /** The normalized path of the file/dir. */ path: string; + /** The type of the entry. */ type: EntryType; + /** The absolute file path. */ src: string; + /** The children of the entry. */ children = new Map(); + /** Temporary flags that are cleared when a file is modified. */ flags = new Set(); #content = new Map | Data>(); #info?: Deno.FileInfo; diff --git a/core/source.ts b/core/source.ts index 864e615d..9b0597e3 100644 --- a/core/source.ts +++ b/core/source.ts @@ -58,10 +58,11 @@ export default class Source { prettyUrls: boolean; /** List of static files and folders to copy */ - staticPaths = new Map< - string, - { dest: string | ((path: string) => string) | undefined; dirOnly: boolean } - >(); + staticPaths: { + from: string; + to: string | ((path: string) => string) | undefined; + dirOnly: boolean; + }[] = []; /** List of static files and folders to copy */ copyRemainingFiles?: (path: string) => string | boolean; @@ -102,10 +103,10 @@ export default class Source { } addStaticPath(from: string, to?: string | ((path: string) => string)) { - this.staticPaths.set( - normalizePath(from.replace(/\/$/, "")), + this.staticPaths.push( { - dest: typeof to === "string" ? normalizePath(to) : to, + from: normalizePath(from.replace(/\/$/, "")), + to: typeof to === "string" ? normalizePath(to) : to, dirOnly: from.endsWith("/"), }, ); @@ -118,12 +119,42 @@ export default class Source { const pages: Page[] = []; const staticFiles: StaticFile[] = []; + // Resolve staticPaths to staticFiles + for (const instruction of this.staticPaths) { + const entry = this.fs.entries.get(instruction.from); + if (entry) { + const path = posix.dirname(entry.path); + + if (entry.type == "file") { + if (instruction.dirOnly) { + continue; + } + staticFiles.push({ + entry, + outputPath: getOutputPath( + entry, + path, + instruction.to, + ), + }); + } else { + const dest = instruction.to; + staticFiles.push(...this.#getStaticFiles( + entry, + typeof dest === "string" ? dest : posix.join(path, entry.name), + typeof dest === "function" ? dest : undefined, + )); + } + } + } + await this.#build( buildFilters, this.fs.entries.get("/")!, "/", globalComponents, {}, + new Set(this.staticPaths.map((p) => p.from)), pages, staticFiles, ); @@ -140,6 +171,7 @@ export default class Source { path: string, parentComponents: Components, parentData: Data, + staticPaths: Set, pages: Page[], staticFiles: StaticFile[], ) { @@ -191,26 +223,8 @@ export default class Source { continue; } - // Static files - if (this.staticPaths.has(entry.path)) { - const { dest, dirOnly } = this.staticPaths.get(entry.path)!; - - if (entry.type === "file") { - if (dirOnly) { - continue; - } - staticFiles.push({ - entry, - outputPath: getOutputPath(entry, path, dest), - }); - continue; - } - - staticFiles.push(...this.#getStaticFiles( - entry, - typeof dest === "string" ? dest : posix.join(path, entry.name), - typeof dest === "function" ? dest : undefined, - )); + if (staticPaths.has(entry.path)) { + // The static file has already been resolved. continue; } @@ -311,6 +325,7 @@ export default class Source { path, parentComponents, dirData, + staticPaths, pages, staticFiles, ); @@ -691,13 +706,18 @@ export function getOutputPath( path: string, dest?: string | ((path: string) => string), ): string { - if (typeof dest === "function") { - return dest(posix.join(path, entry.name)); - } - if (typeof dest === "string") { return dest; } - return posix.join(path, entry.name); + const outputPath = posix.join( + path.split("/").map((comp) => parseDate(comp)[0]).join("/"), + entry.name, + ); + + if (typeof dest === "function") { + return dest(outputPath); + } + + return outputPath; } diff --git a/core/writer.ts b/core/writer.ts index 081f6b3e..7e336cdc 100644 --- a/core/writer.ts +++ b/core/writer.ts @@ -112,6 +112,7 @@ export default class Writer { /** * Copy the static files in the dest folder + * Returns the static files that have been successfully copied */ async copyFiles(files: StaticFile[]): Promise { const copyFiles: StaticFile[] = []; @@ -135,11 +136,11 @@ export default class Writer { async copyFile(file: StaticFile): Promise { const { entry } = file; - if (entry.flags.has("saved")) { + if (entry.flags.has("saved-" + file.outputPath)) { return false; } - entry.flags.add("saved"); + entry.flags.add("saved-" + file.outputPath); const pathTo = posix.join(this.dest, file.outputPath); diff --git a/tests/__snapshots__/static_files.test.ts.snap b/tests/__snapshots__/static_files.test.ts.snap index 19db3953..2b9cb23a 100644 --- a/tests/__snapshots__/static_files.test.ts.snap +++ b/tests/__snapshots__/static_files.test.ts.snap @@ -91,6 +91,16 @@ snapshot[`Copy static files 2`] = ` flags: [], outputPath: "/_headers", }, + { + entry: "/one.yes", + flags: [], + outputPath: "/one.yes", + }, + { + entry: "/one.yes", + flags: [], + outputPath: "/one-again.yes", + }, { entry: "/other/one", flags: [], @@ -166,6 +176,11 @@ snapshot[`Copy static files 2`] = ` flags: [], outputPath: "/script/app/main.js", }, + { + entry: "/static/_redirects", + flags: [], + outputPath: "/_redirects", + }, { entry: "/static/one.yes", flags: [], diff --git a/tests/config.test.ts b/tests/config.test.ts index f9600c79..5b7fce04 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -23,20 +23,19 @@ Deno.test("static files configuration", () => { const { staticPaths } = site.source; site.copy("img"); - equals(staticPaths.size, 1); - equals(staticPaths.has("/img"), true); - equals(staticPaths.get("/img")!.dest, undefined); + equals(staticPaths.length, 1); + equals(staticPaths[0].from, "/img"); + equals(staticPaths[0].to, undefined); site.copy("statics/favicon.ico", "favicon.ico"); - equals(staticPaths.size, 2); - equals( - staticPaths.get("/statics/favicon.ico")!.dest, - "/favicon.ico", - ); + equals(staticPaths.length, 2); + equals(staticPaths[1].from, "/statics/favicon.ico"); + equals(staticPaths[1].to, "/favicon.ico"); site.copy("css", "."); - equals(staticPaths.size, 3); - equals(staticPaths.get("/css")!.dest, "/"); + equals(staticPaths.length, 3); + equals(staticPaths[2].from, "/css"); + equals(staticPaths[2].to, "/"); }); Deno.test("ignored files configuration", () => { diff --git a/tests/static_files.test.ts b/tests/static_files.test.ts index cebec113..edbbd637 100644 --- a/tests/static_files.test.ts +++ b/tests/static_files.test.ts @@ -21,6 +21,10 @@ Deno.test("Copy static files", async (t) => { (file) => "/subdir" + file.replace(/\.copy2/, ".copy3"), ); + // a single file can be copied multiple times + site.copy("one.yes"); + site.copy("one.yes", "one-again.yes"); + // copied with the trailing slash site.copy("other2/");