From 89245d87e87a942cfcab4568c8899321f6cccc46 Mon Sep 17 00:00:00 2001 From: mark-tate <143323+mark-tate@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:04:24 +0100 Subject: [PATCH] Add document assets plugin (#656) * add `DocumentAssetPlugin` The `DocumentAssetsPlugin` is responsible for copying assets from a document sub-directory to the public folder of your site. This is particularly useful for co-locating images within your document structure and referencing them from documents using relative paths. * - switch `assetSubDirs` to an array of globs - remove `isRelativePath` and replace with `path.isAbsolute` * add checks that we do not traverse outside the working directory --- .changeset/weak-suits-shake.md | 8 + .gitignore | 1 + .prettierignore | 1 + .../plugins/document-assets-plugin.mdx | 73 +++++++ docs/configure/plugins/images/mosaic.jpg | Bin 0 -> 25909 bytes .../plugins/public-assets-plugin.mdx | 4 +- packages/plugins/package.json | 3 +- packages/plugins/src/DocumentAssetsPlugin.ts | 183 ++++++++++++++++++ .../__tests__/DocumentAssetsPlugin.test.ts | 168 ++++++++++++++++ packages/site/mosaic.config.mjs | 10 + yarn.lock | 2 +- 11 files changed, 449 insertions(+), 4 deletions(-) create mode 100644 .changeset/weak-suits-shake.md create mode 100644 docs/configure/plugins/document-assets-plugin.mdx create mode 100644 docs/configure/plugins/images/mosaic.jpg create mode 100644 packages/plugins/src/DocumentAssetsPlugin.ts create mode 100644 packages/plugins/src/__tests__/DocumentAssetsPlugin.test.ts diff --git a/.changeset/weak-suits-shake.md b/.changeset/weak-suits-shake.md new file mode 100644 index 00000000..61b471f5 --- /dev/null +++ b/.changeset/weak-suits-shake.md @@ -0,0 +1,8 @@ +--- +'@jpmorganchase/mosaic-plugins': patch +'@jpmorganchase/mosaic-site': patch +--- + +Add DocumentAssetPlugin + +The `DocumentAssetsPlugin` is responsible for copying assets from a document sub-directory to the public folder of your site. This is particularly useful for co-locating images within your document structure and referencing them from documents using relative paths. diff --git a/.gitignore b/.gitignore index 7599c55a..cf3946ce 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ tsconfig.tsbuildinfo # Deployment packages/rig +packages/site/public/images # Test Results coverage diff --git a/.prettierignore b/.prettierignore index c4169c7e..425feed8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,6 +10,7 @@ patches LICENSE *.png *.hbs +*.jpg **/build **/dist diff --git a/docs/configure/plugins/document-assets-plugin.mdx b/docs/configure/plugins/document-assets-plugin.mdx new file mode 100644 index 00000000..2adc6620 --- /dev/null +++ b/docs/configure/plugins/document-assets-plugin.mdx @@ -0,0 +1,73 @@ +--- +title: DocumentAssetsPlugin +layout: DetailOverview +--- + +# {meta.title} + +The `DocumentAssetsPlugin` is responsible for copying assets from a document sub-directory to the public folder of your site. This is particularly useful for co-locating images within your document structure and referencing them from documents using relative paths. + +## Co-locating Images + +A common use case is to store images within the same directory structure as your documents. This allows you to reference images using relative paths. + +For example, to load an image (`mosaic.jpg`) from a sub-directory called `images`, relative to the document's path, you can use the following Markdown: + +```markdown +![alt text](./images/mosaic.jpg) +``` + +This will render the image as follows: +![alt text](./images/mosaic.jpg) + +## Centralized Image Directory + +Alternatively, if you prefer to store all your images in a common parent directory, you can reference them using a relative path that navigates up the directory structure. + +For example, to load an image from a common parent directory, you can use: + +``` +![alt text](../../images/mosaic.jpg) +``` + +## Handling Absolute Paths and URLs + +The plugin ignores image paths that start with a leading slash (/) or are fully qualified URLs. This ensures that only relative paths are processed and copied to the public folder. + +``` +![alt text](/images/mosaic.jpg) +![alt text](https://www.saltdesignsystem.com/img/hero_image.svg) +``` + +## Priority + +This plugin runs with a priority of -1 so it runs _after_ most other plugins. + +## Options + +| Property | Description | Default | +| ------------ | ---------------------------------------------------------------------------------- | ------------- | +| srcDir | The path where pages reside **after** cloning or when running locally | './docs' | +| outputDir | There path to your site's public images directory where you want to put the images | './public' | +| assetSubDirs | An array of subdirectory globs that could contain assets | ['**/images'] | +| imagesPrefix | The prefix that is added to all new paths | '/images' | + +## Adding to Mosaic + +This plugin is **not** included in the mosaic config shipped by the Mosaic standard generator so it must be added manually to the `plugins` collection: + +```js +plugins: [ + { + modulePath: '@jpmorganchase/mosaic-plugins/DocumentAssetsPlugin', + priority: -1, + options: { + srcDir: `../../docs`, + outputDir: './public/images/mosaic', + assetSubDirs: ['**/images'], + imagesPrefix: '/images' + } + } + // other plugins +]; +``` diff --git a/docs/configure/plugins/images/mosaic.jpg b/docs/configure/plugins/images/mosaic.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3b8b0bcd1c8fdda1b8f8268724ccb94077c50164 GIT binary patch literal 25909 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 literal 0 HcmV?d00001 diff --git a/docs/configure/plugins/public-assets-plugin.mdx b/docs/configure/plugins/public-assets-plugin.mdx index a00b74ec..165b002f 100644 --- a/docs/configure/plugins/public-assets-plugin.mdx +++ b/docs/configure/plugins/public-assets-plugin.mdx @@ -7,11 +7,11 @@ layout: DetailOverview The `PublicAssetsPlugin` is responsible for finding "assets" in the Mosaic filesystem and copying them to another directory. -Typical usecase is for copying `sitemap.xml` and `search-data.json` to the public directory of a Next.js site as these are considered [static assets](https://nextjs.org/docs/pages/building-your-application/optimizing/static-assets) for Next.js. +Typical use-case is for copying `sitemap.xml` and `search-data.json` to the public directory of a Next.js site as these are considered [static assets](https://nextjs.org/docs/pages/building-your-application/optimizing/static-assets) for Next.js. ## Priority -This plugin runs with no special priority. +This plugin runs with a priority of -1 so it runs _after_ most other plugins. ## Options diff --git a/packages/plugins/package.json b/packages/plugins/package.json index 367ca96b..6af76703 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -35,7 +35,8 @@ "directory": "packages/plugins" }, "devDependencies": { - "@types/fs-extra": "^9.0.13" + "@types/fs-extra": "^9.0.13", + "mock-fs": "^5.2.0" }, "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", diff --git a/packages/plugins/src/DocumentAssetsPlugin.ts b/packages/plugins/src/DocumentAssetsPlugin.ts new file mode 100644 index 00000000..aca1f715 --- /dev/null +++ b/packages/plugins/src/DocumentAssetsPlugin.ts @@ -0,0 +1,183 @@ +import type { Page, Plugin as PluginType } from '@jpmorganchase/mosaic-types'; +import fsExtra from 'fs-extra'; +import glob from 'fast-glob'; +import path from 'path'; +import { escapeRegExp } from 'lodash-es'; +import { unified } from 'unified'; +import { visit } from 'unist-util-visit'; +import remarkParse from 'remark-parse'; +import remarkStringify from 'remark-stringify'; +import { VFile } from 'vfile'; + +interface DocumentAssetsPluginOptions { + /** + * An array of subdirectory globs that could contain assets + * @default: ['**\/images'] + */ + assetSubDirs?: string[]; + /** + * The source path, where your docs reside, when the site runs + */ + srcDir?: string; + /** + * The directory to copy matched assets to, typically the site's public directory + * @default './public' + */ + outputDir?: string; + /** + * The prefix we add to all images in documents, so that it routes to the public directory + * @default '/images' + */ + imagesPrefix?: string; +} + +function isUrl(assetPath: string): boolean { + try { + new URL(assetPath); + return true; + } catch (_err) {} + return false; +} + +const createPageTest = (ignorePages: string[], pageExtensions: string[]) => { + const extTest = new RegExp(`${pageExtensions.map(ext => escapeRegExp(ext)).join('|')}$`); + const ignoreTest = new RegExp(`${ignorePages.map(ignore => escapeRegExp(ignore)).join('|')}$`); + + return (file: string) => + !ignoreTest.test(file) && extTest.test(file) && !path.basename(file).startsWith('.'); +}; + +function remarkRewriteImagePaths(newPrefix: string) { + return (tree: any) => { + visit(tree, 'image', (node: any) => { + if (node.url) { + if (isUrl(node.url) || /^\//.test(node.url)) { + // Absolute URL or path, do nothing + return; + } else { + const isRelativePath = !isUrl(node.url) && !path.isAbsolute(node.url); + const assetPath = isRelativePath ? node.url : `./${node.url}`; + const resolvedPath = path.resolve(path.dirname(newPrefix), assetPath); + node.url = resolvedPath; + } + } + }); + }; +} + +/** + * Plugin that finds assets within the Mosaic filesystem and copies them to the configured `outputDir`. + * Documents that create relative references to those images, will be re-written to pull the images from the `outputDir`. + */ +const DocumentAssetsPlugin: PluginType = { + async afterUpdate( + _mutableFileSystem, + _helpers, + { + assetSubDirs = [path.join('**', 'images')], + srcDir = path.join(process.cwd(), 'docs'), + outputDir = `${path.sep}public` + } + ) { + const resolvedCwd = path.resolve(process.cwd()); + const resolvedSrcDir = path.resolve(srcDir); + const resolvedOutputDir = path.resolve(outputDir); + if (!resolvedOutputDir.startsWith(resolvedCwd)) { + throw new Error(`outputDir must be within the current working directory: ${outputDir}`); + } + await fsExtra.ensureDir(srcDir); + await fsExtra.ensureDir(outputDir); + + for (const assetSubDir of assetSubDirs) { + const resolvedAssetSubDir = path.resolve(resolvedSrcDir, assetSubDir); + if (!resolvedAssetSubDir.startsWith(resolvedSrcDir)) { + console.log('ERROR 3'); + + throw new Error(`Asset subdirectory must be within srcDir: ${srcDir}`); + } + + let globbedImageDirs; + try { + globbedImageDirs = await glob(assetSubDir, { + cwd: resolvedSrcDir, + onlyDirectories: true + }); + } catch (err) { + console.error(`Error globbing ${assetSubDir} in ${srcDir}:`, err); + continue; + } + + if (globbedImageDirs?.length === 0) { + continue; + } + + for (const globbedImageDir of globbedImageDirs) { + let imageFiles; + let globbedPath; + let rootSrcDir = srcDir; + let rootOutputDir = outputDir; + try { + if (!path.isAbsolute(rootSrcDir)) { + rootSrcDir = path.resolve(path.join(process.cwd(), srcDir)); + } + globbedPath = path.join(rootSrcDir, globbedImageDir); + imageFiles = await fsExtra.promises.readdir(globbedPath); + } catch (err) { + console.error(`Error reading directory ${globbedPath}:`, err); + continue; + } + if (!path.isAbsolute(rootOutputDir)) { + rootOutputDir = path.resolve(path.join(process.cwd(), outputDir)); + } + + for (const imageFile of imageFiles) { + try { + const imageSrcPath = path.join(globbedImageDir, imageFile); + const fullImageSrcPath = path.join(rootSrcDir, imageSrcPath); + const fullImageDestPath = path.join(rootOutputDir, imageSrcPath); + + await fsExtra.mkdir(path.dirname(fullImageDestPath), { recursive: true }); + const symlinkAlreadyExists = await fsExtra.pathExists(fullImageDestPath); + if (!symlinkAlreadyExists) { + await fsExtra.symlink(fullImageSrcPath, fullImageDestPath); + console.log(`Symlink created: ${fullImageSrcPath} -> ${fullImageDestPath}`); + } + } catch (error) { + console.error(`Error processing ${imageFile}:`, error); + } + } + } + } + }, + async $afterSource( + pages, + { ignorePages, pageExtensions }, + { imagesPrefix = `${path.sep}images` } + ) { + if (!pageExtensions.includes('.mdx')) { + return pages; + } + for (const page of pages) { + const isNonHiddenPage = createPageTest(ignorePages, ['.mdx']); + if (!isNonHiddenPage(page.fullPath)) { + continue; + } + + const processor = unified() + .use(remarkParse) + .use(remarkRewriteImagePaths, path.join(imagesPrefix, page.route)) + .use(remarkStringify); + await processor + .process(page.content) + .then((file: VFile) => { + page.content = String(file); + }) + .catch((err: Error) => { + console.error('Error processing Markdown:', err); + }); + } + return pages; + } +}; + +export default DocumentAssetsPlugin; diff --git a/packages/plugins/src/__tests__/DocumentAssetsPlugin.test.ts b/packages/plugins/src/__tests__/DocumentAssetsPlugin.test.ts new file mode 100644 index 00000000..53743dc6 --- /dev/null +++ b/packages/plugins/src/__tests__/DocumentAssetsPlugin.test.ts @@ -0,0 +1,168 @@ +import { expect, describe, test, afterEach, vi, beforeEach } from 'vitest'; + +import fsExtra from 'fs-extra'; +import mockFs from 'mock-fs'; +import path from 'path'; +import DocumentAssetsPlugin from '../DocumentAssetsPlugin'; + +describe('GIVEN the LocalImagePlugin', () => { + describe('afterUpdate', () => { + const srcDir = './src'; + const outputDir = './public'; + const assetSubDirs = ['**/images']; + + beforeEach(() => { + vi.spyOn(process, 'cwd').mockReturnValue('/mocked/cwd'); + mockFs({ + '/mocked/cwd/src': { + images: { + 'image1.png': 'file content', + 'image2.jpg': 'file content' + } + }, + './public': {} + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + mockFs.restore(); + }); + + test('should process image directories and call symlink with correct arguments', async () => { + const symlinkMock = vi.spyOn(fsExtra, 'symlink').mockResolvedValue(undefined); + + await DocumentAssetsPlugin.afterUpdate(null, null, { assetSubDirs, srcDir, outputDir }); + + const outputPathImage1 = path.join(process.cwd(), outputDir, 'images', 'image1.png'); + const outputPathImage2 = path.join(process.cwd(), outputDir, 'images', 'image2.jpg'); + const srcPathImage1 = path.join(process.cwd(), srcDir, 'images', 'image1.png'); + const srcPathImage2 = path.join(process.cwd(), srcDir, 'images', 'image2.jpg'); + + // assert that symlink was called with correct arguments + expect(symlinkMock).toHaveBeenCalledWith(srcPathImage1, outputPathImage1); + expect(symlinkMock).toHaveBeenCalledWith(srcPathImage2, outputPathImage2); + }); + + test('should handle errors gracefully and continue processing other files', async () => { + const symlinkMock = vi + .spyOn(fsExtra, 'symlink') + .mockImplementationOnce((src: fsExtra.PathLike, _dest) => { + if (`${src}`.includes('image1.png')) { + throw new Error('Symlink error'); + } + return Promise.resolve(); + }); + + console.error = vi.fn(); + + await DocumentAssetsPlugin.afterUpdate(null, null, { assetSubDirs, srcDir, outputDir }); + + const outputPathImage2 = path.join(process.cwd(), outputDir, 'images', 'image2.jpg'); + const srcPathImage2 = path.join(process.cwd(), srcDir, 'images', 'image2.jpg'); + + // assert that a single error does not break the plugin + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining('Error processing image1.png'), + expect.any(Error) + ); + expect(symlinkMock).toHaveBeenCalledWith(srcPathImage2, outputPathImage2); + }); + + test('should throw an error if outputDir is not below process.cwd()', async () => { + const options = { + srcDir: './docs', + outputDir: '/outside/cwd/public' + }; + + await expect(DocumentAssetsPlugin.afterUpdate({}, {}, options)).rejects.toThrow( + 'outputDir must be within the current working directory' + ); + }); + }); + + describe('$afterSource', () => { + const srcDir = '/src'; + const outputDir = './public'; + const pages = [ + // relative path `.` - should re-map URL to public images + { + fullPath: '/path/to/page1.mdx', + route: '/namespace/subdir1/page1', + content: '![alt text](./images/image1.png)' + }, + // relative path `..` - should re-map URL to public images + { + fullPath: '/path/to/page2.mdx', + route: '/namespace/subdir1/subdir2/page2', + content: '![alt text](../images/image2.png)' + }, + // No leading slash - should treat as relative path + { + fullPath: '/path/to/page3.mdx', + route: '/namespace/page3', + content: '![alt text](images/image3.jpg)' + }, + // No leading slash with extra level - should treat as relative path + { + fullPath: '/path/to/page4.mdx', + route: '/namespace/subdir1/page4', + content: '![alt text](images/image4.png)' + }, + // Ignored .txt - should remain un-changed + { + fullPath: '/path/to/page5.txt', + route: '/namespace/page5', + content: '![alt text](images/image5.png)' + }, + // ignored path - should remain un-changed + { + fullPath: '/path/to/ignore.mdx', + route: '/namespace/ignore.mdx', + content: '![alt text](images/image6.png)' + }, + // http address - should remain un-changed + { + fullPath: '/path/to/page7.mdx', + route: '/namespace/page7', + content: '![alt text](https://www.saltdesignsystem.com/img/hero_image.svg)' + } + ]; + const ignorePages = ['/path/to/ignore.mdx']; + const pageExtensions = ['.mdx']; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should return pages if .mdx is not included in pageExtensions', async () => { + const result = await DocumentAssetsPlugin.$afterSource( + pages, + { + ignorePages, + pageExtensions: ['.txt'] + }, + { srcDir, outputDir } + ); + expect(result).toEqual(pages); + }); + + test('should process .mdx pages and update their content', async () => { + const result = await DocumentAssetsPlugin.$afterSource( + pages, + { ignorePages, pageExtensions }, + { srcDir, outputDir } + ); + expect(result.length).toEqual(7); + expect(result[0].content).toBe('![alt text](/images/namespace/subdir1/images/image1.png)\n'); + expect(result[1].content).toBe('![alt text](/images/namespace/subdir1/images/image2.png)\n'); + expect(result[2].content).toBe('![alt text](/images/namespace/images/image3.jpg)\n'); + expect(result[3].content).toBe('![alt text](/images/namespace/subdir1/images/image4.png)\n'); + expect(result[4].content).toBe('![alt text](images/image5.png)'); + expect(result[5].content).toBe('![alt text](images/image6.png)'); + expect(result[6].content).toBe( + '![alt text](https://www.saltdesignsystem.com/img/hero_image.svg)\n' + ); + }); + }); +}); diff --git a/packages/site/mosaic.config.mjs b/packages/site/mosaic.config.mjs index 043fd125..359e6dec 100644 --- a/packages/site/mosaic.config.mjs +++ b/packages/site/mosaic.config.mjs @@ -23,6 +23,16 @@ const siteConfig = { outputDir: './public', assets: ['sitemap.xml', 'search-data.json'] } + }, + { + modulePath: '@jpmorganchase/mosaic-plugins/DocumentAssetsPlugin', + priority: -1, + options: { + srcDir: `../../docs`, + outputDir: './public/images/mosaic', + assetSubDirs: ['**/images'], + imagesPrefix: '/images' + } } ] }; diff --git a/yarn.lock b/yarn.lock index 22883282..6e38e607 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9608,7 +9608,7 @@ mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mock-fs@^5.0.0: +mock-fs@^5.0.0, mock-fs@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.2.0.tgz#3502a9499c84c0a1218ee4bf92ae5bf2ea9b2b5e" integrity sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==