@jungvonmatt/contentful-ssg 1.0.0 → 1.0.6
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/README.md +71 -27
- package/dist/cli.js +12 -1
- package/dist/index.js +1 -6
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.js +20 -8
- package/dist/lib/file-manager.d.ts +2 -1
- package/dist/lib/file-manager.js +25 -12
- package/dist/lib/object.d.ts +2 -1
- package/dist/lib/object.js +8 -3
- package/dist/types.d.ts +1 -1
- package/package.json +7 -2
- package/src/cli.ts +29 -3
- package/src/index.ts +1 -7
- package/src/lib/config.ts +38 -20
- package/src/lib/file-manager.test.ts +6 -6
- package/src/lib/file-manager.ts +45 -13
- package/src/lib/object.test.ts +16 -1
- package/src/lib/object.ts +14 -3
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -40,28 +40,67 @@ npx cssg init --typescript
|
|
|
40
40
|
<!-- prettier-ignore -->
|
|
41
41
|
#### Configuration values
|
|
42
42
|
|
|
43
|
-
| Name | Type
|
|
44
|
-
| ------------------ |
|
|
45
|
-
| accessToken | `
|
|
46
|
-
| previewAccessToken | `
|
|
47
|
-
| spaceId | `
|
|
48
|
-
| environmentId | `
|
|
49
|
-
| format | `
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
43
|
+
| Name | Type | Default | Description |
|
|
44
|
+
| ------------------ | --------------------------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| accessToken | `string` | `undefined` | Content Delivery API - access token |
|
|
46
|
+
| previewAccessToken | `string` | `undefined` | Content Preview API - access token |
|
|
47
|
+
| spaceId | `string` | `undefined` | Contentful Space id |
|
|
48
|
+
| environmentId | `string` | `'master'` | Contentful Environment id |
|
|
49
|
+
| format | `string`\|`function`\|`object` | `'yaml'` | File format ( `yaml`, `toml`, `md`, `json`) You can add a function returning the format or you can add a mapping object like `{yaml: [glob pattern]}` ([pattern](https://github.com/micromatch/micromatch) should match the directory) |
|
|
50
|
+
| plugins | `[string]`\|`[[string, options]]`\|`[{resolve:'string', options:{}}]` | `[]` | Add plugins to contentful-ssg. See [Plugins](#plugins) |
|
|
51
|
+
| directory | `string` | `'./content'` | Base directory for content files. |
|
|
52
|
+
| validate | `function` | `undefined` | Pass `function(transformContext, runtimeContext){...}` to validate an entry. Return `false` to skip the entry completely. Without a validate function entries with a missing required field are skipped. |
|
|
53
|
+
| transform | `function` | `undefined` | Pass `function(transformContext, runtimeContext){...}` to modify the stored object. Return `undefined` to skip the entry completely. (no file will be written) |
|
|
54
|
+
| mapDirectory | `function` | `undefined` | Pass `function(transformContext, runtimeContext, defaultValue){...}` to customize the directory per content-type relative to the base directory. |
|
|
55
|
+
| mapFilename | `function` | `undefined` | Pass `function(transformContext, runtimeContext, defaultValue){...}` to customize the filename per entry |
|
|
56
|
+
| mapAssetLink | `function` | `undefined` | Pass `function(transformContext, runtimeContext, defaultValue){...}` to customize how asset links are stored |
|
|
57
|
+
| mapEntryLink | `function` | `undefined` | Pass `function(transformContext, runtimeContext, defaultValue){...}` to customize how entry links are stored |
|
|
58
|
+
| mapMetaFields | `function` | `undefined` | Pass `function(transformContext, runtimeContext, defaultValue){...}` to customize the meta fields per entry |
|
|
59
|
+
| richTextRenderer | `boolean`\|`object`\|`function` | `{}` | We use the contentful [`rich-text-html-renderer`](https://github.com/contentful/rich-text/tree/master/packages/rich-text-html-renderer) to render the html.<br/> You can pass a [configuration object](https://github.com/contentful/rich-text/tree/master/packages/rich-text-html-renderer#usage)<br/> or you can pass `function(document){...}` to use your own richtext renderer or you can turn it off by passing `false` to get a mirrored version of the JSON output |
|
|
60
|
+
| before | `function` | `undefined` | Runs `function(runtimeContext){...}` before processing the content right after pulling data from contentful |
|
|
61
|
+
| after | `function` | `undefined` | Runs `function(runtimeContext){...}` after processing the content right before the cleanup |
|
|
62
|
+
|
|
63
|
+
### Plugins
|
|
64
|
+
|
|
65
|
+
You can add plugins to contentful-ssg by adding the package name or a local path to the plugins array of your configuration.
|
|
61
66
|
|
|
67
|
+
```js
|
|
68
|
+
plugins: ['my-plugin-package', './plugins/my-local-plugin]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
All plugins can have options specified by wrapping the name and an options object in an array inside your config or by using a more verbose object notation.
|
|
72
|
+
|
|
73
|
+
For specifying no options, these are all equivalent:
|
|
74
|
+
```js
|
|
75
|
+
{
|
|
76
|
+
"plugins": ["my-plugin", ["my-plugin"], ["my-plugin", {}], {resolve: "my-plugin", options: {}}]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
To specify an option, pass an object with the keys as the option names.
|
|
81
|
+
```js
|
|
82
|
+
{
|
|
83
|
+
"plugins": [
|
|
84
|
+
[
|
|
85
|
+
"my-plugin-a",
|
|
86
|
+
{
|
|
87
|
+
"option": "value"
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
{
|
|
91
|
+
resolve: "my-plugin-b",
|
|
92
|
+
options: {
|
|
93
|
+
"option": "value"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
62
99
|
|
|
63
100
|
### Runtime Hooks
|
|
101
|
+
|
|
64
102
|
**before**
|
|
103
|
+
|
|
65
104
|
```js
|
|
66
105
|
import
|
|
67
106
|
(runtimeContext) => {
|
|
@@ -72,44 +111,50 @@ import
|
|
|
72
111
|
```
|
|
73
112
|
|
|
74
113
|
**after**
|
|
114
|
+
|
|
75
115
|
```js
|
|
76
116
|
(runtimeContext) => {
|
|
77
117
|
// Do things after processing the localized contentful entries before cleanup
|
|
78
118
|
// We have access to values added to the context in the before hook
|
|
79
|
-
console.log(runtimeContext.key) // -> 'test'
|
|
80
|
-
}
|
|
119
|
+
console.log(runtimeContext.key); // -> 'test'
|
|
120
|
+
};
|
|
81
121
|
```
|
|
82
122
|
|
|
83
123
|
### Transform Hooks
|
|
124
|
+
|
|
84
125
|
**transform**
|
|
126
|
+
|
|
85
127
|
```js
|
|
86
128
|
(transformContext, runtimeContext) => {
|
|
87
|
-
const {content} = transformContext;
|
|
129
|
+
const { content } = transformContext;
|
|
88
130
|
// modify content and
|
|
89
131
|
// return object
|
|
90
132
|
return content;
|
|
91
|
-
}
|
|
133
|
+
};
|
|
92
134
|
```
|
|
93
135
|
|
|
94
136
|
**mapFilename**
|
|
137
|
+
|
|
95
138
|
```js
|
|
96
139
|
(transformContext, runtimeContext, defaultValue) => {
|
|
97
140
|
// customize the filename on entry level
|
|
98
141
|
// return string
|
|
99
142
|
return defaultValue;
|
|
100
|
-
}
|
|
143
|
+
};
|
|
101
144
|
```
|
|
102
145
|
|
|
103
146
|
**mapDirectory**
|
|
147
|
+
|
|
104
148
|
```js
|
|
105
149
|
(transformContext, runtimeContext, defaultValue) => {
|
|
106
150
|
// customize the directory on entry level
|
|
107
151
|
// return string
|
|
108
152
|
return defaultValue;
|
|
109
|
-
}
|
|
153
|
+
};
|
|
110
154
|
```
|
|
111
155
|
|
|
112
156
|
**mapAssetLink**
|
|
157
|
+
|
|
113
158
|
```js
|
|
114
159
|
(transformContext, runtimeContext, defaultValue) => {
|
|
115
160
|
const {asset} = transformContext;
|
|
@@ -120,6 +165,7 @@ import
|
|
|
120
165
|
```
|
|
121
166
|
|
|
122
167
|
**mapEntryLink**
|
|
168
|
+
|
|
123
169
|
```js
|
|
124
170
|
(transformContext, runtimeContext, defaultValue) => {
|
|
125
171
|
const {entry} = transformContext;
|
|
@@ -130,6 +176,7 @@ import
|
|
|
130
176
|
```
|
|
131
177
|
|
|
132
178
|
**mapMetaFields**
|
|
179
|
+
|
|
133
180
|
```js
|
|
134
181
|
(transformContext, runtimeContext, defaultValue) => {
|
|
135
182
|
const {entry} = transformContext;
|
|
@@ -139,7 +186,6 @@ import
|
|
|
139
186
|
}
|
|
140
187
|
```
|
|
141
188
|
|
|
142
|
-
|
|
143
189
|
#### Helper functions
|
|
144
190
|
|
|
145
191
|
###### collectValues
|
|
@@ -177,12 +223,10 @@ npx cssg fetch
|
|
|
177
223
|
|
|
178
224
|
See [`cssg-plugin-grow`](../cssg-plugin-grow)
|
|
179
225
|
|
|
180
|
-
|
|
181
226
|
### Hugo
|
|
182
227
|
|
|
183
228
|
See [`cssg-plugin-hugo`](../cssg-plugin-hugo)
|
|
184
229
|
|
|
185
|
-
|
|
186
230
|
[npm-url]: https://www.npmjs.com/package/@jungvonmatt/contentful-ssg
|
|
187
231
|
[npm-image]: https://img.shields.io/npm/v/@jungvonmatt/contentful-ssg.svg
|
|
188
232
|
[ci-url]: https://github.com/jungvonmatt/contentful-ssg/actions?workflow=Tests
|
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)
|
|
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) {
|
package/dist/index.js
CHANGED
|
@@ -116,12 +116,7 @@ export const run = async (config) => {
|
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
title: 'Cleanup',
|
|
119
|
-
|
|
120
|
-
task: async (ctx) => {
|
|
121
|
-
console.log(`Cleaning ${ctx.fileManager.count} files...`);
|
|
122
|
-
await ctx.fileManager.cleanup();
|
|
123
|
-
console.log('done');
|
|
124
|
-
},
|
|
119
|
+
task: async (ctx) => ctx.fileManager.cleanup(),
|
|
125
120
|
},
|
|
126
121
|
], { renderer: CustomListrRenderer });
|
|
127
122
|
const ctx = await tasks.run();
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -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>;
|
package/dist/lib/config.js
CHANGED
|
@@ -14,8 +14,20 @@ const typescriptLoader = async (filePath) => {
|
|
|
14
14
|
};
|
|
15
15
|
const mergeOptions = mergeOptionsModule.bind({ ignoreUndefined: true });
|
|
16
16
|
const resolvePlugin = async (plugin, config) => {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
let pluginName;
|
|
18
|
+
let pluginOptions;
|
|
19
|
+
if (typeof plugin === 'string') {
|
|
20
|
+
pluginName = plugin;
|
|
21
|
+
pluginOptions = {};
|
|
22
|
+
}
|
|
23
|
+
else if (Array.isArray(plugin)) {
|
|
24
|
+
pluginName = plugin[0];
|
|
25
|
+
pluginOptions = plugin[1] || {};
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
pluginName = plugin.resolve;
|
|
29
|
+
pluginOptions = plugin.options || {};
|
|
30
|
+
}
|
|
19
31
|
const { rootDir, verbose } = config;
|
|
20
32
|
try {
|
|
21
33
|
const requireSource = rootDir === null ? createRequire() : createRequire(`${rootDir}/:internal:`);
|
|
@@ -60,13 +72,13 @@ const loadConfig = async (moduleName) => {
|
|
|
60
72
|
});
|
|
61
73
|
return explorer.search();
|
|
62
74
|
};
|
|
63
|
-
export const getEnvironmentConfig = () => removeEmpty({
|
|
75
|
+
export const getEnvironmentConfig = (strict = true) => removeEmpty({
|
|
64
76
|
spaceId: process.env.CONTENTFUL_SPACE_ID,
|
|
65
77
|
environmentId: process.env.CONTENTFUL_ENVIRONMENT_ID,
|
|
66
78
|
managementToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
|
|
67
79
|
previewAccessToken: process.env.CONTENTFUL_PREVIEW_TOKEN,
|
|
68
80
|
accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN,
|
|
69
|
-
});
|
|
81
|
+
}, strict);
|
|
70
82
|
export const getConfig = async (args) => {
|
|
71
83
|
const defaultOptions = {
|
|
72
84
|
environmentId: 'master',
|
|
@@ -75,18 +87,18 @@ export const getConfig = async (args) => {
|
|
|
75
87
|
plugins: [],
|
|
76
88
|
resolvedPlugins: [],
|
|
77
89
|
};
|
|
78
|
-
const environmentOptions = getEnvironmentConfig();
|
|
90
|
+
const environmentOptions = getEnvironmentConfig(false);
|
|
79
91
|
let contentfulCliOptions = {};
|
|
80
92
|
try {
|
|
81
93
|
const contentfulConfig = await loadConfig('contentful');
|
|
82
|
-
if (!contentfulConfig.isEmpty) {
|
|
94
|
+
if (contentfulConfig && !contentfulConfig.isEmpty) {
|
|
83
95
|
const { managementToken, activeSpaceId, activeEnvironmentId, host } = contentfulConfig.config;
|
|
84
96
|
contentfulCliOptions = removeEmpty({
|
|
85
97
|
spaceId: activeSpaceId,
|
|
86
98
|
managementToken,
|
|
87
99
|
environmentId: activeEnvironmentId,
|
|
88
100
|
host,
|
|
89
|
-
});
|
|
101
|
+
}, false);
|
|
90
102
|
}
|
|
91
103
|
}
|
|
92
104
|
catch (error) {
|
|
@@ -104,7 +116,7 @@ export const getConfig = async (args) => {
|
|
|
104
116
|
args.rootDir = process.cwd();
|
|
105
117
|
try {
|
|
106
118
|
const configFile = await loadConfig('contentful-ssg');
|
|
107
|
-
if (!configFile.isEmpty) {
|
|
119
|
+
if (configFile && !configFile.isEmpty) {
|
|
108
120
|
configFileOptions = configFile.config;
|
|
109
121
|
args.rootDir = dirname(configFile.filepath);
|
|
110
122
|
if (configFileOptions.directory && !isAbsolute(configFileOptions.directory)) {
|
|
@@ -12,5 +12,6 @@ export declare class FileManager {
|
|
|
12
12
|
initialize(): Promise<void>;
|
|
13
13
|
writeFile(file: string, data: any, options?: WriteFileOptions | BufferEncoding | string): Promise<void>;
|
|
14
14
|
deleteFile(file: string): Promise<void>;
|
|
15
|
-
|
|
15
|
+
removeEmptyDirectories(directory: string): Promise<void>;
|
|
16
|
+
cleanup(): Promise<boolean>;
|
|
16
17
|
}
|
package/dist/lib/file-manager.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { dirname, resolve, relative } from 'path';
|
|
1
|
+
import { dirname, resolve, relative, join } from 'path';
|
|
2
2
|
import ignore from 'ignore';
|
|
3
|
-
import { readFile, readdir } from 'fs/promises';
|
|
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,7 @@ 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 existing = await globby([`${this.config.directory}/**/*.*`, `data/**/*.*`]);
|
|
29
30
|
this.files = new Set(existing.map((file) => resolve(file)));
|
|
30
31
|
}
|
|
31
32
|
async writeFile(file, data, options) {
|
|
@@ -35,21 +36,33 @@ export class FileManager {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
async deleteFile(file) {
|
|
38
|
-
await remove(file);
|
|
39
39
|
if (this.files.has(resolve(file))) {
|
|
40
40
|
this.files.delete(resolve(file));
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
return remove(file);
|
|
43
|
+
}
|
|
44
|
+
async removeEmptyDirectories(directory) {
|
|
45
|
+
const fileStats = await lstat(directory);
|
|
46
|
+
if (!fileStats.isDirectory()) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let fileNames = await readdir(directory);
|
|
50
|
+
if (fileNames.length > 0) {
|
|
51
|
+
const recursiveRemovalPromises = fileNames.map(async (fileName) => this.removeEmptyDirectories(join(directory, fileName)));
|
|
52
|
+
await Promise.all(recursiveRemovalPromises);
|
|
53
|
+
fileNames = await readdir(directory);
|
|
54
|
+
}
|
|
55
|
+
if (fileNames.length === 0 && ![this.config.directory, 'data'].includes(directory)) {
|
|
56
|
+
await remove(directory);
|
|
48
57
|
}
|
|
49
|
-
catch { }
|
|
50
58
|
}
|
|
51
59
|
async cleanup() {
|
|
52
60
|
const promises = [...this.ignoredFiles].map(async (file) => this.deleteFile(file));
|
|
53
|
-
|
|
61
|
+
await Promise.allSettled(promises);
|
|
62
|
+
await this.removeEmptyDirectories(this.config.directory);
|
|
63
|
+
if (existsSync('data')) {
|
|
64
|
+
await this.removeEmptyDirectories('data');
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
54
67
|
}
|
|
55
68
|
}
|
package/dist/lib/object.d.ts
CHANGED
|
@@ -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
|
|
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 {};
|
package/dist/lib/object.js
CHANGED
|
@@ -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
|
|
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
|
@@ -47,7 +47,7 @@ export declare type Config = Partial<ContentfulConfig> & Hooks & {
|
|
|
47
47
|
rootDir?: string;
|
|
48
48
|
directory: string;
|
|
49
49
|
verbose?: boolean;
|
|
50
|
-
plugins?: Array<PluginInfo | string>;
|
|
50
|
+
plugins?: Array<[string, KeyValueMap] | PluginInfo | string>;
|
|
51
51
|
resolvedPlugins?: Hooks[];
|
|
52
52
|
preset?: string;
|
|
53
53
|
richTextRenderer?: RichTextConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungvonmatt/contentful-ssg",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
".": "./dist/index.js",
|
|
10
10
|
"./lib/object": "./dist/lib/object.js",
|
|
11
11
|
"./lib/array": "./dist/lib/array.js",
|
|
12
|
+
"./lib/utils": "./dist/lib/utils.js",
|
|
12
13
|
"./lib/contentful": "./dist/lib/contentful.js",
|
|
13
14
|
"./lib/hook-manager": "./dist/lib/hook-manager.js",
|
|
14
15
|
"./lib/file-manager": "./dist/lib/file-manager.js",
|
|
@@ -23,6 +24,9 @@
|
|
|
23
24
|
".": [
|
|
24
25
|
"./dist/types.d.ts"
|
|
25
26
|
],
|
|
27
|
+
"lib/utils": [
|
|
28
|
+
"./dist/lib/utils.d.ts"
|
|
29
|
+
],
|
|
26
30
|
"lib/object": [
|
|
27
31
|
"./dist/lib/object.d.ts"
|
|
28
32
|
],
|
|
@@ -91,6 +95,7 @@
|
|
|
91
95
|
"dotenv": "^10.0.0",
|
|
92
96
|
"dotenv-expand": "^5.1.0",
|
|
93
97
|
"esbuild": "^0.13.13",
|
|
98
|
+
"find-up": "^6.2.0",
|
|
94
99
|
"fs-extra": "^10.0.0",
|
|
95
100
|
"globby": "^12.0.2",
|
|
96
101
|
"gray-matter": "^4.0.3",
|
|
@@ -138,5 +143,5 @@
|
|
|
138
143
|
"module": "es2020"
|
|
139
144
|
}
|
|
140
145
|
},
|
|
141
|
-
"gitHead": "
|
|
146
|
+
"gitHead": "72fea365aa247c8942d01c499becf8548874b09d"
|
|
142
147
|
}
|
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
|
-
|
|
72
|
-
)
|
|
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/index.ts
CHANGED
|
@@ -143,13 +143,7 @@ export const run = async (config: Config): Promise<void> => {
|
|
|
143
143
|
|
|
144
144
|
{
|
|
145
145
|
title: 'Cleanup',
|
|
146
|
-
|
|
147
|
-
task: async (ctx) => {
|
|
148
|
-
console.log(`Cleaning ${ctx.fileManager.count} files...`);
|
|
149
|
-
await ctx.fileManager.cleanup();
|
|
150
|
-
|
|
151
|
-
console.log('done');
|
|
152
|
-
},
|
|
146
|
+
task: async (ctx) => ctx.fileManager.cleanup(),
|
|
153
147
|
},
|
|
154
148
|
],
|
|
155
149
|
{ renderer: CustomListrRenderer }
|
package/src/lib/config.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
ContentfulConfig,
|
|
11
11
|
ContentfulRcConfig,
|
|
12
12
|
Hooks,
|
|
13
|
+
KeyValueMap,
|
|
13
14
|
PluginInfo,
|
|
14
15
|
PluginModule,
|
|
15
16
|
} from '../types.js';
|
|
@@ -29,11 +30,22 @@ const typescriptLoader: Loader = async (filePath: string): Promise<any> => {
|
|
|
29
30
|
const mergeOptions = mergeOptionsModule.bind({ ignoreUndefined: true });
|
|
30
31
|
|
|
31
32
|
const resolvePlugin = async (
|
|
32
|
-
plugin: string | PluginInfo,
|
|
33
|
+
plugin: string | [string, KeyValueMap] | PluginInfo,
|
|
33
34
|
config: Partial<Config>
|
|
34
35
|
): Promise<Hooks> => {
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
let pluginName: string;
|
|
37
|
+
let pluginOptions: KeyValueMap;
|
|
38
|
+
if (typeof plugin === 'string') {
|
|
39
|
+
pluginName = plugin;
|
|
40
|
+
pluginOptions = {};
|
|
41
|
+
} else if (Array.isArray(plugin)) {
|
|
42
|
+
pluginName = plugin[0];
|
|
43
|
+
pluginOptions = plugin[1] || {};
|
|
44
|
+
} else {
|
|
45
|
+
pluginName = plugin.resolve;
|
|
46
|
+
pluginOptions = plugin.options || {};
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
const { rootDir, verbose } = config;
|
|
38
50
|
|
|
39
51
|
try {
|
|
@@ -93,14 +105,17 @@ const loadConfig = async (moduleName: string): Promise<CosmiconfigResult> => {
|
|
|
93
105
|
return explorer.search();
|
|
94
106
|
};
|
|
95
107
|
|
|
96
|
-
export const getEnvironmentConfig = (): ContentfulConfig =>
|
|
97
|
-
removeEmpty(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
export const getEnvironmentConfig = (strict = true): ContentfulConfig =>
|
|
109
|
+
removeEmpty(
|
|
110
|
+
{
|
|
111
|
+
spaceId: process.env.CONTENTFUL_SPACE_ID!,
|
|
112
|
+
environmentId: process.env.CONTENTFUL_ENVIRONMENT_ID!,
|
|
113
|
+
managementToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN!,
|
|
114
|
+
previewAccessToken: process.env.CONTENTFUL_PREVIEW_TOKEN!,
|
|
115
|
+
accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN!,
|
|
116
|
+
},
|
|
117
|
+
strict
|
|
118
|
+
);
|
|
104
119
|
|
|
105
120
|
/**
|
|
106
121
|
* Get configuration
|
|
@@ -115,22 +130,25 @@ export const getConfig = async (args?: Partial<Config>): Promise<Config> => {
|
|
|
115
130
|
resolvedPlugins: [],
|
|
116
131
|
};
|
|
117
132
|
|
|
118
|
-
const environmentOptions = getEnvironmentConfig();
|
|
133
|
+
const environmentOptions = getEnvironmentConfig(false);
|
|
119
134
|
let contentfulCliOptions: Partial<ContentfulConfig> = {};
|
|
120
135
|
|
|
121
136
|
try {
|
|
122
137
|
// Get configuration from contentful rc file (created by the contentful cli command)
|
|
123
138
|
const contentfulConfig = await loadConfig('contentful');
|
|
124
|
-
if (!contentfulConfig.isEmpty) {
|
|
139
|
+
if (contentfulConfig && !contentfulConfig.isEmpty) {
|
|
125
140
|
const { managementToken, activeSpaceId, activeEnvironmentId, host } =
|
|
126
141
|
contentfulConfig.config as ContentfulRcConfig;
|
|
127
142
|
|
|
128
|
-
contentfulCliOptions = removeEmpty(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
contentfulCliOptions = removeEmpty(
|
|
144
|
+
{
|
|
145
|
+
spaceId: activeSpaceId,
|
|
146
|
+
managementToken,
|
|
147
|
+
environmentId: activeEnvironmentId,
|
|
148
|
+
host,
|
|
149
|
+
},
|
|
150
|
+
false
|
|
151
|
+
);
|
|
134
152
|
}
|
|
135
153
|
} catch (error: unknown) {
|
|
136
154
|
if (typeof error === 'string') {
|
|
@@ -147,7 +165,7 @@ export const getConfig = async (args?: Partial<Config>): Promise<Config> => {
|
|
|
147
165
|
try {
|
|
148
166
|
// Get configuration from contentful-ssg rc file
|
|
149
167
|
const configFile = await loadConfig('contentful-ssg');
|
|
150
|
-
if (!configFile.isEmpty) {
|
|
168
|
+
if (configFile && !configFile.isEmpty) {
|
|
151
169
|
configFileOptions = configFile.config as Partial<Config>;
|
|
152
170
|
args.rootDir = dirname(configFile.filepath);
|
|
153
171
|
if (configFileOptions.directory && !isAbsolute(configFileOptions.directory)) {
|
|
@@ -11,8 +11,9 @@ jest.mock('fs-extra', () => ({
|
|
|
11
11
|
remove: jest.fn(),
|
|
12
12
|
}));
|
|
13
13
|
jest.mock('fs/promises', () => ({
|
|
14
|
+
lstat: jest.fn().mockResolvedValue({isDirectory: jest.fn().mockReturnValue(false).mockResolvedValueOnce(true).mockResolvedValueOnce(true)}),
|
|
14
15
|
readFile: jest.fn().mockResolvedValue(''),
|
|
15
|
-
readdir: jest.fn().mockResolvedValue([]).mockResolvedValueOnce(['/test
|
|
16
|
+
readdir: jest.fn().mockResolvedValue([]).mockResolvedValueOnce(['/test']),
|
|
16
17
|
}));
|
|
17
18
|
|
|
18
19
|
describe('FileManager', () => {
|
|
@@ -60,14 +61,13 @@ describe('FileManager', () => {
|
|
|
60
61
|
.mockReturnValueOnce(true)
|
|
61
62
|
.mockReturnValueOnce(true)
|
|
62
63
|
.mockReturnValue(false);
|
|
63
|
-
const result = await fileManager.cleanup();
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
expect(result
|
|
65
|
+
const result = await fileManager.cleanup();
|
|
66
|
+
expect(result).toEqual(true);
|
|
67
67
|
|
|
68
|
-
expect(remove).toHaveBeenCalledTimes(3);
|
|
69
68
|
expect(remove).toHaveBeenNthCalledWith(1, '/test/a.md');
|
|
70
69
|
expect(remove).toHaveBeenNthCalledWith(2, '/test/b.md');
|
|
71
|
-
expect(remove).toHaveBeenNthCalledWith(3, '/test');
|
|
70
|
+
expect(remove).toHaveBeenNthCalledWith(3, '/testbase/test');
|
|
71
|
+
expect(remove).toHaveBeenCalledTimes(3);
|
|
72
72
|
});
|
|
73
73
|
});
|
package/src/lib/file-manager.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { WriteFileOptions } from 'fs-extra';
|
|
2
2
|
import type { Config, Ignore } from '../types';
|
|
3
|
-
import { dirname, resolve, relative } from 'path';
|
|
3
|
+
import { dirname, resolve, relative, join } from 'path';
|
|
4
4
|
import ignore from 'ignore';
|
|
5
|
-
import { readFile, readdir } from 'fs/promises';
|
|
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();
|
|
@@ -39,7 +40,7 @@ export class FileManager {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// Create set of existing files
|
|
42
|
-
const existing = await globby(`${this.config.directory}/**/*.*`);
|
|
43
|
+
const existing = await globby([`${this.config.directory}/**/*.*`, `data/**/*.*`]);
|
|
43
44
|
|
|
44
45
|
this.files = new Set(existing.map((file) => resolve(file)));
|
|
45
46
|
}
|
|
@@ -56,24 +57,55 @@ export class FileManager {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
async deleteFile(file: string) {
|
|
59
|
-
await remove(file);
|
|
60
|
-
|
|
61
60
|
if (this.files.has(resolve(file))) {
|
|
62
61
|
this.files.delete(resolve(file));
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
return remove(file);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Recursively removes empty directories from the given directory.
|
|
69
|
+
*
|
|
70
|
+
* If the directory itself is empty, it is also removed.
|
|
71
|
+
*
|
|
72
|
+
* Code taken from: https://gist.github.com/jakub-g/5903dc7e4028133704a4
|
|
73
|
+
*
|
|
74
|
+
* @param {string} directory Path to the directory to clean up
|
|
75
|
+
*/
|
|
76
|
+
async removeEmptyDirectories(directory: string) {
|
|
77
|
+
// Lstat does not follow symlinks (in contrast to stat)
|
|
78
|
+
const fileStats = await lstat(directory);
|
|
79
|
+
if (!fileStats.isDirectory()) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let fileNames = await readdir(directory);
|
|
84
|
+
if (fileNames.length > 0) {
|
|
85
|
+
const recursiveRemovalPromises = fileNames.map(async (fileName) =>
|
|
86
|
+
this.removeEmptyDirectories(join(directory, fileName))
|
|
87
|
+
);
|
|
88
|
+
await Promise.all(recursiveRemovalPromises);
|
|
89
|
+
|
|
90
|
+
// Re-evaluate fileNames; after deleting subdirectory
|
|
91
|
+
// we may have parent directory empty now
|
|
92
|
+
fileNames = await readdir(directory);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (fileNames.length === 0 && ![this.config.directory, 'data'].includes(directory)) {
|
|
96
|
+
await remove(directory);
|
|
97
|
+
}
|
|
72
98
|
}
|
|
73
99
|
|
|
74
100
|
async cleanup() {
|
|
75
101
|
const promises = [...this.ignoredFiles].map(async (file) => this.deleteFile(file));
|
|
76
102
|
|
|
77
|
-
|
|
103
|
+
await Promise.allSettled(promises);
|
|
104
|
+
await this.removeEmptyDirectories(this.config.directory);
|
|
105
|
+
if (existsSync('data')) {
|
|
106
|
+
await this.removeEmptyDirectories('data');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
78
110
|
}
|
|
79
111
|
}
|
package/src/lib/object.test.ts
CHANGED
|
@@ -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
|
@@ -81,7 +81,7 @@ export type Config = Partial<ContentfulConfig> &
|
|
|
81
81
|
rootDir?: string;
|
|
82
82
|
directory: string;
|
|
83
83
|
verbose?: boolean;
|
|
84
|
-
plugins?: Array<PluginInfo | string>;
|
|
84
|
+
plugins?: Array<[string, KeyValueMap] | PluginInfo | string>;
|
|
85
85
|
resolvedPlugins?: Hooks[];
|
|
86
86
|
preset?: string;
|
|
87
87
|
richTextRenderer?: RichTextConfig;
|