diff --git a/packages/gatsby-remark-code-repls/README.md b/packages/gatsby-remark-code-repls/README.md index 4ff8ffcc67b47..f317848271a6b 100644 --- a/packages/gatsby-remark-code-repls/README.md +++ b/packages/gatsby-remark-code-repls/README.md @@ -163,6 +163,13 @@ specified examples directory. (This will avoid broken links at runtime.) // Note that if a target is specified, "noreferrer" will also be added. // eg ... target: '_blank', + + // Include CSS with matching name. + // This option only applies to REPLs that support it (eg Codepen). + // If set to `true`, when specifying `file1.js` as example file, + // it will try to inject the CSS in `file1.css` if the file exists, + // otherwise the default behaviour is preserved + includeMatchingCSS: false, }, }, ``` diff --git a/packages/gatsby-remark-code-repls/src/__tests__/gatsby-node.js b/packages/gatsby-remark-code-repls/src/__tests__/gatsby-node.js index be21fb86b260e..6eabf27e2e416 100644 --- a/packages/gatsby-remark-code-repls/src/__tests__/gatsby-node.js +++ b/packages/gatsby-remark-code-repls/src/__tests__/gatsby-node.js @@ -30,6 +30,12 @@ const createPagesParams = { reporter, } +const throwFileNotFoundErr = path => { + const err = new Error(`no such file or directory '${path}'`) + err.code = `ENOENT` + throw err +} + describe(`gatsby-remark-code-repls`, () => { beforeEach(() => { fs.existsSync.mockReset() @@ -198,5 +204,105 @@ describe(`gatsby-remark-code-repls`, () => { expect(html).toBe(``) }) + + it(`should support includeMatchingCSS = "true" when matching file exists`, async () => { + readdir.mockResolvedValue([`file.js`, `file.css`]) + fs.readFileSync.mockReset() + fs.readFileSync.mockImplementation((path, options) => { + if (path === `file.js`) { + return `const foo = "bar";` + } else if (path === `file.css`) { + return `html { color: red; }` + } else { + throwFileNotFoundErr(path) + } + return null + }) + + await createPages(createPagesParams, { + includeMatchingCSS: true, + }) + + const { css, js } = JSON.parse( + createPage.mock.calls[0][0].context.payload + ) + + expect(js).toBe(`const foo = "bar";`) + expect(css).toBe(`html { color: red; }`) + }) + + it(`should support includeMatchingCSS = "false" when matching file exists`, async () => { + readdir.mockResolvedValue([`file.js`, `file.css`]) + fs.readFileSync.mockReset() + fs.readFileSync.mockImplementation((path, options) => { + if (path === `file.js`) { + return `const foo = "bar";` + } else if (path === `file.css`) { + return `html { color: red; }` + } else { + throwFileNotFoundErr(path) + } + return null + }) + + await createPages(createPagesParams, { + includeMatchingCSS: false, + }) + + const { css, js } = JSON.parse( + createPage.mock.calls[0][0].context.payload + ) + + expect(js).toBe(`const foo = "bar";`) + expect(css).toBe(undefined) + }) + + it(`should support includeMatchingCSS = "true" when matching file doesn't exist`, async () => { + readdir.mockResolvedValue([`file.js`]) + fs.readFileSync.mockReset() + fs.readFileSync.mockImplementation((path, options) => { + if (path === `file.js`) { + return `const foo = "bar";` + } else { + throwFileNotFoundErr(path) + } + return null + }) + + await createPages(createPagesParams, { + includeMatchingCSS: true, + }) + + const { css, js } = JSON.parse( + createPage.mock.calls[0][0].context.payload + ) + + expect(js).toBe(`const foo = "bar";`) + expect(css).toBe(undefined) + }) + + it(`should support includeMatchingCSS = "false" when matching file doesn't exist`, async () => { + readdir.mockResolvedValue([`file.js`]) + fs.readFileSync.mockReset() + fs.readFileSync.mockImplementation((path, options) => { + if (path === `file.js`) { + return `const foo = "bar";` + } else { + throwFileNotFoundErr(path) + } + return null + }) + + await createPages(createPagesParams, { + includeMatchingCSS: false, + }) + + const { css, js } = JSON.parse( + createPage.mock.calls[0][0].context.payload + ) + + expect(js).toBe(`const foo = "bar";`) + expect(css).toBe(undefined) + }) }) }) diff --git a/packages/gatsby-remark-code-repls/src/__tests__/index.js b/packages/gatsby-remark-code-repls/src/__tests__/index.js index 1256a10ee766f..21c254c299e5e 100644 --- a/packages/gatsby-remark-code-repls/src/__tests__/index.js +++ b/packages/gatsby-remark-code-repls/src/__tests__/index.js @@ -141,6 +141,21 @@ describe(`gatsby-remark-code-repls`, () => { } }) + it(`supports includeMatchingCSS`, () => { + const markdownAST = remark.parse( + `[](${protocol}path/to/nested/file.js)` + ) + const runPlugin = () => + plugin( + { markdownAST }, + { + directory: `examples`, + includeMatchingCSS: true, + } + ) + expect(runPlugin).not.toThrow() + }) + if (protocol === PROTOCOL_CODE_SANDBOX) { it(`supports custom html config option for index html`, () => { const markdownAST = remark.parse( diff --git a/packages/gatsby-remark-code-repls/src/constants.js b/packages/gatsby-remark-code-repls/src/constants.js index 6314b9561d555..a6ef0ea45a5ec 100644 --- a/packages/gatsby-remark-code-repls/src/constants.js +++ b/packages/gatsby-remark-code-repls/src/constants.js @@ -9,6 +9,7 @@ module.exports = { OPTION_DEFAULT_REDIRECT_TEMPLATE_PATH: normalizePath( join(__dirname, `default-redirect-template.js`) ), + OPTION_DEFAULT_INCLUDE_MATCHING_CSS: false, PROTOCOL_BABEL: `babel://`, PROTOCOL_CODEPEN: `codepen://`, PROTOCOL_CODE_SANDBOX: `codesandbox://`, diff --git a/packages/gatsby-remark-code-repls/src/gatsby-node.js b/packages/gatsby-remark-code-repls/src/gatsby-node.js index a18e02fb333a7..69559a16302c5 100644 --- a/packages/gatsby-remark-code-repls/src/gatsby-node.js +++ b/packages/gatsby-remark-code-repls/src/gatsby-node.js @@ -9,6 +9,7 @@ const { OPTION_DEFAULT_LINK_TEXT, OPTION_DEFAULT_HTML, OPTION_DEFAULT_REDIRECT_TEMPLATE_PATH, + OPTION_DEFAULT_INCLUDE_MATCHING_CSS, } = require(`./constants`) exports.createPages = async ( @@ -18,6 +19,7 @@ exports.createPages = async ( externals = [], html = OPTION_DEFAULT_HTML, redirectTemplate = OPTION_DEFAULT_REDIRECT_TEMPLATE_PATH, + includeMatchingCSS = OPTION_DEFAULT_INCLUDE_MATCHING_CSS, } = {} ) => { if (!directory.endsWith(`/`)) { @@ -51,6 +53,18 @@ exports.createPages = async ( .replace(new RegExp(`^${directory}`), `redirect-to-codepen/`) const code = fs.readFileSync(file, `utf8`) + let css + if (includeMatchingCSS === true) { + try { + css = fs.readFileSync(file.replace(extname(file), `.css`), `utf8`) + } catch (err) { + // If the file doesn't exist, we gracefully ignore the error + if (err.code !== `ENOENT`) { + throw err + } + } + } + // Codepen configuration. // https://blog.codepen.io/documentation/api/prefill/ const action = `https://codepen.io/pen/define` @@ -61,6 +75,7 @@ exports.createPages = async ( js_external: externals.join(`;`), js_pre_processor: `babel`, layout: `left`, + css, }) createPage({