@jpmorganchase/mosaic-plugins 0.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ @jpmorganchase/mosaic-plugins:build: cache hit, replaying output 1ab365469776992b
2
+ @jpmorganchase/mosaic-plugins:build: $ yarn clean && tsc && swc ./src/*.mjs.ts -d ./dist -C module.type=es6 -C sourceMaps=true && renamer --find '.mjs.js' --replace '.mjs' './dist/*.mjs.js' && renamer --find '.mjs.js.map' --replace '.mjs.map' './dist/*.mjs.js.map'
3
+ @jpmorganchase/mosaic-plugins:build: $ rm -fr dist
4
+ @jpmorganchase/mosaic-plugins:build: Successfully compiled: 2 files with swc (30.78ms)
5
+ @jpmorganchase/mosaic-plugins:build: ✔︎ ./dist/ReadingTimePlugin.mjs.js → dist/ReadingTimePlugin.mjs
6
+ @jpmorganchase/mosaic-plugins:build: ✔︎ ./dist/TableOfContentsPlugin.mjs.js → dist/TableOfContentsPlugin.mjs
7
+ @jpmorganchase/mosaic-plugins:build:
8
+ @jpmorganchase/mosaic-plugins:build: Rename complete: 2 of 2 files renamed.
9
+ @jpmorganchase/mosaic-plugins:build:
10
+ @jpmorganchase/mosaic-plugins:build: ✔︎ ./dist/ReadingTimePlugin.mjs.js.map → dist/ReadingTimePlugin.mjs.map
11
+ @jpmorganchase/mosaic-plugins:build: ✔︎ ./dist/TableOfContentsPlugin.mjs.js.map → dist/TableOfContentsPlugin.mjs.map
12
+ @jpmorganchase/mosaic-plugins:build:
13
+ @jpmorganchase/mosaic-plugins:build: Rename complete: 2 of 2 files renamed.
14
+ @jpmorganchase/mosaic-plugins:build:
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # @jpmorganchase/mosaic-plugins
2
+
3
+ ## 0.1.0-beta.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 457df5e: switch to public package
8
+ - Updated dependencies [457df5e]
9
+ - @jpmorganchase/mosaic-types@0.1.0-beta.3
10
+
11
+ ## 0.1.0-beta.2
12
+
13
+ ### Patch Changes
14
+
15
+ - e1bbbe7: Initial release of Mosaic Core file-system.
16
+
17
+ Mosaic is a content aggregating service which pulls content from heterogeneous data sources.
18
+
19
+ - 4b2bc51: pipped to 0.1.0-beta.1 for publishing to NPM
20
+ - Updated dependencies [e1bbbe7]
21
+ - Updated dependencies [4b2bc51]
22
+ - @jpmorganchase/mosaic-types@0.1.0-beta.2
23
+
24
+ ## 0.1.0
25
+
26
+ ### Patch Changes
27
+
28
+ - e1bbbe7: Initial release of Mosaic Core file-system.
29
+
30
+ Mosaic is a content aggregating service which pulls content from heterogeneous data sources.
31
+
32
+ - Updated dependencies [e1bbbe7]
33
+ - Updated dependencies [bbc853c]
34
+ - @jpmorganchase/mosaic-types@0.1.0
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@jpmorganchase/mosaic-plugins",
3
+ "version": "0.1.0-beta.3",
4
+ "author": "",
5
+ "description": "",
6
+ "license": "Apache-2.0",
7
+ "main": "./dist/index.js",
8
+ "engines": {
9
+ "node": ">= 12.0.0"
10
+ },
11
+ "scripts": {
12
+ "build": "yarn clean && tsc && swc ./src/*.mjs.ts -d ./dist -C module.type=es6 -C sourceMaps=true && renamer --find '.mjs.js' --replace '.mjs' './dist/*.mjs.js' && renamer --find '.mjs.js.map' --replace '.mjs.map' './dist/*.mjs.js.map'",
13
+ "clean": "rm -fr dist"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git@github.com:jpmorganchase/mosaic.git"
18
+ },
19
+ "devDependencies": {
20
+ "@swc/cli": "^0.1.55",
21
+ "@swc/core": "^1.2.237",
22
+ "renamer": "^4.0.0"
23
+ },
24
+ "dependencies": {
25
+ "@apidevtools/json-schema-ref-parser": "^9.0.9",
26
+ "@jpmorganchase/mosaic-types": "^0.1.0-beta.3",
27
+ "@types/github-slugger": "^1.3.0",
28
+ "github-slugger": "^1.4.0",
29
+ "hast-util-to-string": "^2.0.0",
30
+ "lodash": "^4.17.21",
31
+ "memfs": "^3.3.0",
32
+ "reading-time": "^1.5.0",
33
+ "rxjs": "^7.5.5"
34
+ }
35
+ }
@@ -0,0 +1,62 @@
1
+ import path from 'path';
2
+
3
+ import type Page from '@jpmorganchase/mosaic-types/dist/Page';
4
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
5
+ import { escapeRegExp } from 'lodash';
6
+
7
+ /**
8
+ * Plugin that scrapes `aliases` from page metadata and also applies all aliases stored in `config.data.aliases`
9
+ * Other plugins can use `setAliases` to apply new aliases, as long as they call it before this plugin has reaches `$beforeSend`
10
+ */
11
+ const $AliasPlugin: PluginType<{ aliases: { [key: string]: Set<string> } }> = {
12
+ async $afterSource(pages: Page<{ aliases?: string[] }>[], { config, ignorePages, pageExtensions }, options) {
13
+ const isNonHiddenPage = createPageTest(ignorePages, pageExtensions);
14
+
15
+ // Group together all aliases defined in the frontmatter and store them in the alias config object
16
+ for (const page of pages) {
17
+ if (!isNonHiddenPage(page.fullPath)) {
18
+ continue;
19
+ }
20
+ if (page.aliases) {
21
+ config.setAliases(page.fullPath, page.aliases);
22
+ }
23
+ delete page.aliases;
24
+ }
25
+ return pages;
26
+ },
27
+
28
+ async $beforeSend(mutableFilesystem, { config }) {
29
+ // Prevent any more aliases being set after this lifecycle has been called - as they won't take effect
30
+ config.setAliases = () => {
31
+ throw new Error('Cannot set aliases after `$AliasPlugin` has reached `$beforeSend` lifecycle phase.');
32
+ }; if (!config.data.aliases) {
33
+ return;
34
+ }
35
+ for (const fullPath in config.data.aliases) {
36
+ for (const alias of config.data.aliases[fullPath]) {
37
+ const aliasPath = path.resolve(path.dirname(fullPath), alias).toLowerCase();
38
+ const aliasDir = path.dirname(aliasPath);
39
+ if (!(await mutableFilesystem.promises.exists(aliasDir))) {
40
+ await mutableFilesystem.promises.mkdir(aliasDir, {
41
+ recursive: true
42
+ });
43
+ }
44
+ if (!(await mutableFilesystem.promises.exists(aliasPath))) {
45
+ await mutableFilesystem.promises.symlink(fullPath, aliasPath);
46
+ } else {
47
+ console.warn(`Alias '${aliasPath}' already exists. Is there a duplicate alias, or a page with the same name but different file extensions?`);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ };
53
+
54
+ export default $AliasPlugin;
55
+
56
+ function createPageTest(ignorePages, pageExtensions) {
57
+ const extTest = new RegExp(`${pageExtensions.map(escapeRegExp).join('|')}$`);
58
+ const ignoreTest = new RegExp(`${ignorePages.map(escapeRegExp).join('|')}$`);
59
+ return file => {
60
+ return !ignoreTest.test(file) && extTest.test(file) && !path.basename(file).startsWith('.');
61
+ };
62
+ }
@@ -0,0 +1,34 @@
1
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
2
+ import type Page from '@jpmorganchase/mosaic-types/dist/Page';
3
+ import { escapeRegExp } from 'lodash';
4
+ import path from 'path';
5
+
6
+ const $CodeModPlugin: PluginType<{
7
+ nextPrev: { [key: string]: string[] };
8
+ refs: { [key: string]: { $$path: (number | string)[]; $$value: string }[] };
9
+ }> = {
10
+ async $afterSource(pages: Page[], { ignorePages, pageExtensions }) {
11
+ const isNonHiddenPage = createPageTest(ignorePages, pageExtensions);
12
+
13
+ for (const page of pages) {
14
+ if (!isNonHiddenPage(page.fullPath)) {
15
+ continue;
16
+ }
17
+ if (page.frameOverrides) {
18
+ page.sharedConfig = page.frameOverrides;
19
+ page.frameOverrides = { $ref: '#/sharedConfig' };
20
+ }
21
+ }
22
+ return pages;
23
+ }
24
+ };
25
+
26
+ export default $CodeModPlugin;
27
+
28
+ function createPageTest(ignorePages, pageExtensions) {
29
+ const extTest = new RegExp(`${pageExtensions.map(escapeRegExp).join('|')}$`);
30
+ const ignoreTest = new RegExp(`${ignorePages.map(escapeRegExp).join('|')}$`);
31
+ return file => {
32
+ return !ignoreTest.test(file) && extTest.test(file) && !path.basename(file).startsWith('.');
33
+ };
34
+ }
@@ -0,0 +1,200 @@
1
+ import path from 'path';
2
+
3
+ import reduce from 'lodash/reduce';
4
+ import omit from 'lodash/omit';
5
+
6
+ import escapeRegExp from 'lodash/escapeRegExp';
7
+ import $RefParser from '@apidevtools/json-schema-ref-parser';
8
+
9
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
10
+ import type Page from '@jpmorganchase/mosaic-types/dist/Page';
11
+
12
+ import normaliseRefs from './utils/normaliseRefs';
13
+
14
+ /**
15
+ * Plugin that scrapes `$ref` properties from page metadata and also applies all refs stored in `config.data.refs`.
16
+ * Other plugins can use `setRef` to apply new refs, as long as they call it before this plugin has reaches `$beforeSend`
17
+ */
18
+ const $RefPlugin: PluginType<{
19
+ refs: { [key: string]: { $$path: string[]; $$value: string }[] };
20
+ globalRefs: { [key: string]: { $$path: string[]; $$value: string }[] };
21
+ subscribedTags: string[];
22
+ }> = {
23
+ async afterUpdate(
24
+ mutableFilesystem,
25
+ { ignorePages, globalFilesystem, serialiser, pageExtensions, globalConfig }
26
+ ) {
27
+ mutableFilesystem.__internal_do_not_use_addReadFileHook(async (pagePath: string, fileData) => {
28
+ if (globalConfig.data.globalRefs[pagePath]) {
29
+ const normalisedRef = {
30
+ ...(await serialiser.deserialise(pagePath, fileData)),
31
+ ...(await normaliseRefs(
32
+ String(pagePath),
33
+ globalConfig.data.globalRefs[pagePath],
34
+ globalFilesystem,
35
+ pageExtensions,
36
+ ignorePages
37
+ ))
38
+ } as any;
39
+ const resolve = createRefResolver({
40
+ [pagePath]: normalisedRef
41
+ }, serialiser, globalFilesystem);
42
+ const refParser = new $RefParser();
43
+
44
+ try {
45
+ const resolved: $RefParser.JSONSchema = await refParser.dereference(
46
+ String(pagePath),
47
+ normalisedRef,
48
+ {
49
+ resolve,
50
+ dereference: { circular: false }
51
+ }
52
+ );
53
+ return serialiser.serialise(pagePath, { ...normalisedRef, ...resolved });
54
+ } catch (e) {
55
+ console.warn(
56
+ `Error resolving ref(s) for page '${pagePath}'. ${e.message.replace(/\.$/, '')} in '${
57
+ e.source
58
+ }'`
59
+ );
60
+ throw e;
61
+ }
62
+ }
63
+
64
+ return fileData;
65
+ });
66
+ },
67
+ async $beforeSend(
68
+ mutableFilesystem,
69
+ { config, serialiser, pageExtensions, ignorePages },
70
+ options
71
+ ) {
72
+ // Prevent any more refs being set after this lifecycle has been called - as they won't take effect
73
+ config.setRef = () => {
74
+ throw new Error(
75
+ 'Cannot set refs after `RefPlugin` has reached `$beforeSend` lifecycle phase.'
76
+ );
77
+ };
78
+ const normalisedRefs = {};
79
+
80
+ if (config.data.refs) {
81
+ // First pass of refs, normalises any $ref entries to expand wildcards
82
+ for (const fullPath in config.data.refs) {
83
+ const page: Page = await serialiser.deserialise(
84
+ fullPath,
85
+ await mutableFilesystem.promises.readFile(fullPath)
86
+ );
87
+
88
+ normalisedRefs[fullPath] = {
89
+ ...page,
90
+ ...(await normaliseRefs(
91
+ String(fullPath),
92
+ config.data.refs[fullPath],
93
+ mutableFilesystem,
94
+ pageExtensions,
95
+ ignorePages
96
+ ))
97
+ };
98
+ }
99
+
100
+ const resolve = createRefResolver(normalisedRefs, serialiser, mutableFilesystem);
101
+ const refParser = new $RefParser();
102
+
103
+ // Second pass of refs, resolves the refs
104
+ for (const fullPath in normalisedRefs) {
105
+ const page = normalisedRefs[fullPath];
106
+ try {
107
+ const resolved: $RefParser.JSONSchema = await refParser.dereference(
108
+ String(fullPath),
109
+ page,
110
+ {
111
+ resolve,
112
+ dereference: { circular: false }
113
+ }
114
+ );
115
+ normalisedRefs[fullPath] = { ...page, ...resolved };
116
+ } catch (e) {
117
+ console.warn(
118
+ `Error resolving ref(s) for page '${fullPath}'. ${e.message.replace(/\.$/, '')} in '${
119
+ e.source
120
+ }'`
121
+ );
122
+ throw e;
123
+ }
124
+ }
125
+ // Third pass, write out the files
126
+ for (const fullPath in normalisedRefs) {
127
+ await mutableFilesystem.promises.writeFile(
128
+ fullPath,
129
+ await serialiser.serialise(fullPath, normalisedRefs[fullPath])
130
+ );
131
+ }
132
+ }
133
+ },
134
+ async $afterSource(pages: Page[], { config, ignorePages, pageExtensions }) {
135
+ const isNonHiddenPage = createPageTest(ignorePages, pageExtensions);
136
+
137
+ for (const page of pages) {
138
+ if (!isNonHiddenPage(page.fullPath)) {
139
+ continue;
140
+ }
141
+ const refs = findKeys(page, '$ref');
142
+
143
+ if (refs.length) {
144
+ for (const ref of refs) {
145
+ config.setRef(page.fullPath, ...ref);
146
+ }
147
+ }
148
+ }
149
+ return pages;
150
+ }
151
+ };
152
+
153
+ export default $RefPlugin;
154
+
155
+ function createRefResolver(normalisedRefs, serialiser, mutableFilesystem) {
156
+ return {
157
+ file: {
158
+ canRead: true,
159
+ order: 1,
160
+ async read(file, callback) {
161
+ try {
162
+ const refedPage =
163
+ normalisedRefs[file.url] ||
164
+ (await serialiser.deserialise(
165
+ file.url,
166
+ await mutableFilesystem.promises.readFile(file.url)
167
+ ));
168
+ return callback(null, omit(refedPage, 'content'));
169
+ } catch (e) {
170
+ return callback(e, null);
171
+ }
172
+ }
173
+ }
174
+ };
175
+ }
176
+
177
+ const findKeys = (obj, targetProp, pathParts: string[] = []): [string[], string][] => {
178
+ return reduce<string, [string[], string][]>(
179
+ obj,
180
+ (found, value, key) => {
181
+ if (key === targetProp) {
182
+ return found.concat([[pathParts.concat(String(key)), value]]);
183
+ }
184
+ if (typeof value === 'object') {
185
+ const foundParents = findKeys(value, targetProp, pathParts.concat(String(key)));
186
+ return found.concat(foundParents);
187
+ }
188
+ return found;
189
+ },
190
+ []
191
+ );
192
+ };
193
+
194
+ function createPageTest(ignorePages, pageExtensions) {
195
+ const extTest = new RegExp(`${pageExtensions.map(escapeRegExp).join('|')}$`);
196
+ const ignoreTest = new RegExp(`${ignorePages.map(escapeRegExp).join('|')}$`);
197
+ return file => {
198
+ return !ignoreTest.test(file) && extTest.test(file) && !path.basename(file).startsWith('.');
199
+ };
200
+ }
@@ -0,0 +1,99 @@
1
+ import path from 'path';
2
+
3
+ import reduce from 'lodash/reduce';
4
+ import escapeRegExp from 'lodash/escapeRegExp';
5
+
6
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
7
+ import type Page from '@jpmorganchase/mosaic-types/dist/Page';
8
+ import type Meta from '@jpmorganchase/mosaic-types/dist/Meta';
9
+
10
+ /**
11
+ * Plugin that scrapes `$tag` from page metadata and also applies all aliases stored in `config.data.tags`.
12
+ * Tags ultimately resolve down into refs, but are different from normal refs, in that they are applied to the
13
+ * union filesystem (all merged filesystems), not to the individual source filesystem that they were defined on.
14
+ * Other plugins can use `setData()` and modify the `tags` property, to apply new global refs, as long as
15
+ * they do so before this plugin has reaches `$beforeSend`
16
+ */
17
+ const $TagPlugin: PluginType<{
18
+ tagRefs: { [key: string]: { $$path: string[]; $$value: string[] }[] };
19
+ globalRefs: { [key: string]: { $$path: string[]; $$value: string[] }[] };
20
+ subscribedTags: string[];
21
+ }> = {
22
+ // Check if the updated source has any /.tag aliases that we care about - if it does, we should re-run `afterUpdate` to
23
+ // make sure we pull in the latest pages
24
+ async shouldClearCache(updatedSourceFilesystem, { config }) {
25
+ if (
26
+ !config.data?.subscribedTags ||
27
+ !(await updatedSourceFilesystem.promises.exists('/.tags'))
28
+ ) {
29
+ return false;
30
+ }
31
+ // Does the updated source have tags we are interested in?
32
+ return ((await updatedSourceFilesystem.promises.readdir('/.tags')) as string[]).some(tag =>
33
+ config.data.subscribedTags.includes(tag)
34
+ );
35
+ },
36
+ // Every source with tags will create symlinks with the tagged pages, inside /.tags
37
+ async $afterSource(pages: Page<{ tags?: string[] }>[], { config, ignorePages, pageExtensions }) {
38
+ const isNonHiddenPage = createPageTest(ignorePages, pageExtensions);
39
+
40
+ for (const page of pages) {
41
+ if (!isNonHiddenPage(page.fullPath)) {
42
+ continue;
43
+ }
44
+ const meta = page as Meta<{ tags?: string[] }>;
45
+ // Symlink every `tag` item to '/.tags' folder
46
+ if (meta.tags?.length) {
47
+ config.setTags(page.fullPath, meta.tags);
48
+ delete meta.tags;
49
+ }
50
+
51
+ // Find any references to $tag
52
+ const foundTags = findKeys(page, '$tag');
53
+ if (foundTags.length) {
54
+ const tags = new Set<string>();
55
+
56
+ foundTags.forEach(ref => {
57
+ const [tag, fragment = ''] = ref.$$value.split('#');
58
+
59
+ tags.add(tag);
60
+
61
+ config.setGlobalRef(
62
+ page.fullPath,
63
+ ref.$$path,
64
+ `${path.join('/.tags', tag, '**')}#${fragment}`
65
+ );
66
+ });
67
+ config.setData({ subscribedTags: Array.from(tags) });
68
+ }
69
+ }
70
+ return pages;
71
+ }
72
+ };
73
+
74
+ export default $TagPlugin;
75
+
76
+ const findKeys = (obj, targetProp, pathParts: string[] = []) => {
77
+ return reduce<string, { $$path: string[]; $$value: string }[]>(
78
+ obj,
79
+ (found, value, key) => {
80
+ if (key === targetProp) {
81
+ return found.concat({ $$path: pathParts.concat(String(key)), $$value: value });
82
+ }
83
+ if (typeof value === 'object') {
84
+ const foundParents = findKeys(value, targetProp, pathParts.concat(String(key)));
85
+ return found.concat(...foundParents);
86
+ }
87
+ return found;
88
+ },
89
+ []
90
+ );
91
+ };
92
+
93
+ function createPageTest(ignorePages, pageExtensions) {
94
+ const extTest = new RegExp(`${pageExtensions.map(escapeRegExp).join('|')}$`);
95
+ const ignoreTest = new RegExp(`${ignorePages.map(escapeRegExp).join('|')}$`);
96
+ return file => {
97
+ return !ignoreTest.test(file) && extTest.test(file) && !path.basename(file).startsWith('.');
98
+ };
99
+ }
@@ -0,0 +1,43 @@
1
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
2
+ import type Page from '@jpmorganchase/mosaic-types/dist/Page';
3
+ import path from 'path';
4
+
5
+ export type Breadcrumb = { label: string; path: string; id: string };
6
+
7
+ /**
8
+ * Calculates breadcrumbs for pages then embeds a `breadcrumbs` property to the metadata
9
+ */
10
+ const BreadcrumbsPlugin: PluginType<{}, { indexPageName: string }> = {
11
+ async $afterSource(pages: Page[], {}, options) {
12
+ for (const page of pages) {
13
+ const breadcrumbs: Array<Breadcrumb> = [];
14
+ let currentPage = page;
15
+ let parentDir = path.posix.dirname(currentPage.fullPath);
16
+
17
+ while (currentPage !== undefined) {
18
+ if (breadcrumbs.findIndex(breadcrumb => breadcrumb.id === currentPage.fullPath) === -1) {
19
+ breadcrumbs.unshift({
20
+ label: currentPage.title,
21
+ path: currentPage.route,
22
+ id: currentPage.fullPath
23
+ });
24
+ }
25
+
26
+ currentPage = pages.find(
27
+ page => page.fullPath === path.posix.join(parentDir, options.indexPageName)
28
+ );
29
+ if (currentPage) {
30
+ parentDir = path.posix.dirname(path.posix.join(String(currentPage.fullPath), '..'));
31
+ }
32
+ }
33
+
34
+ if (!page.breadcrumbs) {
35
+ page.breadcrumbs = breadcrumbs;
36
+ }
37
+ }
38
+
39
+ return pages;
40
+ }
41
+ };
42
+
43
+ export default BreadcrumbsPlugin;
@@ -0,0 +1,101 @@
1
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
2
+ import fs from 'fs';
3
+ import fsExtra from 'fs-extra';
4
+ import merge from 'lodash/merge';
5
+ import escapeRegExp from 'lodash/escapeRegExp';
6
+ import path from 'path';
7
+ import { TDataOut } from 'memfs/lib/encoding';
8
+
9
+ const LazyPagePlugin: PluginType<
10
+ { aliases: { [key: string]: Set<string> }; hddPaths: { [key: string]: string } },
11
+ { cacheDir: string }
12
+ > = {
13
+ async afterUpdate(mutableFilesystem, { ignorePages, config, pageExtensions, serialiser }) {
14
+ const isNonHiddenPage = createPageTest(ignorePages, pageExtensions);
15
+
16
+ // This adds a hook that triggers everytime a non-cached page is read from the filesystem.
17
+ mutableFilesystem.__internal_do_not_use_addReadFileHook(
18
+ async (pagePath: string, fileData: TDataOut) => {
19
+ // If this is a 'page' - read the original file from disk and try to inject anything that's missing.
20
+ if (isNonHiddenPage(pagePath)) {
21
+ const currentPage = await serialiser.deserialise(pagePath, fileData);
22
+ if (config.data.hddPaths[pagePath]) {
23
+ const page = await serialiser.deserialise(
24
+ pagePath,
25
+ await fs.promises.readFile(config.data.hddPaths[pagePath])
26
+ );
27
+ const completePage = await serialiser.serialise(
28
+ pagePath,
29
+ merge(page, currentPage, { content: page.content })
30
+ );
31
+ return completePage;
32
+ }
33
+ }
34
+ return fileData;
35
+ }
36
+ );
37
+ },
38
+ async $beforeSend(
39
+ mutableFilesystem,
40
+ { config, serialiser, ignorePages, pageExtensions },
41
+ options
42
+ ) {
43
+ const hddPaths = {};
44
+ let originalDiskSize = 0;
45
+ let newDiskSize = 0;
46
+ const allPages = await mutableFilesystem.promises.glob(createFileGlob('**', pageExtensions), {
47
+ ignore: ignorePages.map(ignore => `**/${ignore}`),
48
+ cwd: '/',
49
+ onlyFiles: true
50
+ });
51
+ const baseDir = path.join(
52
+ process.cwd(),
53
+ options.cacheDir || '.mosaic-lazy-page-plugin-cache'
54
+ );
55
+ await fsExtra.ensureDir(baseDir);
56
+ for (const filePath of allPages) {
57
+ const rawPage = await mutableFilesystem.promises.readFile(String(filePath));
58
+ const page = await serialiser.deserialise(String(filePath), rawPage);
59
+ originalDiskSize += rawPage.length;
60
+ await fs.promises.mkdir(path.dirname(path.join(baseDir, String(filePath))), {
61
+ recursive: true
62
+ });
63
+ await fs.promises.writeFile(
64
+ path.join(baseDir, String(filePath)),
65
+ await serialiser.serialise(String(filePath), page)
66
+ );
67
+ const { content, ...pageRest } = page;
68
+ hddPaths[String(filePath)] = path.join(baseDir, String(filePath));
69
+ const redactedPage = await serialiser.serialise(String(filePath), pageRest);
70
+ newDiskSize += redactedPage.length;
71
+ }
72
+
73
+ config.setData({hddPaths});
74
+
75
+ const reduction = originalDiskSize - newDiskSize;
76
+ console.info(
77
+ `[PullDocs] LazyPagePlugin moved ${((reduction / originalDiskSize) * 100).toFixed(
78
+ 2
79
+ )}% of virtual filesystem content to the hard disk. This reduced the virtual filesystem size from ${(
80
+ originalDiskSize / 1000000
81
+ ).toFixed(2)}mb to ${(newDiskSize / 1000000).toFixed(2)}mb (yay!)`
82
+ );
83
+ }
84
+ };
85
+
86
+ function createFileGlob(url, pageExtensions) {
87
+ if (pageExtensions.length === 1) {
88
+ return `${url}${pageExtensions[0]}`;
89
+ }
90
+ return `${url}{${pageExtensions.join(',')}}`;
91
+ }
92
+
93
+ export default LazyPagePlugin;
94
+
95
+ function createPageTest(ignorePages, pageExtensions) {
96
+ const extTest = new RegExp(`${pageExtensions.map(escapeRegExp).join('|')}$`);
97
+ const ignoreTest = new RegExp(`${ignorePages.map(escapeRegExp).join('|')}$`);
98
+ return file => {
99
+ return !ignoreTest.test(file) && extTest.test(file) && !path.basename(file).startsWith('.');
100
+ };
101
+ }
@@ -0,0 +1,80 @@
1
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
2
+ import { escapeRegExp } from 'lodash';
3
+
4
+ /**
5
+ * Sorts the pages in a folder alphabetically and then embeds a `navigation` property to the metadata with ordered
6
+ * next/prev pages
7
+ */
8
+ const NextPrevPlugin: PluginType<
9
+ {
10
+ nextPrev: { [key: string]: string[] };
11
+ refs: { [key: string]: { $$path: (number | string)[]; $$value: string }[] };
12
+ },
13
+ { indexFirst?: boolean; sortBy?: 'a-z' }
14
+ > = {
15
+ async $beforeSend(
16
+ mutableFilesystem,
17
+ { config, pageExtensions, ignorePages },
18
+ { indexFirst, sortBy }
19
+ ) {
20
+ const pageDirs = (await mutableFilesystem.promises.glob('**', {
21
+ onlyDirectories: true,
22
+ ignore: ignorePages.map(ignore => `**/${ignore}`),
23
+ cwd: '/'
24
+ })) as string[];
25
+ for (const dirName of pageDirs) {
26
+ const pages = (
27
+ await mutableFilesystem.promises.glob(createFileGlob('*', pageExtensions), { onlyFiles: true, cwd: dirName, ignore: ignorePages })
28
+ ).sort(createSortFn(pageExtensions, { indexFirst, sortBy })) as string[];
29
+ for (let i = 0; i < pages.length; i++) {
30
+ if (i > 0) {
31
+ config.setRef(
32
+ pages[i],
33
+ ['navigation', 'prev', 'title', '$ref'],
34
+ `${pages[i - 1]}#/title`
35
+ );
36
+ config.setRef(
37
+ pages[i],
38
+ ['navigation', 'prev', 'fullPath', '$ref'],
39
+ `${pages[i - 1]}#/route`
40
+ );
41
+ }
42
+ if (i < pages.length - 1) {
43
+ config.setRef(
44
+ pages[i],
45
+ ['navigation', 'next', 'title', '$ref'],
46
+ `${pages[i + 1]}#/title`
47
+ );
48
+ config.setRef(
49
+ pages[i],
50
+ ['navigation', 'next', 'fullPath', '$ref'],
51
+ `${pages[i + 1]}#/route`
52
+ );
53
+ }
54
+ }
55
+ }
56
+ }
57
+ };
58
+
59
+ export default NextPrevPlugin;
60
+
61
+ function createFileGlob(url, pageExtensions) {
62
+ if (pageExtensions.length === 1) {
63
+ return `${url}${pageExtensions[0]}`;
64
+ }
65
+ return `${url}{${pageExtensions.join(',')}}`;
66
+ }
67
+
68
+ function createSortFn(pageExtensions, { sortBy: _sortBy, indexFirst }) {
69
+ const indexRegExp =
70
+ indexFirst && new RegExp(`index${pageExtensions.map(escapeRegExp).join('|')}$`);
71
+
72
+ // TODO: Switch out the algorithm here, based on `sortBy`
73
+ return function sortFn(fullPathA, fullPathB) {
74
+ // Always pin /index to the front
75
+ if (indexFirst && indexRegExp.test(fullPathA)) {
76
+ return -1;
77
+ }
78
+ return fullPathA.localeCompare(fullPathB);
79
+ };
80
+ }
@@ -0,0 +1,38 @@
1
+ import type Page from '@jpmorganchase/mosaic-types/dist/Page';
2
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
3
+ import { escapeRegExp } from 'lodash';
4
+ import path from 'path';
5
+
6
+ /**
7
+ * Plugin that creates aliases without the file extension for every page emitted by the source.
8
+ * This allows pages to be retrieved from the filesystem without specifying `.mdx` (or whatever file extension is specified in `options.stripExt`).
9
+ * The plugin also modifies the `route` to point to the shorter alias. Friendly fullPaths are the shortest possible paths that
10
+ * will point to the page (see `Page` docs)
11
+ */
12
+ const PagesWithoutFileExtPlugin: PluginType<
13
+ {}
14
+ > = {
15
+ async $afterSource(pages: Page[], { config, ignorePages, pageExtensions }) {
16
+ const isNonHiddenPage = createPageTest(ignorePages, pageExtensions);
17
+ const pageTest = new RegExp(`${pageExtensions.map(escapeRegExp).join('|')}$`);
18
+
19
+ for (const page of pages) {
20
+ if (!isNonHiddenPage(page.fullPath)) {
21
+ continue;
22
+ }
23
+ page.route = page.route.replace(pageTest, '');
24
+ config.setAliases(page.fullPath, [page.route]);
25
+ }
26
+ return pages;
27
+ }
28
+ };
29
+
30
+ export default PagesWithoutFileExtPlugin;
31
+
32
+ function createPageTest(ignorePages, pageExtensions) {
33
+ const extTest = new RegExp(`${pageExtensions.map(escapeRegExp).join('|')}$`);
34
+ const ignoreTest = new RegExp(`${ignorePages.map(escapeRegExp).join('|')}$`);
35
+ return file => {
36
+ return !ignoreTest.test(file) && extTest.test(file) && !path.basename(file).startsWith('.');
37
+ };
38
+ }
@@ -0,0 +1,32 @@
1
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
2
+ import type Page from '@jpmorganchase/mosaic-types/dist/Page';
3
+ import getReadingTime from 'reading-time';
4
+ import { unified } from 'unified';
5
+ import { visit } from 'unist-util-visit';
6
+ import markdown from 'remark-parse';
7
+
8
+ /**
9
+ * Calculates reading time for pages and adds to frontmatter
10
+ */
11
+ const ReadingTimePlugin: PluginType<{}> = {
12
+ async $afterSource(pages: Page[]) {
13
+ const processor = unified().use(markdown);
14
+ for (const page of pages) {
15
+ const tree = await processor.parse(page.content);
16
+ let textContent = '';
17
+
18
+ visit(
19
+ tree,
20
+ node => node.type === 'text' || node.type === 'code',
21
+ node => {
22
+ textContent += node.value;
23
+ }
24
+ );
25
+ page.readingTime = getReadingTime(textContent);
26
+ }
27
+
28
+ return pages;
29
+ }
30
+ };
31
+
32
+ export default ReadingTimePlugin;
@@ -0,0 +1,49 @@
1
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
2
+ import flatten from 'lodash/flatten';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * Plugin that crawls the page hierarchy to find the closest `sharedConfig` from any parent index's page metadata.
7
+ * It then exports a JSON file (name: `options.filename`) into each directory with the merged config for that level
8
+ */
9
+ const SharedConfigPlugin: PluginType<{}, { filename: string }> = {
10
+ async $beforeSend(
11
+ mutableFilesystem,
12
+ { config, serialiser, ignorePages, pageExtensions },
13
+ options
14
+ ) {
15
+ const pagePaths = await mutableFilesystem.promises.glob(
16
+ createFileGlob('**/index,/index', pageExtensions),
17
+ {
18
+ ignore: [options.filename, ...flatten(ignorePages.map(ignore => [ignore, `**/${ignore}`]))],
19
+ cwd: '/'
20
+ }
21
+ );
22
+
23
+ for (const pagePath of pagePaths) {
24
+ const sharedConfigFile = path.join(path.dirname(String(pagePath)), options.filename);
25
+
26
+ const page = await serialiser.deserialise(
27
+ String(pagePath),
28
+ await mutableFilesystem.promises.readFile(String(pagePath))
29
+ );
30
+ if (page.sharedConfig) {
31
+ config.setRef(sharedConfigFile, ['config', '$ref'], `${String(pagePath)}#/sharedConfig`);
32
+ await mutableFilesystem.promises.writeFile(sharedConfigFile, '{}');
33
+ } else {
34
+ const baseDir = path.resolve(path.dirname(String(pagePath)), '../');
35
+
36
+ config.setAliases(path.join(baseDir, options.filename), [sharedConfigFile]);
37
+ }
38
+ }
39
+ }
40
+ };
41
+
42
+ export default SharedConfigPlugin;
43
+
44
+ function createFileGlob(url, pageExtensions) {
45
+ if (pageExtensions.length === 1) {
46
+ return `${url}${pageExtensions[0]}`;
47
+ }
48
+ return `${url}{${pageExtensions.join(',')}}`;
49
+ }
@@ -0,0 +1,154 @@
1
+ import path from 'path';
2
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
3
+
4
+ /**
5
+ * Sorts the pages in a folder by priority and then exports a JSON file (name: `options.filename`) with the
6
+ * sidebar tree from that directory downwards and adds sidebar data into frontmatter for each page.
7
+ */
8
+
9
+ const SidebarPlugin: PluginType<
10
+ {
11
+ dirs: string[];
12
+ refs: { [key: string]: { $$path: (number | string)[]; $$value: string }[] };
13
+ },
14
+ { filename: string }
15
+ > = {
16
+ async $beforeSend(
17
+ mutableFilesystem,
18
+ { config, serialiser, ignorePages, pageExtensions },
19
+ options
20
+ ) {
21
+ const dirs = await mutableFilesystem.promises.glob('**', {
22
+ onlyDirectories: true,
23
+ cwd: '/'
24
+ });
25
+ for (const dirName of dirs) {
26
+ const sidebarFilePath = path.posix.join(String(dirName), options.filename);
27
+ const pages = (
28
+ await Promise.all(
29
+ ((await mutableFilesystem.promises.glob(
30
+ createFileGlob(['*', '*/index'], pageExtensions),
31
+ {
32
+ onlyFiles: true,
33
+ cwd: String(dirName),
34
+ ignore: ignorePages.map(ignore => `**/${ignore}`)
35
+ }
36
+ )) as string[]).map(async pagePath => {
37
+ return await serialiser.deserialise(
38
+ pagePath,
39
+ await mutableFilesystem.promises.readFile(pagePath)
40
+ );
41
+ })
42
+ )
43
+ )
44
+ .filter(page => filterFn(page))
45
+ .sort((pageA, pageB) => sortFn(pageA, pageB, dirName));
46
+
47
+ for (let i = 0; i < pages.length; i++) {
48
+ setPageRefs(sidebarFilePath, pages[i], i);
49
+ setChildPageRefs(pages[i], dirName, i, sidebarFilePath);
50
+ addSidebarDataToFrontmatter(pages[i], dirName);
51
+ }
52
+ await mutableFilesystem.promises.writeFile(sidebarFilePath, '[]');
53
+ }
54
+
55
+ function setPageRefs(sidebarFilePath, page, i) {
56
+ config.setRef(sidebarFilePath, ['pages', `${i}`, 'id', '$ref'], `${page.fullPath}#/route`);
57
+ page.sidebar && page.sidebar.label
58
+ ? config.setRef(
59
+ sidebarFilePath,
60
+ ['pages', `${i}`, 'name', '$ref'],
61
+ `${page.fullPath}#/sidebar/label`
62
+ )
63
+ : config.setRef(
64
+ sidebarFilePath,
65
+ ['pages', `${i}`, 'name', '$ref'],
66
+ `${page.fullPath}#/title`
67
+ );
68
+ config.setRef(
69
+ sidebarFilePath,
70
+ ['pages', `${i}`, 'data', 'link', '$ref'],
71
+ `${page.fullPath}#/route`
72
+ );
73
+ }
74
+
75
+ async function setChildPageRefs(page, dirName, i, sidebarFilePath) {
76
+ if (isSubfolderIndex(page, dirName)) {
77
+ const childDirName = path.posix.dirname(page.fullPath);
78
+ const childNodeSidebarPath = path.posix.join(childDirName, options.filename);
79
+ const childPages = (
80
+ await Promise.all(
81
+ ((await mutableFilesystem.promises.glob(
82
+ createFileGlob(['*', '*/index'], pageExtensions),
83
+ {
84
+ onlyFiles: true,
85
+ cwd: String(childDirName),
86
+ ignore: ignorePages.map(ignore => `**/${ignore}`)
87
+ }
88
+ )) as string[]).map(async pagePath => {
89
+ return await serialiser.deserialise(
90
+ pagePath,
91
+ await mutableFilesystem.promises.readFile(pagePath)
92
+ );
93
+ })
94
+ )
95
+ ).filter(page => filterFn(page));
96
+ if (childPages.length > 1) {
97
+ for (let j = 1; j < childPages.length; j++) {
98
+ config.setRef(
99
+ sidebarFilePath,
100
+ ['pages', `${i}`, 'childNodes', `${j - 1}`, '$ref'],
101
+ `${childNodeSidebarPath}#/pages/${j}`
102
+ );
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ function addSidebarDataToFrontmatter(page, dirName) {
109
+ const dirNameLevels = String(dirName)
110
+ .split('/')
111
+ .filter(level => level);
112
+ const sidebarRootDir =
113
+ `/` + dirNameLevels.slice(0, dirNameLevels.length < 3 ? dirNameLevels.length : 3).join(`/`);
114
+ config.setRef(
115
+ String(page.fullPath),
116
+ ['sidebarData', '$ref'],
117
+ path.posix.join(String(sidebarRootDir), options.filename, '#', 'pages')
118
+ );
119
+ }
120
+ }
121
+ };
122
+ export default SidebarPlugin;
123
+
124
+ function createFileGlob(patterns, pageExtensions) {
125
+ if (Array.isArray(patterns)) {
126
+ return patterns.map(pattern => createFileGlob(pattern, pageExtensions));
127
+ }
128
+ if (pageExtensions.length === 1) {
129
+ return `${patterns}${pageExtensions[0]}`;
130
+ }
131
+ return `${patterns}{${pageExtensions.join(',')}}`;
132
+ }
133
+
134
+ function isSubfolderIndex(page, dirName) {
135
+ return page.fullPath.split('/').length == String(dirName).split('/').length + 2;
136
+ }
137
+
138
+ function sortFn(pageA, pageB, dirName) {
139
+ // Always pin /index to the front
140
+ if (pageA.route === dirName + '/index') {
141
+ return -1;
142
+ }
143
+ if (pageB.route === dirName + '/index') {
144
+ return 1;
145
+ }
146
+ return (
147
+ (pageB.sidebar && pageB.sidebar.priority ? pageB.sidebar.priority : -1) -
148
+ (pageA.sidebar && pageA.sidebar.priority ? pageA.sidebar.priority : -1)
149
+ );
150
+ }
151
+
152
+ function filterFn(page) {
153
+ return !(page.sidebar && page.sidebar.exclude);
154
+ }
@@ -0,0 +1,44 @@
1
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
2
+
3
+ const SiteMapPlugin: PluginType<
4
+ { sitemaps: string[][] },
5
+ {}
6
+ > = {
7
+ // Merge together all of the individual sitemaps from each source
8
+ async afterUpdate(mutableFilesystem, { sharedFilesystem, globalConfig }) {
9
+ if (!Array.isArray(globalConfig.data.sitemaps)) {
10
+ return;
11
+ }
12
+ await sharedFilesystem.promises.writeFile(
13
+ '/sitemap.xml',
14
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' +
15
+ globalConfig.data.sitemaps
16
+ .map(items =>
17
+ items
18
+ .map(
19
+ item =>
20
+ `\t<url>\n\t\t<loc>${String(
21
+ item
22
+ )}</loc>\n\t\t<changefreq>weekly</changefreq>\n\t\t<priority>0.5</priority>\n\t</url>`
23
+ )
24
+ .join('\n')
25
+ )
26
+ .join('') +
27
+ '\n</urlset>'
28
+ );
29
+ },
30
+ // Store a sitemap for just this source
31
+ async $beforeSend(mutableFilesystem, { config, ignorePages }, options) {
32
+ config.setData({
33
+ sitemaps: [
34
+ (await mutableFilesystem.promises.glob('**', {
35
+ onlyFiles: true,
36
+ ignore: ignorePages.map(ignore => `**/${ignore}`),
37
+ cwd: '/'
38
+ })) as string[]
39
+ ]
40
+ });
41
+ }
42
+ };
43
+
44
+ export default SiteMapPlugin;
@@ -0,0 +1,52 @@
1
+ import type PluginType from '@jpmorganchase/mosaic-types/dist/Plugin';
2
+ import type Page from '@jpmorganchase/mosaic-types/dist/Page';
3
+ import { visit } from 'unist-util-visit';
4
+ import markdown from 'remark-parse';
5
+ import { unified } from 'unified';
6
+ import Slugger from 'github-slugger';
7
+ import { toString } from 'hast-util-to-string';
8
+
9
+ type TOCItem = { level: number; id: string; text: string };
10
+
11
+ /**
12
+ * Calculates table of contents from page headings
13
+ */
14
+ const TableOfContentsPlugin: PluginType<{}, { minRank: 2; maxRank: 4 }> = {
15
+ async $afterSource(pages: Page[], {}, { minRank, maxRank }) {
16
+ const processor = unified().use(markdown);
17
+ const slugger = new Slugger();
18
+ for (const page of pages) {
19
+ const tree = await processor.parse(page.content);
20
+ const items: TOCItem[] = [];
21
+ visit(
22
+ tree,
23
+ node => test(node),
24
+ node => {
25
+ const text = toString(node).trim();
26
+ items.push({
27
+ level: node.depth,
28
+ id: slugger.slug(text),
29
+ text: text
30
+ });
31
+ }
32
+ );
33
+ page.tableOfContents = items;
34
+ }
35
+
36
+ return pages;
37
+
38
+ function test(node) {
39
+ return node.type === 'heading' && isHeadingInRange(node);
40
+ }
41
+
42
+ function isHeadingInRange(node) {
43
+ const depth = node.depth;
44
+ if (depth < minRank || depth > maxRank) {
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ }
50
+ };
51
+
52
+ export default TableOfContentsPlugin;
@@ -0,0 +1,85 @@
1
+ import path from 'path';
2
+
3
+ import glob from 'fast-glob';
4
+ import set from 'lodash/set';
5
+
6
+ function createFileGlob(url, pageExtensions) {
7
+ if (pageExtensions.length === 1) {
8
+ return `${url}${pageExtensions[0]}`;
9
+ }
10
+ return `${url}{${pageExtensions.join(',')}}`;
11
+ }
12
+
13
+ export default async function normaliseRefs(filepath, refs, filesystem, pageExtensions, ignorePages) {
14
+ const configWithoutGlobs = {};
15
+
16
+ for (let i = 0; i < refs.length; i++) {
17
+ const ref = refs[i];
18
+ const [url, fragment = ''] = ref.$$value.split('#');
19
+
20
+ if (!url) {
21
+ continue;
22
+ }
23
+
24
+ const fullUrl = path.resolve(path.dirname(filepath), url);
25
+
26
+ if (!glob.isDynamicPattern(url)) {
27
+ if (!(await filesystem.promises.exists(fullUrl))) {
28
+ throw new Error(
29
+ `A \`$ref\` for '${filepath}' pointed to page '${fullUrl}' - which could not be found.`
30
+ );
31
+ }
32
+ const refRealPath = await filesystem.promises.realpath(
33
+ fullUrl
34
+ //path.resolve(path.dirname(filepath), url)
35
+ );
36
+ const stat = await filesystem.promises.stat(refRealPath);
37
+ // Turn directory refs into full index paths
38
+ // TODO: But DO we want to do this?
39
+ if (stat.isDirectory()) {
40
+ // && !/\/index(\.\w{1,4})?$/.test(url)) {
41
+ set(configWithoutGlobs, ref.$$path.slice(0, -1), {
42
+ $ref: `${await filesystem.promises.realpath(path.join(refRealPath, 'index'))}#${fragment}`
43
+ });
44
+ } else {
45
+ set(configWithoutGlobs, ref.$$path.slice(0, -1), {
46
+ $ref: `${refRealPath}#${fragment}`
47
+ });
48
+ }
49
+ continue;
50
+ }
51
+
52
+ try {
53
+ let globResolvedRefs =
54
+ // If the pattern doesn't have an explicit file extension - then we'll make it just return pages
55
+ await filesystem.promises.glob(
56
+ !/\/[^/]+\.\w{2,4}$/.test(url) ? createFileGlob(url, pageExtensions) : url,
57
+ {
58
+ ignore: [filepath, ...ignorePages.map(ignore => `**/${ignore}`)],
59
+ cwd: path.dirname(filepath),
60
+ absolute: true
61
+ }
62
+ );
63
+
64
+ // Use a set so we de-dupe any symlinks that point to the same page
65
+ const resolvedRefs = new Set();
66
+
67
+ for (const value of globResolvedRefs) {
68
+ const refRealPath = await filesystem.promises.realpath(path.resolve(filepath, value));
69
+ if (refRealPath !== (await filesystem.promises.realpath(filepath))) {
70
+ resolvedRefs.add(`${refRealPath}#${fragment}`);
71
+ }
72
+ }
73
+
74
+ set(
75
+ configWithoutGlobs,
76
+ ref.$$path.slice(0, -1),
77
+ Array.from(resolvedRefs).map($ref => ({ $ref }))
78
+ );
79
+ } catch (e) {
80
+ throw e;
81
+ }
82
+ }
83
+
84
+ return configWithoutGlobs;
85
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "allowJs": true,
5
+ "target": "es2015",
6
+ "esModuleInterop": true,
7
+ "moduleResolution": "NodeNext",
8
+ "outDir": "dist",
9
+ "skipLibCheck": true,
10
+ "sourceMap": true,
11
+ "rootDir": "./src"
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": [
15
+ "node_modules",
16
+ "src/**/__tests__/**/*",
17
+ "src/__tests__/**/*",
18
+ "src/**/*.mjs.*",
19
+ "**/dist/**/*"
20
+ ]
21
+ }