From 19eab8760899c92a45a2f894a914392eca18579b Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Sat, 10 Aug 2019 02:39:35 -0700 Subject: [PATCH] feat(gatsby-remark-copy-linked-files): change default destinationDir and allow override via config (#16508) * change default destination & make it customizable * document the new option in README --- .../gatsby-remark-copy-linked-files/README.md | 186 ++++++++++++++---- .../src/__tests__/index.js | 82 +++++++- .../src/index.js | 78 +++++--- 3 files changed, 271 insertions(+), 75 deletions(-) diff --git a/packages/gatsby-remark-copy-linked-files/README.md b/packages/gatsby-remark-copy-linked-files/README.md index 777ed1e15731a..3bceae2997170 100644 --- a/packages/gatsby-remark-copy-linked-files/README.md +++ b/packages/gatsby-remark-copy-linked-files/README.md @@ -1,35 +1,88 @@ # gatsby-remark-copy-linked-files -Copies local files linked to/from markdown to your `public` folder. +Copies local files linked to/from Markdown (`.md|.markdown`) files to the root directory (i.e., `public` folder). -## Install +**A sample markdown file:** + +```md +--- +title: My awesome blog post +--- + +Hey everyone, I just made a sweet PDF with lots of interesting stuff in it. + +[Download it now](my-awesome-pdf.pdf) +``` + +**When you build your site:** + +The `my-awesome-pdf.pdf` file will be copied to the root directory (i.e., `public/some-really-long-contenthash/my-awesome-pdf.pdf`) and the generated HTML page will be modified to point to it. + +> **Note**: The `my-awesome-pdf.pdf` file should be in the same directory as the markdown file. + +--- + +## Install plugin `npm install --save gatsby-remark-copy-linked-files` -## How to use +## Add plugin to Gatsby Config + +**Default settings:** -#### Basic usage +Add `gatsby-remark-copy-linked-files` plugin as a plugin to [`gatsby-transformer-remark`](https://www.gatsbyjs.org/packages/gatsby-transformer-remark/): ```javascript // In your gatsby-config.js + +// add plugin by name only plugins: [ { resolve: `gatsby-transformer-remark`, options: { - plugins: ["gatsby-remark-copy-linked-files"], + plugins: [`gatsby-remark-copy-linked-files`], }, }, ] ``` -### How to change the directory the files are added to. +**Custom settings:** -By default, all files will be copied to the root of the `public` dir, but you -can choose a different location using the `destinationDir` option. Provide a -path, relative to the `public` directory. The path must be within the public -directory, so `path/to/dir` is fine, but `../../dir` is not. +```js +// In your gatsby-config.js +// add plugin by name and options +plugins: [ + { + resolve: `gatsby-transformer-remark`, + options: { + plugins: [ + { + resolve: `gatsby-remark-copy-linked-files`, + options: { + destinationDir: `path/to/dir`, + ignoreFileExtensions: [`png`, `jpg`, `jpeg`, `bmp`, `tiff`], + }, + }, + ], + }, + }, +] ``` + +--- + +## Custom set where to copy the files using `destinationDir` + +By default, all files will be copied to the root directory (i.e., `public` folder) in the following format: `contentHash/fileName.ext`. + +> For example, `[Download it now](my-awesome-pdf.pdf)` will copy the file `my-awesome-pdf.pdf` to something like `public/2a0039f3a61f4510f41678438e4c863a/my-awesome-pdf.pdf` + +### Simple usage + +To change this, set `destinationDir` to a path of your own choosing (i.e., `path/to/dir`). + +```js // In your gatsby-config.js plugins: [ { @@ -37,18 +90,95 @@ plugins: [ options: { plugins: [ { - resolve: 'gatsby-remark-copy-linked-files', + resolve: "gatsby-remark-copy-linked-files", options: { - destinationDir: 'path/to/dir', - } - } - ] - } - } + destinationDir: "path/to/dir", + }, + }, + ], + }, + }, ] ``` -### How to override which file types are ignored +> So now, `[Download it now](my-awesome-pdf.pdf)` will copy the file `my-awesome-pdf.pdf` to `public/path/to/dir/2a0039f3a61f4510f41678438e4c863a/my-awesome-pdf.pdf` + +### Advanced usage + +For more advanced control, set `destinationDir` to a function expression using properties `name` and/or `hash` to specify the path. + +**Examples:** + +```js +# save `my-awesome-pdf.pdf` to `public/my-awesome-pdf.pdf` +destinationDir: f => `${f.name}` + +# save `my-awesome-pdf.pdf` to `public/2a0039f3a61f4510f41678438e4c863a.pdf` +destinationDir: f => `${f.hash}` + +# save `my-awesome-pdf.pdf` to `public/downloads/2a0039f3a61f4510f41678438e4c863a/my-awesome-pdf.pdf` +destinationDir: f => `downloads/${f.hash}/${f.name}` + +# save `my-awesome-pdf.pdf` to `public/downloads/2a0039f3a61f4510f41678438e4c863a-my-awesome-pdf.pdf` +destinationDir: f => `downloads/${f.hash}-${f.name}` + +# save `my-awesome-pdf.pdf` to `public/my-awesome-pdf/2a0039f3a61f4510f41678438e4c863a.pdf` +destinationDir: f => `${f.name}/${f.hash}` + +# save `my-awesome-pdf.pdf` to `public/path/to/dir/hello-my-awesome-pdf+2a0039f3a61f4510f41678438e4c863a_world.pdf` +destinationDir: f => `path/to/dir/hello-${f.name}+${f.hash}_world` +``` + +> **Note:** Make sure you use either `name` or `hash` property in your function expression! +> If you don't include both `name` and `hash` properties in your function expression, `gatsby-remark-copy-linked-files` plugin will resolve the function expression to a string value and use default settings as a fallback mechanism to prevent your local files from getting copied with the same name (causing files to get overwritten). + +```js +# Note: `my-awesome-pdf.pdf` is saved to `public/hello/2a0039f3a61f4510f41678438e4c863a/my-awesome-pdf.pdf` +# because `name` and `hash` properties are not referenced in the function expression. +# So these function expressions are treated the same way +destinationDir: _ => `hello` +destinationDir: `hello` +``` + +### Caveat: Error thrown if `destinationDir` points outside the root directory (i.e. `public` folder) + +> **Note:** An error will be thrown if the destination points outside the root directory (i.e. `public` folder). + +**Correct:** + +```js +# saves to `public/path/to/dir/` +destinationDir: `path/to/dir` + +# saves to `public/path/to/dir/` +destinationDir: _ => `path/to/dir` + +# saves to `public/path/to/dir/fileName.ext` +destinationDir: f => `path/to/dir/${f.name}` + +# saves to `public/contentHash.ext` +destinationDir: f => `${f.hash}` +``` + +**Error thrown:** + +```js +# cannot save outside root directory (i.e., outside `public` folder) +destinationDir: `../path/to/dir` +destinationDir: _ => `../path/to/dir` +destinationDir: f => `../path/to/dir/${f.name}` +destinationDir: f => `../${f.hash}` +``` + +--- + +### Custom set which file types to ignore using `ignoreFileExtensions` + +By default, the file types that this plugin ignores are: `png`, `jpg`, `jpeg`, `bmp`, `tiff`. + +> For example, `[Download it now](image.png)` will be ignored and not copied to the root dir (i.e. `public` folder) + +To change this, set `ignoreFileExtensions` to an array of extensions to ignore (i.e., an empty array `[]` to ignore nothing). ```javascript // In your gatsby-config.js @@ -77,28 +207,14 @@ plugins: [ ] ``` -Then in your Markdown files, link to the file you desire to reference. - -E.g. +> So now, `[Download it now](image.png)` will be copied to the root dir (i.e. `public` folder) -```markdown --- -title: My awesome blog post ---- - -Hey everyone, I just made a sweet PDF with lots of interesting stuff in it. - -[Download it now](my-awesome-pdf.pdf) -``` - -`my-awesome-pdf.pdf` should be in the same directory as the markdown file. When -you build your site, the file will be copied to the `public` folder and the -markdown HTML will be modified to point to it. ### Supported Markdown tags -- img -- link +- img - `![Image](my-img.png)` +- link - `[Link](myFile.txt)` ### Supported HTML tags diff --git a/packages/gatsby-remark-copy-linked-files/src/__tests__/index.js b/packages/gatsby-remark-copy-linked-files/src/__tests__/index.js index 811abb528927f..46f2f585b923b 100644 --- a/packages/gatsby-remark-copy-linked-files/src/__tests__/index.js +++ b/packages/gatsby-remark-copy-linked-files/src/__tests__/index.js @@ -265,7 +265,7 @@ describe(`gatsby-remark-copy-linked-files`, () => { describe(`options.destinationDir`, () => { const imagePath = `images/sample-image.gif` - it(`throws an error if the destination directory is not within 'public'`, async () => { + it(`throws an error if the destination supplied by destinationDir points outside of the root dir`, async () => { const markdownAST = remark.parse(`![some absolute image](${imagePath})`) const invalidDestinationDir = `../destination` expect.assertions(2) @@ -280,14 +280,31 @@ describe(`gatsby-remark-copy-linked-files`, () => { }) }) - it(`copies file to destinationDir when supplied`, async () => { + it(`throws an error if the destination supplied by the destinationDir function points outside of the root dir`, async () => { + const markdownAST = remark.parse(`![some absolute image](${imagePath})`) + const invalidDestinationDir = `../destination` + const customDestinationDir = f => + `../destination/${f.hash}/${f.name}/${f.notexist}` + expect.assertions(2) + return plugin( + { files: getFiles(imagePath), markdownAST, markdownNode, getNode }, + { + destinationDir: customDestinationDir, + } + ).catch(e => { + expect(e).toEqual(expect.stringContaining(invalidDestinationDir)) + expect(fsExtra.copy).not.toHaveBeenCalled() + }) + }) + + it(`copies file to the destination supplied by destinationDir`, async () => { const markdownAST = remark.parse(`![some absolute image](${imagePath})`) const validDestinationDir = `path/to/dir` const expectedNewPath = path.posix.join( process.cwd(), `public`, validDestinationDir, - `/undefined-undefined.gif` + `/undefined/undefined.gif` ) expect.assertions(3) await plugin( @@ -299,12 +316,30 @@ describe(`gatsby-remark-copy-linked-files`, () => { expect(v).toBeDefined() expect(fsExtra.copy).toHaveBeenCalledWith(imagePath, expectedNewPath) expect(imageURL(markdownAST)).toEqual( - `/path/to/dir/undefined-undefined.gif` + `/path/to/dir/undefined/undefined.gif` + ) + }) + }) + + it(`copies file to the destination supplied by the destinationDir function`, async () => { + const markdownAST = remark.parse(`![some absolute image](${imagePath})`) + const customDestinationDir = f => `foo/${f.hash}--bar` + const expectedDestination = `foo/undefined--bar.gif` + expect.assertions(3) + await plugin( + { files: getFiles(imagePath), markdownAST, markdownNode, getNode }, + { destinationDir: customDestinationDir } + ).then(v => { + const expectedNewPath = path.posix.join( + ...[process.cwd(), `public`, expectedDestination] ) + expect(v).toBeDefined() + expect(fsExtra.copy).toHaveBeenCalledWith(imagePath, expectedNewPath) + expect(imageURL(markdownAST)).toEqual(`/${expectedDestination}`) }) }) - it(`copies file to destinationDir when supplied (with pathPrefix)`, async () => { + it(`copies file to the destination supplied by destinationDir (with pathPrefix)`, async () => { const markdownAST = remark.parse(`![some absolute image](${imagePath})`) const pathPrefix = `/blog` const validDestinationDir = `path/to/dir` @@ -312,7 +347,7 @@ describe(`gatsby-remark-copy-linked-files`, () => { process.cwd(), `public`, validDestinationDir, - `/undefined-undefined.gif` + `/undefined/undefined.gif` ) expect.assertions(3) await plugin( @@ -330,17 +365,44 @@ describe(`gatsby-remark-copy-linked-files`, () => { expect(v).toBeDefined() expect(fsExtra.copy).toHaveBeenCalledWith(imagePath, expectedNewPath) expect(imageURL(markdownAST)).toEqual( - `${pathPrefix}/path/to/dir/undefined-undefined.gif` + `${pathPrefix}/path/to/dir/undefined/undefined.gif` + ) + }) + }) + + it(`copies file to the destination supplied by the destinationDir function (with pathPrefix)`, async () => { + const markdownAST = remark.parse(`![some absolute image](${imagePath})`) + const pathPrefix = `/blog` + const customDestinationDir = f => `hello${f.name}123` + const expectedDestination = `helloundefined123.gif` + expect.assertions(3) + await plugin( + { + files: getFiles(imagePath), + markdownAST, + markdownNode, + pathPrefix, + getNode, + }, + { destinationDir: customDestinationDir } + ).then(v => { + const expectedNewPath = path.posix.join( + ...[process.cwd(), `public`, expectedDestination] + ) + expect(v).toBeDefined() + expect(fsExtra.copy).toHaveBeenCalledWith(imagePath, expectedNewPath) + expect(imageURL(markdownAST)).toEqual( + `${pathPrefix}/${expectedDestination}` ) }) }) - it(`copies file to root dir when not supplied'`, async () => { + it(`copies file to the root dir when destinationDir is not supplied'`, async () => { const markdownAST = remark.parse(`![some absolute image](${imagePath})`) const expectedNewPath = path.posix.join( process.cwd(), `public`, - `/undefined-undefined.gif` + `/undefined/undefined.gif` ) expect.assertions(3) await plugin({ @@ -351,7 +413,7 @@ describe(`gatsby-remark-copy-linked-files`, () => { }).then(v => { expect(v).toBeDefined() expect(fsExtra.copy).toHaveBeenCalledWith(imagePath, expectedNewPath) - expect(imageURL(markdownAST)).toEqual(`/undefined-undefined.gif`) + expect(imageURL(markdownAST)).toEqual(`/undefined/undefined.gif`) }) }) }) diff --git a/packages/gatsby-remark-copy-linked-files/src/index.js b/packages/gatsby-remark-copy-linked-files/src/index.js index 402a5c9a5055a..640cecb17a037 100644 --- a/packages/gatsby-remark-copy-linked-files/src/index.js +++ b/packages/gatsby-remark-copy-linked-files/src/index.js @@ -12,38 +12,56 @@ const DEPLOY_DIR = `public` const invalidDestinationDirMessage = dir => `[gatsby-remark-copy-linked-files You have supplied an invalid destination directory. The destination directory must be a child but was: ${dir}` -// dir must be a child -const destinationDirIsValid = dir => !path.relative(`./`, dir).startsWith(`..`) - -const validateDestinationDir = dir => - !dir || (dir && destinationDirIsValid(dir)) - -const newFileName = linkNode => - `${linkNode.name}-${linkNode.internal.contentDigest}.${linkNode.extension}` - -const newPath = (linkNode, destinationDir) => { - if (destinationDir) { - return path.posix.join( - process.cwd(), - DEPLOY_DIR, - destinationDir, - newFileName(linkNode) - ) +// dest must be a child +const destinationIsValid = dest => !path.relative(`./`, dest).startsWith(`..`) + +const validateDestinationDir = dir => { + if (typeof dir === `undefined`) { + return true + } else if (typeof dir === `string`) { + // need to pass dummy data for validation to work + return destinationIsValid(`${dir}/h/n`) + } else if (_.isFunction(dir)) { + // need to pass dummy data for validation to work + return destinationIsValid(`${dir({ name: `n`, hash: `h` })}`) + } else { + return false } - return path.posix.join(process.cwd(), DEPLOY_DIR, newFileName(linkNode)) } -const newLinkURL = (linkNode, destinationDir, pathPrefix) => { - const linkPaths = [ - `/`, - pathPrefix, - destinationDir, - newFileName(linkNode), - ].filter(function(lpath) { - if (lpath) return true - return false - }) +const defaultDestination = linkNode => + `${linkNode.internal.contentDigest}/${linkNode.name}.${linkNode.extension}` + +const getDestination = (linkNode, dir) => { + if (_.isFunction(dir)) { + // need to pass dummy data for validation to work + const isValidFunction = `${dir({ name: `n`, hash: `h` })}` !== `${dir({})}` + return isValidFunction + ? `${dir({ + name: linkNode.name, + hash: linkNode.internal.contentDigest, + })}.${linkNode.extension}` + : `${dir()}/${defaultDestination(linkNode)}` + } else if (_.isString(dir)) { + return `${dir}/${defaultDestination(linkNode)}` + } else { + return defaultDestination(linkNode) + } +} + +const newPath = (linkNode, options) => { + const { destinationDir } = options + const destination = getDestination(linkNode, destinationDir) + const paths = [process.cwd(), DEPLOY_DIR, destination] + return path.posix.join(...paths) +} +const newLinkURL = (linkNode, options, pathPrefix) => { + const { destinationDir } = options + const destination = getDestination(linkNode, destinationDir) + const linkPaths = [`/`, pathPrefix, destination].filter(lpath => + lpath ? true : false + ) return path.posix.join(...linkPaths) } @@ -89,12 +107,12 @@ module.exports = ( return null }) if (linkNode && linkNode.absolutePath) { - const newFilePath = newPath(linkNode, options.destinationDir) + const newFilePath = newPath(linkNode, options) // Prevent uneeded copying if (linkPath === newFilePath) return - const linkURL = newLinkURL(linkNode, options.destinationDir, pathPrefix) + const linkURL = newLinkURL(linkNode, options, pathPrefix) link.url = linkURL filesToCopy.set(linkPath, newFilePath) }