@jungvonmatt/contentful-ssg 1.7.4 → 1.7.5-alpha.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/README.md +18 -2
- package/dist/__test__/mock.d.ts +9 -7
- package/dist/__test__/mock.js +18 -5
- package/dist/cli.js +47 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +99 -11
- package/dist/lib/contentful.d.ts +19 -1
- package/dist/lib/contentful.js +106 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.js +29 -0
- package/dist/tasks/fetch.js +7 -1
- package/dist/tasks/remove.d.ts +2 -0
- package/dist/tasks/remove.js +5 -0
- package/dist/tasks/write.d.ts +2 -0
- package/dist/tasks/write.js +20 -6
- package/dist/types.d.ts +14 -1
- package/package.json +10 -3
- package/src/__test__/mock.ts +32 -10
- package/src/cli.ts +59 -1
- package/src/index.test.ts +65 -1
- package/src/index.ts +150 -17
- package/src/lib/contentful.test.ts +163 -2
- package/src/lib/contentful.ts +157 -2
- package/src/server/index.test.ts +44 -0
- package/src/server/index.ts +71 -0
- package/src/tasks/fetch.test.ts +25 -1
- package/src/tasks/fetch.ts +20 -1
- package/src/tasks/remove.ts +12 -0
- package/src/tasks/write.ts +45 -12
- package/src/types.ts +17 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ npx cssg init --typescript
|
|
|
47
47
|
| spaceId | `string` | `undefined` | Contentful Space id |
|
|
48
48
|
| environmentId | `string` | `'master'` | Contentful Environment id |
|
|
49
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)
|
|
50
|
+
| plugins | `[string]`\|`[[string, options]]`\|`[{resolve:'string', options:{}}]` | `[]` | Add plugins to contentful-ssg. See [Plugins](#plugins) |
|
|
51
51
|
| directory | `string` | `'./content'` | Base directory for content files. |
|
|
52
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
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) |
|
|
@@ -71,6 +71,7 @@ plugins: ['my-plugin-package', './plugins/my-local-plugin]
|
|
|
71
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
72
|
|
|
73
73
|
For specifying no options, these are all equivalent:
|
|
74
|
+
|
|
74
75
|
```js
|
|
75
76
|
{
|
|
76
77
|
"plugins": ["my-plugin", ["my-plugin"], ["my-plugin", {}], {resolve: "my-plugin", options: {}}]
|
|
@@ -78,6 +79,7 @@ For specifying no options, these are all equivalent:
|
|
|
78
79
|
```
|
|
79
80
|
|
|
80
81
|
To specify an option, pass an object with the keys as the option names.
|
|
82
|
+
|
|
81
83
|
```js
|
|
82
84
|
{
|
|
83
85
|
"plugins": [
|
|
@@ -225,7 +227,6 @@ itself is waiting for the current entry to be transformed.
|
|
|
225
227
|
|
|
226
228
|
// Do something usefull with the transformed data
|
|
227
229
|
// which you can't do with context.entryMap.get('<contentful-id>')
|
|
228
|
-
|
|
229
230
|
} catch (error) {
|
|
230
231
|
// Entry isn't available, the transform method for the entry throws an error
|
|
231
232
|
// or we encountered a cyclic dependency
|
|
@@ -245,10 +246,25 @@ npx cssg fetch
|
|
|
245
246
|
```
|
|
246
247
|
|
|
247
248
|
To see all available command line options call
|
|
249
|
+
|
|
248
250
|
```bash
|
|
249
251
|
npx cssg help fetch
|
|
250
252
|
```
|
|
251
253
|
|
|
254
|
+
### watch
|
|
255
|
+
|
|
256
|
+
Same as `fetch` but also starts a webserver listening for changes and registers a contentful webhook
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
npx cssg watch
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
To see all available command line options call
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
npx cssg help watch
|
|
266
|
+
```
|
|
267
|
+
|
|
252
268
|
## Example configuration
|
|
253
269
|
|
|
254
270
|
### Grow
|
package/dist/__test__/mock.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { Config, RuntimeContext, TransformContext } from '../types.js';
|
|
1
|
+
import type { Entry, Config, RuntimeContext, TransformContext } from '../types.js';
|
|
2
2
|
export declare const readFixture: (file: any) => Promise<any>;
|
|
3
3
|
export declare const readFixtureSync: (file: any) => any;
|
|
4
4
|
export declare const getContent: () => Promise<{
|
|
5
|
-
entries:
|
|
6
|
-
assets:
|
|
7
|
-
contentTypes:
|
|
8
|
-
locales:
|
|
5
|
+
entries: Entry[];
|
|
6
|
+
assets: import("contentful").Asset[];
|
|
7
|
+
contentTypes: import("contentful").ContentType[];
|
|
8
|
+
locales: import("contentful").Locale[];
|
|
9
9
|
assetLink: {
|
|
10
10
|
sys: {
|
|
11
11
|
id: string;
|
|
@@ -20,8 +20,10 @@ export declare const getContent: () => Promise<{
|
|
|
20
20
|
linkType: string;
|
|
21
21
|
};
|
|
22
22
|
};
|
|
23
|
-
entry:
|
|
24
|
-
asset:
|
|
23
|
+
entry: Entry;
|
|
24
|
+
asset: import("contentful").Asset;
|
|
25
|
+
assetMap: Map<string, import("contentful").Asset>;
|
|
26
|
+
entryMap: Map<string, Entry>;
|
|
25
27
|
}>;
|
|
26
28
|
export declare const getConfig: (fixture?: Partial<Config>) => Config;
|
|
27
29
|
export declare const getRuntimeContext: (fixture?: Partial<RuntimeContext>) => RuntimeContext;
|
package/dist/__test__/mock.js
CHANGED
|
@@ -19,10 +19,10 @@ export const readFixtureSync = (file) => {
|
|
|
19
19
|
return cache.get(file);
|
|
20
20
|
};
|
|
21
21
|
export const getContent = async () => {
|
|
22
|
-
const assets = await readFixture('assets.json');
|
|
23
|
-
const entries = await readFixture('entries.json');
|
|
24
|
-
const locales = await readFixture('locales.json');
|
|
25
|
-
const contentTypes = await readFixture('content_types.json');
|
|
22
|
+
const assets = (await readFixture('assets.json'));
|
|
23
|
+
const entries = (await readFixture('entries.json'));
|
|
24
|
+
const locales = (await readFixture('locales.json'));
|
|
25
|
+
const contentTypes = (await readFixture('content_types.json'));
|
|
26
26
|
const [entry] = entries;
|
|
27
27
|
const [asset] = assets;
|
|
28
28
|
const assetLink = {
|
|
@@ -39,7 +39,20 @@ export const getContent = async () => {
|
|
|
39
39
|
linkType: LINK_TYPE_ENTRY,
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
|
-
|
|
42
|
+
const assetMap = new Map(assets.map((asset) => [asset.sys.id, asset]));
|
|
43
|
+
const entryMap = new Map(entries.map((entry) => [entry.sys.id, entry]));
|
|
44
|
+
return {
|
|
45
|
+
entries,
|
|
46
|
+
assets,
|
|
47
|
+
contentTypes,
|
|
48
|
+
locales,
|
|
49
|
+
assetLink,
|
|
50
|
+
entryLink,
|
|
51
|
+
entry,
|
|
52
|
+
asset,
|
|
53
|
+
assetMap,
|
|
54
|
+
entryMap,
|
|
55
|
+
};
|
|
43
56
|
};
|
|
44
57
|
export const getConfig = (fixture = {}) => ({
|
|
45
58
|
directory: 'test',
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
+
import ngrok from 'ngrok';
|
|
5
|
+
import getPort from 'get-port';
|
|
6
|
+
import exitHook from 'async-exit-hook';
|
|
4
7
|
import { existsSync } from 'fs';
|
|
5
8
|
import { readFile } from 'fs/promises';
|
|
6
9
|
import { outputFile } from 'fs-extra';
|
|
@@ -10,8 +13,10 @@ import dotenv from 'dotenv';
|
|
|
10
13
|
import dotenvExpand from 'dotenv-expand';
|
|
11
14
|
import { logError, confirm, askAll, askMissing } from './lib/ui.js';
|
|
12
15
|
import { omitKeys } from './lib/object.js';
|
|
16
|
+
import { getApp } from './server/index.js';
|
|
13
17
|
import { getConfig, getEnvironmentConfig } from './lib/config.js';
|
|
14
18
|
import { run } from './index.js';
|
|
19
|
+
import { addWatchWebhook, resetSync } from './lib/contentful.js';
|
|
15
20
|
const env = dotenv.config();
|
|
16
21
|
dotenvExpand(env);
|
|
17
22
|
const parseFetchArgs = (cmd) => ({
|
|
@@ -90,8 +95,50 @@ program
|
|
|
90
95
|
.option('-v, --verbose', 'Verbose output')
|
|
91
96
|
.option('--ignore-errors', 'No error return code when transform has errors')
|
|
92
97
|
.action(actionRunner(async (cmd) => {
|
|
98
|
+
await resetSync();
|
|
93
99
|
const config = await getConfig(parseFetchArgs(cmd || {}));
|
|
94
100
|
const verified = await askMissing(config);
|
|
95
101
|
return run(verified);
|
|
96
102
|
}));
|
|
103
|
+
program
|
|
104
|
+
.command('watch')
|
|
105
|
+
.description('Fetch content objects && watch for changes')
|
|
106
|
+
.option('-p, --preview', 'Fetch with preview mode')
|
|
107
|
+
.option('-v, --verbose', 'Verbose output')
|
|
108
|
+
.option('--url <url>', 'Url where the the server is reachable from the outside')
|
|
109
|
+
.option('--ignore-errors', 'No error return code when transform has errors')
|
|
110
|
+
.action(actionRunner(async (cmd) => {
|
|
111
|
+
await resetSync();
|
|
112
|
+
const config = await getConfig(parseFetchArgs(cmd || {}));
|
|
113
|
+
const verified = await askMissing(config);
|
|
114
|
+
let prev = await run({ ...verified, sync: true });
|
|
115
|
+
let port = await getPort({ port: 1314 });
|
|
116
|
+
if (cmd.url) {
|
|
117
|
+
const url = new URL(cmd.url);
|
|
118
|
+
port = url.port || url.protocol === 'https:' ? 443 : 80;
|
|
119
|
+
}
|
|
120
|
+
const app = getApp(async () => {
|
|
121
|
+
prev = await run({ ...verified, sync: true }, prev);
|
|
122
|
+
});
|
|
123
|
+
const server = app.listen(port);
|
|
124
|
+
const stopServer = async () => new Promise((resolve, reject) => {
|
|
125
|
+
server.close((err) => {
|
|
126
|
+
if (err) {
|
|
127
|
+
reject(err);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
resolve(true);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
const url = cmd.url || (await ngrok.connect(port));
|
|
135
|
+
console.log(`\n Listening for hooks on ${chalk.cyan(url)}\n`);
|
|
136
|
+
const webhook = await addWatchWebhook(verified, url);
|
|
137
|
+
exitHook(async (cb) => {
|
|
138
|
+
await webhook.delete();
|
|
139
|
+
await stopServer();
|
|
140
|
+
await resetSync();
|
|
141
|
+
cb();
|
|
142
|
+
});
|
|
143
|
+
}));
|
|
97
144
|
program.parse(process.argv);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import type { Config } from './types.js';
|
|
2
|
-
export declare const
|
|
1
|
+
import type { Config, RunResult, RuntimeContext } from './types.js';
|
|
2
|
+
export declare const cleanupPrevData: (ctx: RuntimeContext, prev: RunResult) => void;
|
|
3
|
+
export declare const run: (config: Config, prev?: RunResult) => Promise<RunResult>;
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import Listr from 'listr';
|
|
2
3
|
import { ReplaySubject } from 'rxjs';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { getContentId, getContentTypeId, isSyncRequest } from './lib/contentful.js';
|
|
5
|
+
import { ValidationError } from './lib/error.js';
|
|
6
|
+
import { collectParentValues, collectValues, waitFor } from './lib/utils.js';
|
|
6
7
|
import { fetch } from './tasks/fetch.js';
|
|
7
8
|
import { localize } from './tasks/localize.js';
|
|
9
|
+
import { remove } from './tasks/remove.js';
|
|
10
|
+
import { setup } from './tasks/setup.js';
|
|
8
11
|
import { transform } from './tasks/transform.js';
|
|
9
12
|
import { write } from './tasks/write.js';
|
|
10
|
-
import { collectParentValues, collectValues, waitFor } from './lib/utils.js';
|
|
11
|
-
import { ValidationError } from './lib/error.js';
|
|
12
13
|
class CustomListrRenderer {
|
|
13
14
|
_tasks;
|
|
14
15
|
constructor(tasks) {
|
|
@@ -38,7 +39,39 @@ class CustomListrRenderer {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
export const
|
|
42
|
+
export const cleanupPrevData = (ctx, prev) => {
|
|
43
|
+
const entryMap = prev.localized?.[ctx.defaultLocale]?.entryMap ?? new Map();
|
|
44
|
+
ctx.data.deletedEntries =
|
|
45
|
+
ctx?.data?.deletedEntries?.map((entry) => {
|
|
46
|
+
if (entryMap.has(entry.sys.id)) {
|
|
47
|
+
const prevEntry = entryMap.get(entry.sys.id);
|
|
48
|
+
return { ...prevEntry, sys: { ...prevEntry.sys, ...entry.sys } };
|
|
49
|
+
}
|
|
50
|
+
return entry;
|
|
51
|
+
}) ?? [];
|
|
52
|
+
ctx.data.locales.forEach((locale) => {
|
|
53
|
+
if (ctx?.data?.deletedEntries?.length) {
|
|
54
|
+
ctx.data.deletedEntries.forEach((entry) => {
|
|
55
|
+
if (prev.localized?.[locale.code]?.entryMap.has(entry.sys.id)) {
|
|
56
|
+
prev.localized?.[locale.code]?.entryMap.delete(entry.sys.id);
|
|
57
|
+
prev.localized[locale.code].entries = Array.from(prev.localized?.[locale.code]?.entryMap.values());
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (ctx?.data?.deletedAssets?.length) {
|
|
62
|
+
ctx.data.deletedAssets.forEach((asset) => {
|
|
63
|
+
if (prev.localized?.[locale.code]?.assetMap.has(asset.sys.id)) {
|
|
64
|
+
prev.localized?.[locale.code]?.assetMap.delete(asset.sys.id);
|
|
65
|
+
prev.localized[locale.code].assets = Array.from(prev.localized?.[locale.code]?.assetMap.values());
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
export const run = async (config, prev = {
|
|
72
|
+
observables: {},
|
|
73
|
+
localized: {},
|
|
74
|
+
}) => {
|
|
42
75
|
const tasks = new Listr([
|
|
43
76
|
{
|
|
44
77
|
title: 'Setup',
|
|
@@ -46,7 +79,10 @@ export const run = async (config) => {
|
|
|
46
79
|
},
|
|
47
80
|
{
|
|
48
81
|
title: 'Pulling data from contentful',
|
|
49
|
-
task: async (ctx) =>
|
|
82
|
+
task: async (ctx) => {
|
|
83
|
+
await fetch(ctx, config);
|
|
84
|
+
cleanupPrevData(ctx, prev);
|
|
85
|
+
},
|
|
50
86
|
},
|
|
51
87
|
{
|
|
52
88
|
title: 'Localize data',
|
|
@@ -57,6 +93,41 @@ export const run = async (config) => {
|
|
|
57
93
|
skip: (ctx) => !ctx.hooks.has('before'),
|
|
58
94
|
task: async (ctx) => ctx.hooks.before(),
|
|
59
95
|
},
|
|
96
|
+
{
|
|
97
|
+
title: 'Remove deleted files',
|
|
98
|
+
skip: (ctx) => (ctx.data.deletedEntries ?? []).length === 0,
|
|
99
|
+
task: async (ctx) => {
|
|
100
|
+
const { locales = [], deletedEntries = [] } = ctx.data;
|
|
101
|
+
const tasks = locales.map((locale) => ({
|
|
102
|
+
title: `${locale.code}`,
|
|
103
|
+
task: async () => {
|
|
104
|
+
const data = ctx.localized.get(locale.code);
|
|
105
|
+
if (!prev?.observables?.[locale.code]) {
|
|
106
|
+
prev.observables[locale.code] = new ReplaySubject();
|
|
107
|
+
}
|
|
108
|
+
const subject = prev.observables[locale.code];
|
|
109
|
+
const observable = subject.asObservable();
|
|
110
|
+
const promises = deletedEntries.map(async (entry) => {
|
|
111
|
+
const id = getContentId(entry);
|
|
112
|
+
const contentTypeId = getContentTypeId(entry);
|
|
113
|
+
const utils = {};
|
|
114
|
+
const transformContext = {
|
|
115
|
+
...data,
|
|
116
|
+
id,
|
|
117
|
+
contentTypeId,
|
|
118
|
+
entry,
|
|
119
|
+
locale,
|
|
120
|
+
observable,
|
|
121
|
+
utils,
|
|
122
|
+
};
|
|
123
|
+
return remove(transformContext, ctx, config);
|
|
124
|
+
});
|
|
125
|
+
return Promise.all(promises);
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
return new Listr(tasks, { concurrent: true });
|
|
129
|
+
},
|
|
130
|
+
},
|
|
60
131
|
{
|
|
61
132
|
title: 'Writing files',
|
|
62
133
|
task: async (ctx) => {
|
|
@@ -64,10 +135,24 @@ export const run = async (config) => {
|
|
|
64
135
|
const tasks = locales.map((locale) => ({
|
|
65
136
|
title: `${locale.code}`,
|
|
66
137
|
task: async () => {
|
|
67
|
-
const subject = new ReplaySubject();
|
|
68
|
-
const observable = subject.asObservable();
|
|
69
138
|
const data = ctx.localized.get(locale.code);
|
|
70
139
|
const { entries = [] } = data || {};
|
|
140
|
+
if (!prev?.observables?.[locale.code]) {
|
|
141
|
+
prev.observables[locale.code] = new ReplaySubject();
|
|
142
|
+
}
|
|
143
|
+
const subject = prev.observables[locale.code];
|
|
144
|
+
const observable = subject.asObservable();
|
|
145
|
+
data.assetMap = new Map([
|
|
146
|
+
...(prev?.localized[locale.code]?.assetMap ?? new Map()),
|
|
147
|
+
...data.assetMap,
|
|
148
|
+
]);
|
|
149
|
+
data.entryMap = new Map([
|
|
150
|
+
...(prev?.localized[locale.code]?.entryMap ?? new Map()),
|
|
151
|
+
...data.entryMap,
|
|
152
|
+
]);
|
|
153
|
+
data.entries = Array.from(data.entryMap.values());
|
|
154
|
+
data.assets = Array.from(data.assetMap.values());
|
|
155
|
+
prev.localized[locale.code] = data;
|
|
71
156
|
const promises = entries.map(async (entry) => {
|
|
72
157
|
const id = getContentId(entry);
|
|
73
158
|
const contentTypeId = getContentTypeId(entry);
|
|
@@ -107,6 +192,7 @@ export const run = async (config) => {
|
|
|
107
192
|
else {
|
|
108
193
|
ctx.stats.addError(transformContext, error);
|
|
109
194
|
}
|
|
195
|
+
await remove(transformContext, ctx, config);
|
|
110
196
|
}
|
|
111
197
|
});
|
|
112
198
|
return Promise.all(promises);
|
|
@@ -122,13 +208,15 @@ export const run = async (config) => {
|
|
|
122
208
|
},
|
|
123
209
|
{
|
|
124
210
|
title: 'Cleanup',
|
|
211
|
+
skip: () => isSyncRequest(),
|
|
125
212
|
task: async (ctx) => ctx.fileManager.cleanup(),
|
|
126
213
|
},
|
|
127
214
|
], { renderer: CustomListrRenderer });
|
|
128
215
|
const ctx = await tasks.run();
|
|
129
216
|
await ctx.stats.print();
|
|
130
|
-
console.log('\n
|
|
131
|
-
if (ctx.stats.errors?.length && !config.ignoreErrors) {
|
|
217
|
+
console.log('\n -------------------------------------------');
|
|
218
|
+
if (!ctx.config.sync && ctx.stats.errors?.length && !config.ignoreErrors) {
|
|
132
219
|
process.exit(1);
|
|
133
220
|
}
|
|
221
|
+
return prev;
|
|
134
222
|
};
|
package/dist/lib/contentful.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Space } from 'contentful-management/types';
|
|
1
|
+
import type { Space, CreateWebhooksProps } from 'contentful-management/types';
|
|
2
2
|
import type { ContentfulConfig, FieldSettings, Node, Entry, ContentType } from '../types.js';
|
|
3
3
|
import contentful from 'contentful';
|
|
4
4
|
export declare const FIELD_TYPE_SYMBOL = "Symbol";
|
|
@@ -15,6 +15,7 @@ export declare const FIELD_TYPE_OBJECT = "Object";
|
|
|
15
15
|
export declare const LINK_TYPE_ASSET = "Asset";
|
|
16
16
|
export declare const LINK_TYPE_ENTRY = "Entry";
|
|
17
17
|
export declare const MAX_ALLOWED_LIMIT = 1000;
|
|
18
|
+
export declare const SYNC_TOKEN_FILENAME = ".contentful_sync.lock";
|
|
18
19
|
export declare const getContentTypeId: <T extends contentful.Asset | Entry | contentful.Entry<unknown>>(node: T) => string;
|
|
19
20
|
export declare const getEnvironmentId: <T extends Node>(node: T) => string;
|
|
20
21
|
export declare const getContentId: <T extends contentful.ContentType | contentful.Asset | Entry | contentful.Entry<unknown>>(node: T) => string;
|
|
@@ -24,12 +25,29 @@ export declare const getEnvironments: (options: ContentfulConfig) => Promise<imp
|
|
|
24
25
|
export declare const getEnvironment: (options: ContentfulConfig) => Promise<import("contentful-management/types").Environment>;
|
|
25
26
|
export declare const getApiKey: (options: ContentfulConfig) => Promise<string>;
|
|
26
27
|
export declare const getPreviewApiKey: (options: ContentfulConfig) => Promise<string>;
|
|
28
|
+
export declare const getWebhooks: (options: ContentfulConfig) => Promise<import("contentful-management/types").WebHooks[]>;
|
|
29
|
+
export declare const addWebhook: (options: ContentfulConfig, id: string, data: CreateWebhooksProps) => Promise<import("contentful-management/types").WebHooks>;
|
|
30
|
+
export declare const deleteWebhook: (options: ContentfulConfig, id: string) => Promise<void>;
|
|
31
|
+
export declare const addWatchWebhook: (options: ContentfulConfig, url: string) => Promise<import("contentful-management/types").WebHooks>;
|
|
32
|
+
export declare const isSyncRequest: () => boolean;
|
|
33
|
+
export declare const resetSync: () => Promise<true | void>;
|
|
27
34
|
export declare const getContent: (options: ContentfulConfig) => Promise<{
|
|
35
|
+
entries: contentful.Entry<any>[];
|
|
36
|
+
assets: contentful.Asset[];
|
|
37
|
+
deletedEntries: contentful.Entry<any>[];
|
|
38
|
+
deletedAssets: contentful.Asset[];
|
|
39
|
+
contentTypes: contentful.ContentType[];
|
|
40
|
+
locales: contentful.Locale[];
|
|
41
|
+
} | {
|
|
28
42
|
entries: Entry[];
|
|
29
43
|
assets: contentful.Asset[];
|
|
30
44
|
contentTypes: contentful.ContentType[];
|
|
31
45
|
locales: contentful.Locale[];
|
|
46
|
+
deletedEntries?: undefined;
|
|
47
|
+
deletedAssets?: undefined;
|
|
32
48
|
}>;
|
|
49
|
+
export declare const getEntriesLinkedToEntry: (options: ContentfulConfig, id: string) => Promise<Entry[]>;
|
|
50
|
+
export declare const getEntriesLinkedToAsset: (options: ContentfulConfig, id: string) => Promise<Entry[]>;
|
|
33
51
|
export declare const isContentfulObject: (obj: any) => boolean;
|
|
34
52
|
export declare const isLink: (obj: any) => boolean;
|
|
35
53
|
export declare const isAssetLink: (obj: any) => boolean;
|
package/dist/lib/contentful.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { hostname } from 'os';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import { readFile, unlink, writeFile } from 'fs/promises';
|
|
1
5
|
import contentful from 'contentful';
|
|
2
6
|
import contentfulManagement from 'contentful-management';
|
|
3
7
|
let client;
|
|
@@ -16,6 +20,7 @@ export const FIELD_TYPE_OBJECT = 'Object';
|
|
|
16
20
|
export const LINK_TYPE_ASSET = 'Asset';
|
|
17
21
|
export const LINK_TYPE_ENTRY = 'Entry';
|
|
18
22
|
export const MAX_ALLOWED_LIMIT = 1000;
|
|
23
|
+
export const SYNC_TOKEN_FILENAME = '.contentful_sync.lock';
|
|
19
24
|
export const getContentTypeId = (node) => node?.sys?.contentType?.sys?.id ?? 'unknown';
|
|
20
25
|
export const getEnvironmentId = (node) => node?.sys?.environment?.sys?.id ?? 'unknown';
|
|
21
26
|
export const getContentId = (node) => node?.sys?.id ?? 'unknown';
|
|
@@ -89,6 +94,68 @@ export const getPreviewApiKey = async (options) => {
|
|
|
89
94
|
const { accessToken: previewAccessToken } = previewApiKey;
|
|
90
95
|
return previewAccessToken;
|
|
91
96
|
};
|
|
97
|
+
export const getWebhooks = async (options) => {
|
|
98
|
+
const space = await getSpace(options);
|
|
99
|
+
const { items: webhooks = [] } = await space.getWebhooks();
|
|
100
|
+
return webhooks;
|
|
101
|
+
};
|
|
102
|
+
export const addWebhook = async (options, id, data) => {
|
|
103
|
+
const space = await getSpace(options);
|
|
104
|
+
return space.createWebhookWithId(id, data);
|
|
105
|
+
};
|
|
106
|
+
export const deleteWebhook = async (options, id) => {
|
|
107
|
+
const space = await getSpace(options);
|
|
108
|
+
const webhook = await space.getWebhook(id);
|
|
109
|
+
return webhook.delete();
|
|
110
|
+
};
|
|
111
|
+
export const addWatchWebhook = async (options, url) => {
|
|
112
|
+
let topics = [
|
|
113
|
+
'ContentType.publish',
|
|
114
|
+
'ContentType.unpublish',
|
|
115
|
+
'ContentType.delete',
|
|
116
|
+
'Entry.archive',
|
|
117
|
+
'Entry.unarchive',
|
|
118
|
+
'Entry.publish',
|
|
119
|
+
'Entry.unpublish',
|
|
120
|
+
'Entry.delete',
|
|
121
|
+
'Asset.archive',
|
|
122
|
+
'Asset.unarchive',
|
|
123
|
+
'Asset.publish',
|
|
124
|
+
'Asset.unpublish',
|
|
125
|
+
'Asset.delete',
|
|
126
|
+
];
|
|
127
|
+
if (options.preview) {
|
|
128
|
+
topics = [
|
|
129
|
+
...topics,
|
|
130
|
+
'ContentType.save',
|
|
131
|
+
'Entry.save',
|
|
132
|
+
'Entry.auto_save',
|
|
133
|
+
'Asset.save',
|
|
134
|
+
'Asset.auto_save',
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
const uuid = uuidv4();
|
|
138
|
+
return addWebhook(options, uuid, {
|
|
139
|
+
name: `contentful-ssg (${hostname()})`,
|
|
140
|
+
url,
|
|
141
|
+
httpBasicUsername: null,
|
|
142
|
+
topics,
|
|
143
|
+
filters: [
|
|
144
|
+
{
|
|
145
|
+
equals: [
|
|
146
|
+
{
|
|
147
|
+
doc: 'sys.environment.sys.id',
|
|
148
|
+
},
|
|
149
|
+
options.environmentId,
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
transformation: {
|
|
154
|
+
includeContentLength: true,
|
|
155
|
+
},
|
|
156
|
+
headers: [],
|
|
157
|
+
});
|
|
158
|
+
};
|
|
92
159
|
const pagedGet = async (apiClient, { method, skip = 0, aggregatedResponse = null, query = null }) => {
|
|
93
160
|
const fullQuery = {
|
|
94
161
|
skip,
|
|
@@ -115,6 +182,25 @@ const pagedGet = async (apiClient, { method, skip = 0, aggregatedResponse = null
|
|
|
115
182
|
}
|
|
116
183
|
return aggregatedResponse;
|
|
117
184
|
};
|
|
185
|
+
const sync = async (apiClient) => {
|
|
186
|
+
const options = { initial: true };
|
|
187
|
+
if (existsSync(SYNC_TOKEN_FILENAME)) {
|
|
188
|
+
options.nextSyncToken = await readFile(SYNC_TOKEN_FILENAME, 'utf8');
|
|
189
|
+
delete options.initial;
|
|
190
|
+
}
|
|
191
|
+
const response = (await apiClient.sync(options));
|
|
192
|
+
if (response.nextSyncToken) {
|
|
193
|
+
await writeFile(SYNC_TOKEN_FILENAME, response.nextSyncToken);
|
|
194
|
+
}
|
|
195
|
+
return response;
|
|
196
|
+
};
|
|
197
|
+
export const isSyncRequest = () => existsSync(SYNC_TOKEN_FILENAME);
|
|
198
|
+
export const resetSync = async () => {
|
|
199
|
+
if (isSyncRequest()) {
|
|
200
|
+
return unlink(SYNC_TOKEN_FILENAME);
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
};
|
|
118
204
|
export const getContent = async (options) => {
|
|
119
205
|
const apiClient = getClient(options);
|
|
120
206
|
const { items: locales } = await pagedGet(apiClient, {
|
|
@@ -123,6 +209,10 @@ export const getContent = async (options) => {
|
|
|
123
209
|
const { items: contentTypes } = await pagedGet(apiClient, {
|
|
124
210
|
method: 'getContentTypes',
|
|
125
211
|
});
|
|
212
|
+
if (options.sync) {
|
|
213
|
+
const { entries, assets, deletedEntries, deletedAssets } = await sync(apiClient);
|
|
214
|
+
return { entries, assets, deletedEntries, deletedAssets, contentTypes, locales };
|
|
215
|
+
}
|
|
126
216
|
const { items: entries } = await pagedGet(apiClient, {
|
|
127
217
|
method: 'getEntries',
|
|
128
218
|
});
|
|
@@ -131,6 +221,22 @@ export const getContent = async (options) => {
|
|
|
131
221
|
});
|
|
132
222
|
return { entries, assets, contentTypes, locales };
|
|
133
223
|
};
|
|
224
|
+
export const getEntriesLinkedToEntry = async (options, id) => {
|
|
225
|
+
const apiClient = getClient(options);
|
|
226
|
+
const { items: entries } = await pagedGet(apiClient, {
|
|
227
|
+
method: 'getEntries',
|
|
228
|
+
query: { links_to_entry: id },
|
|
229
|
+
});
|
|
230
|
+
return entries;
|
|
231
|
+
};
|
|
232
|
+
export const getEntriesLinkedToAsset = async (options, id) => {
|
|
233
|
+
const apiClient = getClient(options);
|
|
234
|
+
const { items: entries } = await pagedGet(apiClient, {
|
|
235
|
+
method: 'getEntries',
|
|
236
|
+
query: { links_to_asset: id },
|
|
237
|
+
});
|
|
238
|
+
return entries;
|
|
239
|
+
};
|
|
134
240
|
export const isContentfulObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]' && Object.keys(obj).includes('sys');
|
|
135
241
|
export const isLink = (obj) => isContentfulObject(obj) && obj.sys.type === FIELD_TYPE_LINK;
|
|
136
242
|
export const isAssetLink = (obj) => isLink(obj) && obj.sys.linkType === LINK_TYPE_ASSET;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
declare module 'http' {
|
|
2
|
+
interface IncomingHttpHeaders {
|
|
3
|
+
'x-contentful-topic': 'ContentManagement.ContentType.create' | 'ContentManagement.ContentType.save' | 'ContentManagement.ContentType.publish' | 'ContentManagement.ContentType.unpublish' | 'ContentManagement.ContentType.delete' | 'ContentManagement.Entry.create' | 'ContentManagement.Entry.save' | 'ContentManagement.Entry.auto_save' | 'ContentManagement.Entry.archive' | 'ContentManagement.Entry.unarchive' | 'ContentManagement.Entry.publish' | 'ContentManagement.Entry.unpublish' | 'ContentManagement.Entry.delete' | 'ContentManagement.Asset.create' | 'ContentManagement.Asset.save' | 'ContentManagement.Asset.auto_save' | 'ContentManagement.Asset.archive' | 'ContentManagement.Asset.unarchive' | 'ContentManagement.Asset.publish' | 'ContentManagement.Asset.unpublish' | 'ContentManagement.Asset.delete';
|
|
4
|
+
'X-Contentful-Webhook-Name': string;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export declare const getApp: (callback: () => Promise<void>) => import("express-serve-static-core").Express;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
const app = express();
|
|
3
|
+
app.disable('x-powered-by');
|
|
4
|
+
app.use(express.urlencoded({ extended: true }));
|
|
5
|
+
app.use(express.json({
|
|
6
|
+
type: [
|
|
7
|
+
'application/vnd.contentful.management.v1+json',
|
|
8
|
+
'application/vnd.contentful.management.v1+json; charset=utf-8',
|
|
9
|
+
'application/json',
|
|
10
|
+
'application/json; charset=utf-8',
|
|
11
|
+
'application/x-www-form-urlencoded',
|
|
12
|
+
'application/x-www-form-urlencoded; charset=utf-8',
|
|
13
|
+
],
|
|
14
|
+
}));
|
|
15
|
+
export const getApp = (callback) => {
|
|
16
|
+
app.get('/status', (_req, res) => res.status(200).send('ok'));
|
|
17
|
+
app.get('/', async (_req, res) => {
|
|
18
|
+
await callback();
|
|
19
|
+
return res.status(200).send('ok');
|
|
20
|
+
});
|
|
21
|
+
app.post('/', async (req, res) => {
|
|
22
|
+
if (!req.body.sys) {
|
|
23
|
+
return res.status(401).send('error');
|
|
24
|
+
}
|
|
25
|
+
await callback();
|
|
26
|
+
return res.status(200).send('ok');
|
|
27
|
+
});
|
|
28
|
+
return app;
|
|
29
|
+
};
|
package/dist/tasks/fetch.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { getContent, getFieldSettings } from '../lib/contentful.js';
|
|
1
|
+
import { getContent, getFieldSettings, getEntriesLinkedToEntry, getEntriesLinkedToAsset, } from '../lib/contentful.js';
|
|
2
2
|
export const fetch = async (context, config) => {
|
|
3
3
|
const content = await getContent(config);
|
|
4
4
|
const { locales, contentTypes } = content;
|
|
5
|
+
const additionalEntriesPromise = [
|
|
6
|
+
...(content?.deletedEntries?.map(async (entry) => getEntriesLinkedToEntry(config, entry.sys.id)) ?? []),
|
|
7
|
+
...(content?.deletedAssets?.map(async (asset) => getEntriesLinkedToAsset(config, asset.sys.id)) ?? []),
|
|
8
|
+
];
|
|
9
|
+
const additionalEntries = (await Promise.all(additionalEntriesPromise)).flat();
|
|
10
|
+
content.entries = [...(content?.entries ?? []), ...additionalEntries];
|
|
5
11
|
const fieldSettings = getFieldSettings(contentTypes);
|
|
6
12
|
const { code: defaultLocale } = locales.find((locale) => locale.default);
|
|
7
13
|
context.defaultLocale = defaultLocale;
|
package/dist/tasks/write.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import type { TransformContext, RuntimeContext, Config } from '../types.js';
|
|
2
|
+
export declare const getFormat: (transformContext: TransformContext, runtimeContext: RuntimeContext, config: Config) => Promise<string>;
|
|
3
|
+
export declare const getFilepath: (transformContext: TransformContext, runtimeContext: RuntimeContext, config: Config) => Promise<string>;
|
|
2
4
|
export declare const write: (transformContext: TransformContext, runtimeContext: RuntimeContext, config: Config) => Promise<void>;
|