@jungvonmatt/contentful-ssg 1.0.4 → 1.4.0

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.
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import { existsSync } from 'fs';
5
+ import { readFile } from 'fs/promises';
5
6
  import { outputFile } from 'fs-extra';
6
7
  import prettier from 'prettier';
7
8
  import { Command } from 'commander';
@@ -44,7 +45,17 @@ program
44
45
  if (verified.directory?.startsWith('/')) {
45
46
  verified.directory = path.relative(process.cwd(), verified.directory);
46
47
  }
47
- const environmentKeys = Object.keys(environmentConfig).filter((key) => environmentConfig[key] === verified[key]);
48
+ const environmentKeys = Object.keys(environmentConfig);
49
+ if (environmentConfig && existsSync('.env')) {
50
+ const envSource = await readFile('.env', 'utf8');
51
+ const nextEnvSource = envSource
52
+ .replace(/(CONTENTFUL_SPACE_ID\s*=\s*['"]?)[^'"]*(['"]?)/, `$1${verified.spaceId}$2`)
53
+ .replace(/(CONTENTFUL_ENVIRONMENT_ID\s*=\s*['"]?)[^'"]*(['"]?)/, `$1${verified.environmentId}$2`)
54
+ .replace(/(CONTENTFUL_MANAGEMENT_TOKEN\s*=\s*['"]?)[^'"]*(['"]?)/, `$1${verified.managementToken}$2`)
55
+ .replace(/(CONTENTFUL_PREVIEW_TOKEN\s*=\s*['"]?)[^'"]*(['"]?)/, `$1${verified.previewAccessToken}$2`)
56
+ .replace(/(CONTENTFUL_DELIVERY_TOKEN\s*=\s*['"]?)[^'"]*(['"]?)/, `$1${verified.accessToken}$2`);
57
+ await outputFile('.env', nextEnvSource);
58
+ }
48
59
  const cleanedConfig = omitKeys(verified, 'preview', 'verbose', 'rootDir', 'resolvedPlugins', 'host', 'managementToken', ...environmentKeys);
49
60
  let content = '';
50
61
  if (useTypescript) {
@@ -1,3 +1,3 @@
1
1
  import type { Config, ContentfulConfig } from '../types.js';
2
- export declare const getEnvironmentConfig: () => ContentfulConfig;
2
+ export declare const getEnvironmentConfig: (strict?: boolean) => ContentfulConfig;
3
3
  export declare const getConfig: (args?: Partial<Config>) => Promise<Config>;
@@ -4,6 +4,7 @@ import { cosmiconfig } from 'cosmiconfig';
4
4
  import mergeOptionsModule from 'merge-options';
5
5
  import { dirname, isAbsolute, resolve } from 'path';
6
6
  import slash from 'slash';
7
+ import { reduceAsync } from './array.js';
7
8
  import { createRequire } from './create-require.js';
8
9
  import { isObject, removeEmpty } from './object.js';
9
10
  const typescriptLoader = async (filePath) => {
@@ -72,33 +73,34 @@ const loadConfig = async (moduleName) => {
72
73
  });
73
74
  return explorer.search();
74
75
  };
75
- export const getEnvironmentConfig = () => removeEmpty({
76
+ export const getEnvironmentConfig = (strict = true) => removeEmpty({
76
77
  spaceId: process.env.CONTENTFUL_SPACE_ID,
77
78
  environmentId: process.env.CONTENTFUL_ENVIRONMENT_ID,
78
79
  managementToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
79
80
  previewAccessToken: process.env.CONTENTFUL_PREVIEW_TOKEN,
80
81
  accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN,
81
- });
82
+ }, strict);
82
83
  export const getConfig = async (args) => {
83
84
  const defaultOptions = {
84
85
  environmentId: 'master',
85
86
  host: 'api.contentful.com',
86
87
  directory: resolve(process.cwd(), 'content'),
88
+ managedDirectories: [],
87
89
  plugins: [],
88
90
  resolvedPlugins: [],
89
91
  };
90
- const environmentOptions = getEnvironmentConfig();
92
+ const environmentOptions = getEnvironmentConfig(false);
91
93
  let contentfulCliOptions = {};
92
94
  try {
93
95
  const contentfulConfig = await loadConfig('contentful');
94
- if (!contentfulConfig.isEmpty) {
96
+ if (contentfulConfig && !contentfulConfig.isEmpty) {
95
97
  const { managementToken, activeSpaceId, activeEnvironmentId, host } = contentfulConfig.config;
96
98
  contentfulCliOptions = removeEmpty({
97
99
  spaceId: activeSpaceId,
98
100
  managementToken,
99
101
  environmentId: activeEnvironmentId,
100
102
  host,
101
- });
103
+ }, false);
102
104
  }
103
105
  }
104
106
  catch (error) {
@@ -116,7 +118,7 @@ export const getConfig = async (args) => {
116
118
  args.rootDir = process.cwd();
117
119
  try {
118
120
  const configFile = await loadConfig('contentful-ssg');
119
- if (!configFile.isEmpty) {
121
+ if (configFile && !configFile.isEmpty) {
120
122
  configFileOptions = configFile.config;
121
123
  args.rootDir = dirname(configFile.filepath);
122
124
  if (configFileOptions.directory && !isAbsolute(configFileOptions.directory)) {
@@ -140,5 +142,10 @@ export const getConfig = async (args) => {
140
142
  ...result.resolvedPlugins,
141
143
  ...(await Promise.all((result.plugins || []).map(async (plugin) => resolvePlugin(plugin, result)))),
142
144
  ];
143
- return { ...result, resolvedPlugins };
145
+ result.managedDirectories = [...result.managedDirectories, result.directory];
146
+ const hookedConfig = await reduceAsync(resolvedPlugins.filter((plugin) => typeof plugin.config === 'function'), async (prev, hooks) => {
147
+ const hook = hooks.config;
148
+ return hook(prev);
149
+ }, result);
150
+ return { ...hookedConfig, ...result, resolvedPlugins };
144
151
  };
@@ -2,6 +2,7 @@ import { dirname, resolve, relative, join } from 'path';
2
2
  import ignore from 'ignore';
3
3
  import { readFile, readdir, lstat } from 'fs/promises';
4
4
  import { remove, outputFile } from 'fs-extra';
5
+ import { existsSync } from 'fs';
5
6
  export class FileManager {
6
7
  ignoreBase = process.cwd();
7
8
  ignore;
@@ -25,7 +26,11 @@ export class FileManager {
25
26
  const ignorePatterns = await readFile(gitignore);
26
27
  this.ignore = ignore().add(ignorePatterns.toString('utf8'));
27
28
  }
28
- const existing = await globby(`${this.config.directory}/**/*.*`);
29
+ const directories = [
30
+ ...new Set([...(this.config.managedDirectories || []), this.config.directory]),
31
+ ];
32
+ const globPattern = directories.map((directory) => resolve(this.config.rootDir || '', directory, '**/*.*'));
33
+ const existing = await globby(globPattern);
29
34
  this.files = new Set(existing.map((file) => resolve(file)));
30
35
  }
31
36
  async writeFile(file, data, options) {
@@ -51,7 +56,7 @@ export class FileManager {
51
56
  await Promise.all(recursiveRemovalPromises);
52
57
  fileNames = await readdir(directory);
53
58
  }
54
- if (fileNames.length === 0 && directory !== this.config.directory) {
59
+ if (fileNames.length === 0 && ![this.config.directory, 'data'].includes(directory)) {
55
60
  await remove(directory);
56
61
  }
57
62
  }
@@ -59,6 +64,9 @@ export class FileManager {
59
64
  const promises = [...this.ignoredFiles].map(async (file) => this.deleteFile(file));
60
65
  await Promise.allSettled(promises);
61
66
  await this.removeEmptyDirectories(this.config.directory);
67
+ if (existsSync('data')) {
68
+ await this.removeEmptyDirectories('data');
69
+ }
62
70
  return true;
63
71
  }
64
72
  }
@@ -5,7 +5,8 @@ export declare const isObject: (something: any) => boolean;
5
5
  export declare const getEntries: <T>(obj: T) => Entries<T>;
6
6
  export declare const fromEntries: <T = [string, unknown][]>(entries: Entries<T>) => T;
7
7
  export declare const omitKeys: <T, K extends keyof T>(obj: T, ...keys: K[]) => T;
8
- export declare const removeEmpty: <T>(iterable: T) => T;
8
+ export declare const filterKeys: <T, K extends keyof T>(obj: T, ...keys: K[]) => T;
9
+ export declare const removeEmpty: <T>(iterable: T, strict?: boolean) => T;
9
10
  export declare const snakeCaseKeys: <T>(iterable: T) => T;
10
11
  export declare const groupBy: <T extends Record<string, unknown>>(array: T[], key: keyof T) => Record<string, T[]>;
11
12
  export {};
@@ -8,14 +8,19 @@ export const omitKeys = (obj, ...keys) => {
8
8
  const filtered = entries.filter(([key]) => !keys.includes(key));
9
9
  return fromEntries(filtered);
10
10
  };
11
- export const removeEmpty = (iterable) => {
11
+ export const filterKeys = (obj, ...keys) => {
12
+ const entries = getEntries(obj);
13
+ const filtered = entries.filter(([key]) => keys.includes(key));
14
+ return fromEntries(filtered);
15
+ };
16
+ export const removeEmpty = (iterable, strict = true) => {
12
17
  if (Array.isArray(iterable)) {
13
18
  return iterable
14
- .filter((v) => v !== null && v !== undefined)
19
+ .filter((v) => v !== null && v !== undefined && (strict || Boolean(v)))
15
20
  .map((v) => (v === Object(v) ? removeEmpty(v) : v));
16
21
  }
17
22
  return fromEntries(getEntries(iterable)
18
- .filter(([, v]) => v !== null && v !== undefined)
23
+ .filter(([, v]) => v !== null && v !== undefined && (strict || Boolean(v)))
19
24
  .map(([k, v]) => [k, v === Object(v) ? removeEmpty(v) : v]));
20
25
  };
21
26
  export const snakeCaseKeys = (iterable) => {
package/dist/types.d.ts CHANGED
@@ -30,10 +30,12 @@ export interface ContentfulRcConfig {
30
30
  activeEnvironmentId: string;
31
31
  host: string;
32
32
  }
33
+ export declare type ConfigHook = (config: Config) => Config | Promise<Config>;
33
34
  export declare type RuntimeHook = (runtimeContext: RuntimeContext) => Promise<Partial<RuntimeContext>> | Partial<RuntimeContext> | void;
34
35
  export declare type TransformHook<T> = (transformContext: TransformContext, runtimeContext?: RuntimeContext, prev?: T) => Promise<T> | T;
35
36
  export declare type ValidateHook = (transformContext: TransformContext, runtimeContext?: RuntimeContext) => Promise<boolean> | boolean;
36
37
  export interface Hooks {
38
+ config?: ConfigHook;
37
39
  before?: RuntimeHook;
38
40
  after?: RuntimeHook;
39
41
  transform?: TransformHook<KeyValueMap>;
@@ -46,6 +48,7 @@ export interface Hooks {
46
48
  export declare type Config = Partial<ContentfulConfig> & Hooks & {
47
49
  rootDir?: string;
48
50
  directory: string;
51
+ managedDirectories?: string[];
49
52
  verbose?: boolean;
50
53
  plugins?: Array<[string, KeyValueMap] | PluginInfo | string>;
51
54
  resolvedPlugins?: Hooks[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungvonmatt/contentful-ssg",
3
- "version": "1.0.4",
3
+ "version": "1.4.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -8,10 +8,14 @@
8
8
  "exports": {
9
9
  ".": "./dist/index.js",
10
10
  "./lib/object": "./dist/lib/object.js",
11
+ "./lib/config": "./dist/lib/config.js",
11
12
  "./lib/array": "./dist/lib/array.js",
13
+ "./lib/utils": "./dist/lib/utils.js",
14
+ "./lib/ui": "./dist/lib/ui.js",
12
15
  "./lib/contentful": "./dist/lib/contentful.js",
13
16
  "./lib/hook-manager": "./dist/lib/hook-manager.js",
14
17
  "./lib/file-manager": "./dist/lib/file-manager.js",
18
+ "./converter": "./dist/converter/index.js",
15
19
  "./converter/json": "./dist/converter/json.js",
16
20
  "./converter/yaml": "./dist/converter/yaml.js",
17
21
  "./converter/toml": "./dist/converter/toml.js",
@@ -23,6 +27,15 @@
23
27
  ".": [
24
28
  "./dist/types.d.ts"
25
29
  ],
30
+ "lib/utils": [
31
+ "./dist/lib/utils.d.ts"
32
+ ],
33
+ "lib/ui": [
34
+ "./dist/lib/ui.d.ts"
35
+ ],
36
+ "lib/config": [
37
+ "./dist/lib/config.d.ts"
38
+ ],
26
39
  "lib/object": [
27
40
  "./dist/lib/object.d.ts"
28
41
  ],
@@ -38,6 +51,9 @@
38
51
  "lib/file-manager": [
39
52
  "./dist/lib/file-manager.d.ts"
40
53
  ],
54
+ "converter": [
55
+ "./dist/converter/index.d.ts"
56
+ ],
41
57
  "converter/json": [
42
58
  "./dist/converter/json.d.ts"
43
59
  ],
@@ -75,7 +91,7 @@
75
91
  "dist"
76
92
  ],
77
93
  "author": "Jung von Matt TECH GmbH",
78
- "license": "ISC",
94
+ "license": "MIT",
79
95
  "dependencies": {
80
96
  "@contentful/rich-text-html-renderer": "^15.6.2",
81
97
  "@contentful/rich-text-types": "^15.6.2",
@@ -139,5 +155,5 @@
139
155
  "module": "es2020"
140
156
  }
141
157
  },
142
- "gitHead": "9da40bcd4fa8124cf7ae3ff6a2dcfca91c82c4fb"
158
+ "gitHead": "0d323f9884ae8fa48049e25bcd127954ee7988b8"
143
159
  }
package/src/cli.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  import path from 'path';
5
5
  import chalk from 'chalk';
6
6
  import { existsSync } from 'fs';
7
+ import { readFile } from 'fs/promises';
7
8
  import { outputFile } from 'fs-extra';
8
9
  import prettier from 'prettier';
9
10
  import { Command } from 'commander';
@@ -67,9 +68,34 @@ program
67
68
  verified.directory = path.relative(process.cwd(), verified.directory);
68
69
  }
69
70
 
70
- const environmentKeys: Array<keyof ContentfulConfig> = (
71
- Object.keys(environmentConfig) as Array<keyof ContentfulConfig>
72
- ).filter((key) => environmentConfig[key] === verified[key]);
71
+ const environmentKeys: Array<keyof ContentfulConfig> = Object.keys(
72
+ environmentConfig
73
+ ) as Array<keyof ContentfulConfig>;
74
+
75
+ // Update .env file
76
+ if (environmentConfig && existsSync('.env')) {
77
+ const envSource = await readFile('.env', 'utf8');
78
+ const nextEnvSource = envSource
79
+ .replace(/(CONTENTFUL_SPACE_ID\s*=\s*['"]?)[^'"]*(['"]?)/, `$1${verified.spaceId}$2`)
80
+ .replace(
81
+ /(CONTENTFUL_ENVIRONMENT_ID\s*=\s*['"]?)[^'"]*(['"]?)/,
82
+ `$1${verified.environmentId}$2`
83
+ )
84
+ .replace(
85
+ /(CONTENTFUL_MANAGEMENT_TOKEN\s*=\s*['"]?)[^'"]*(['"]?)/,
86
+ `$1${verified.managementToken}$2`
87
+ )
88
+ .replace(
89
+ /(CONTENTFUL_PREVIEW_TOKEN\s*=\s*['"]?)[^'"]*(['"]?)/,
90
+ `$1${verified.previewAccessToken}$2`
91
+ )
92
+ .replace(
93
+ /(CONTENTFUL_DELIVERY_TOKEN\s*=\s*['"]?)[^'"]*(['"]?)/,
94
+ `$1${verified.accessToken}$2`
95
+ );
96
+
97
+ await outputFile('.env', nextEnvSource);
98
+ }
73
99
 
74
100
  const cleanedConfig = omitKeys(
75
101
  verified,
package/src/lib/config.ts CHANGED
@@ -14,6 +14,7 @@ import type {
14
14
  PluginInfo,
15
15
  PluginModule,
16
16
  } from '../types.js';
17
+ import { reduceAsync } from './array.js';
17
18
  import { createRequire } from './create-require.js';
18
19
  import { isObject, removeEmpty } from './object.js';
19
20
 
@@ -105,14 +106,17 @@ const loadConfig = async (moduleName: string): Promise<CosmiconfigResult> => {
105
106
  return explorer.search();
106
107
  };
107
108
 
108
- export const getEnvironmentConfig = (): ContentfulConfig =>
109
- removeEmpty({
110
- spaceId: process.env.CONTENTFUL_SPACE_ID!,
111
- environmentId: process.env.CONTENTFUL_ENVIRONMENT_ID!,
112
- managementToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN!,
113
- previewAccessToken: process.env.CONTENTFUL_PREVIEW_TOKEN!,
114
- accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN!,
115
- });
109
+ export const getEnvironmentConfig = (strict = true): ContentfulConfig =>
110
+ removeEmpty(
111
+ {
112
+ spaceId: process.env.CONTENTFUL_SPACE_ID!,
113
+ environmentId: process.env.CONTENTFUL_ENVIRONMENT_ID!,
114
+ managementToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN!,
115
+ previewAccessToken: process.env.CONTENTFUL_PREVIEW_TOKEN!,
116
+ accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN!,
117
+ },
118
+ strict
119
+ );
116
120
 
117
121
  /**
118
122
  * Get configuration
@@ -123,26 +127,30 @@ export const getConfig = async (args?: Partial<Config>): Promise<Config> => {
123
127
  environmentId: 'master',
124
128
  host: 'api.contentful.com',
125
129
  directory: resolve(process.cwd(), 'content'),
130
+ managedDirectories: [],
126
131
  plugins: [],
127
132
  resolvedPlugins: [],
128
133
  };
129
134
 
130
- const environmentOptions = getEnvironmentConfig();
135
+ const environmentOptions = getEnvironmentConfig(false);
131
136
  let contentfulCliOptions: Partial<ContentfulConfig> = {};
132
137
 
133
138
  try {
134
139
  // Get configuration from contentful rc file (created by the contentful cli command)
135
140
  const contentfulConfig = await loadConfig('contentful');
136
- if (!contentfulConfig.isEmpty) {
141
+ if (contentfulConfig && !contentfulConfig.isEmpty) {
137
142
  const { managementToken, activeSpaceId, activeEnvironmentId, host } =
138
143
  contentfulConfig.config as ContentfulRcConfig;
139
144
 
140
- contentfulCliOptions = removeEmpty({
141
- spaceId: activeSpaceId,
142
- managementToken,
143
- environmentId: activeEnvironmentId,
144
- host,
145
- });
145
+ contentfulCliOptions = removeEmpty(
146
+ {
147
+ spaceId: activeSpaceId,
148
+ managementToken,
149
+ environmentId: activeEnvironmentId,
150
+ host,
151
+ },
152
+ false
153
+ );
146
154
  }
147
155
  } catch (error: unknown) {
148
156
  if (typeof error === 'string') {
@@ -159,7 +167,7 @@ export const getConfig = async (args?: Partial<Config>): Promise<Config> => {
159
167
  try {
160
168
  // Get configuration from contentful-ssg rc file
161
169
  const configFile = await loadConfig('contentful-ssg');
162
- if (!configFile.isEmpty) {
170
+ if (configFile && !configFile.isEmpty) {
163
171
  configFileOptions = configFile.config as Partial<Config>;
164
172
  args.rootDir = dirname(configFile.filepath);
165
173
  if (configFileOptions.directory && !isAbsolute(configFileOptions.directory)) {
@@ -191,5 +199,17 @@ export const getConfig = async (args?: Partial<Config>): Promise<Config> => {
191
199
  (result.plugins || []).map(async (plugin) => resolvePlugin(plugin, result))
192
200
  )),
193
201
  ];
194
- return { ...result, resolvedPlugins };
202
+
203
+ result.managedDirectories = [...result.managedDirectories, result.directory];
204
+
205
+ const hookedConfig = await reduceAsync(
206
+ resolvedPlugins.filter((plugin) => typeof plugin.config === 'function'),
207
+ async (prev: Config, hooks) => {
208
+ const hook = hooks.config;
209
+ return hook(prev);
210
+ },
211
+ result
212
+ );
213
+
214
+ return { ...hookedConfig, ...result, resolvedPlugins };
195
215
  };
@@ -4,6 +4,7 @@ import { dirname, resolve, relative, join } from 'path';
4
4
  import ignore from 'ignore';
5
5
  import { readFile, readdir, lstat } from 'fs/promises';
6
6
  import { remove, outputFile } from 'fs-extra';
7
+ import { existsSync } from 'fs';
7
8
 
8
9
  export class FileManager {
9
10
  ignoreBase: string = process.cwd();
@@ -38,8 +39,15 @@ export class FileManager {
38
39
  this.ignore = ignore().add(ignorePatterns.toString('utf8'));
39
40
  }
40
41
 
42
+ const directories = [
43
+ ...new Set([...(this.config.managedDirectories || []), this.config.directory]),
44
+ ];
45
+
46
+ const globPattern = directories.map((directory) =>
47
+ resolve(this.config.rootDir || '', directory, '**/*.*')
48
+ );
41
49
  // Create set of existing files
42
- const existing = await globby(`${this.config.directory}/**/*.*`);
50
+ const existing = await globby(globPattern);
43
51
 
44
52
  this.files = new Set(existing.map((file) => resolve(file)));
45
53
  }
@@ -91,7 +99,7 @@ export class FileManager {
91
99
  fileNames = await readdir(directory);
92
100
  }
93
101
 
94
- if (fileNames.length === 0 && directory !== this.config.directory) {
102
+ if (fileNames.length === 0 && ![this.config.directory, 'data'].includes(directory)) {
95
103
  await remove(directory);
96
104
  }
97
105
  }
@@ -101,6 +109,10 @@ export class FileManager {
101
109
 
102
110
  await Promise.allSettled(promises);
103
111
  await this.removeEmptyDirectories(this.config.directory);
112
+ if (existsSync('data')) {
113
+ await this.removeEmptyDirectories('data');
114
+ }
115
+
104
116
  return true;
105
117
  }
106
118
  }
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/naming-convention */
2
- import { isObject, omitKeys, removeEmpty, snakeCaseKeys, groupBy } from './object';
2
+ import { isObject, omitKeys, filterKeys, removeEmpty, snakeCaseKeys, groupBy } from './object';
3
3
 
4
4
  test('isObject', async () => {
5
5
  const arr = [];
@@ -17,6 +17,11 @@ test('omitKeys', () => {
17
17
  expect(value).toEqual({ b: 2 });
18
18
  });
19
19
 
20
+ test('filterKeys', () => {
21
+ const value = filterKeys({ a: 1, b: 2, c: 3 }, 'a', 'c');
22
+ expect(value).toEqual({ a: 1, c: 3 });
23
+ });
24
+
20
25
  test('removeEmpty', () => {
21
26
  const value = removeEmpty({
22
27
  a: { c: undefined },
@@ -26,6 +31,16 @@ test('removeEmpty', () => {
26
31
  expect(value).toEqual({ a: {}, c: [1, { x: 1, z: [7] }, 3, 5] });
27
32
  });
28
33
 
34
+
35
+ test('removeEmpty non-strict', () => {
36
+ const value = removeEmpty({
37
+ a: "",
38
+ b: 0,
39
+ c: 1,
40
+ }, false);
41
+ expect(value).toEqual({ c: 1 });
42
+ });
43
+
29
44
  test('groupBy', () => {
30
45
  const value = groupBy(
31
46
  [
package/src/lib/object.ts CHANGED
@@ -30,16 +30,27 @@ export const omitKeys = <T, K extends keyof T>(obj: T, ...keys: K[]): T => {
30
30
  return fromEntries(filtered);
31
31
  };
32
32
 
33
+ /**
34
+ * Filter values by key from object
35
+ * @param {*} obj
36
+ * @param {*} keys
37
+ */
38
+ export const filterKeys = <T, K extends keyof T>(obj: T, ...keys: K[]): T => {
39
+ const entries: Entries<T> = getEntries(obj);
40
+ const filtered = entries.filter(([key]) => keys.includes(key as K));
41
+ return fromEntries(filtered);
42
+ };
43
+
33
44
  /**
34
45
  * Recursive remove empty items (null,undefined) from object
35
46
  * @param iterable Source object
36
47
  * @returns Cleaned object
37
48
  */
38
- export const removeEmpty = <T>(iterable: T): T => {
49
+ export const removeEmpty = <T>(iterable: T, strict = true): T => {
39
50
  if (Array.isArray(iterable)) {
40
51
  return (
41
52
  iterable
42
- .filter((v) => v !== null && v !== undefined)
53
+ .filter((v) => v !== null && v !== undefined && (strict || Boolean(v)))
43
54
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
44
55
  .map((v) => (v === Object(v) ? removeEmpty(v) : v)) as unknown as T
45
56
  );
@@ -47,7 +58,7 @@ export const removeEmpty = <T>(iterable: T): T => {
47
58
 
48
59
  return fromEntries(
49
60
  getEntries(iterable)
50
- .filter(([, v]) => v !== null && v !== undefined)
61
+ .filter(([, v]) => v !== null && v !== undefined && (strict || Boolean(v)))
51
62
  .map(([k, v]) => [k, v === Object(v) ? removeEmpty(v) : v])
52
63
  );
53
64
  };
package/src/types.ts CHANGED
@@ -52,6 +52,8 @@ export interface ContentfulRcConfig {
52
52
  host: string;
53
53
  }
54
54
 
55
+ export type ConfigHook = (config: Config) => Config | Promise<Config>;
56
+
55
57
  export type RuntimeHook = (
56
58
  runtimeContext: RuntimeContext
57
59
  ) => Promise<Partial<RuntimeContext>> | Partial<RuntimeContext> | void;
@@ -66,6 +68,7 @@ export type ValidateHook = (
66
68
  ) => Promise<boolean> | boolean;
67
69
 
68
70
  export interface Hooks {
71
+ config?: ConfigHook;
69
72
  before?: RuntimeHook;
70
73
  after?: RuntimeHook;
71
74
  transform?: TransformHook<KeyValueMap>;
@@ -80,6 +83,7 @@ export type Config = Partial<ContentfulConfig> &
80
83
  Hooks & {
81
84
  rootDir?: string;
82
85
  directory: string;
86
+ managedDirectories?: string[];
83
87
  verbose?: boolean;
84
88
  plugins?: Array<[string, KeyValueMap] | PluginInfo | string>;
85
89
  resolvedPlugins?: Hooks[];