@druxt-contrib/node-preview 0.1.0-dev.20261030024907 → 0.1.0-dev.20261109195239
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/CHANGELOG.md +7 -1
- package/README.md +101 -226
- package/dist/index.esm.js +281 -6
- package/dist/index.ssr.js +281 -6
- package/package.json +4 -2
- package/templates/plugin.js +177 -20
package/dist/index.esm.js
CHANGED
|
@@ -1,13 +1,263 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
|
+
import { DruxtClient } from 'druxt';
|
|
3
|
+
|
|
4
|
+
const PREFIX = "[druxt-node-preview] ";
|
|
5
|
+
const TIMEOUT = 5e3;
|
|
6
|
+
const HEARTBEAT = 6 * 60 * 60 * 1e3;
|
|
7
|
+
const warn$1 = (message) => console.warn(PREFIX + message);
|
|
8
|
+
const info = (message) => console.info(PREFIX + message);
|
|
9
|
+
const fromEnv = (env, name) => (env[name] || "").trim() || void 0;
|
|
10
|
+
const toBaseUrl = (value) => {
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(value);
|
|
13
|
+
return typeof value === "string" && ["http:", "https:"].includes(url.protocol) && !url.search && !url.hash ? (url.origin + url.pathname).replace(/\/+$/, "") : null;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const resolveBackend = (value, env) => {
|
|
19
|
+
const options = registerOptions(value);
|
|
20
|
+
const raw = options && rawBackend(options, env);
|
|
21
|
+
return raw ? toBaseUrl(raw) : null;
|
|
22
|
+
};
|
|
23
|
+
const registerOptions = (value) => {
|
|
24
|
+
if (value === true) return {};
|
|
25
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
26
|
+
};
|
|
27
|
+
const rawBackend = (options, env) => fromEnv(env, "DRUXT_NODE_PREVIEW_REGISTER_BACKEND") || options.backend;
|
|
28
|
+
const resolveRegister = (value, env) => {
|
|
29
|
+
if (value === void 0 || value === null || value === false) return null;
|
|
30
|
+
const options = registerOptions(value);
|
|
31
|
+
if (!options) {
|
|
32
|
+
warn$1("Ignored the register option. Use true or an object.");
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (options.secret !== void 0) {
|
|
36
|
+
warn$1(
|
|
37
|
+
"Ignored druxt.nodePreview.register.secret. Never put the secret in nuxt.config.js."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const url = toBaseUrl(fromEnv(env, "DRUXT_NODE_PREVIEW_URL") || options.url);
|
|
41
|
+
const consumer = fromEnv(env, "DRUXT_NODE_PREVIEW_CLIENT_ID") || options.consumer;
|
|
42
|
+
const setBackend = rawBackend(options, env);
|
|
43
|
+
const backend = setBackend ? toBaseUrl(setBackend) : null;
|
|
44
|
+
const secretName = setBackend ? "DRUXT_NODE_PREVIEW_REGISTER_SECRET" : "DRUXT_NODE_PREVIEW_CLIENT_SECRET";
|
|
45
|
+
const secret = fromEnv(env, secretName);
|
|
46
|
+
const envToken = fromEnv(env, "DRUXT_NODE_PREVIEW_REGISTER_TOKEN");
|
|
47
|
+
const rawToken = envToken !== void 0 ? envToken : options.token;
|
|
48
|
+
const token = rawToken === void 0 ? Boolean(backend) : (/* @__PURE__ */ new Map([
|
|
49
|
+
["true", true],
|
|
50
|
+
["1", true],
|
|
51
|
+
["false", false],
|
|
52
|
+
["0", false]
|
|
53
|
+
])).get(String(rawToken));
|
|
54
|
+
const problems = [
|
|
55
|
+
!url && "DRUXT_NODE_PREVIEW_URL or register.url must be the frontend's full http(s) URL, with no query or fragment.",
|
|
56
|
+
(typeof consumer !== "string" || !/^[\x21-\x7e]+$/.test(consumer)) && "DRUXT_NODE_PREVIEW_CLIENT_ID or register.consumer must be the consumer's client ID.",
|
|
57
|
+
setBackend && !backend && "DRUXT_NODE_PREVIEW_REGISTER_BACKEND or register.backend must be the Drupal site's full http(s) URL.",
|
|
58
|
+
token === void 0 && "DRUXT_NODE_PREVIEW_REGISTER_TOKEN or register.token must be true or false.",
|
|
59
|
+
!secret && `${secretName} must be the consumer's secret, in the environment that runs Nuxt.`
|
|
60
|
+
].filter(Boolean);
|
|
61
|
+
if (problems.length) {
|
|
62
|
+
warn$1(`Ignored the register option. ${problems.join(" ")}`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const label = typeof options.label === "string" && options.label.trim() || new URL(url).host;
|
|
66
|
+
return {
|
|
67
|
+
url,
|
|
68
|
+
label,
|
|
69
|
+
consumer,
|
|
70
|
+
secret,
|
|
71
|
+
token,
|
|
72
|
+
...backend ? { backend } : {}
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
const targetUrl = (url, path, layouts, token = false, backend = false) => url + path + "?vm=[view_mode]" + (layouts.length ? "&layout=[layout]" : "") + (backend ? "#[jsonapi_node_preview]" : "#[jsonapi_node_preview_path]") + (token ? ";token=[preview_token]" : "");
|
|
76
|
+
const CREDENTIALS = "Check the secret, and that the consumer is confidential, allows the Client credentials grant, and has a Client credentials user.";
|
|
77
|
+
const TOKEN_HINTS = {
|
|
78
|
+
400: CREDENTIALS,
|
|
79
|
+
401: CREDENTIALS,
|
|
80
|
+
404: "Install and set up the Simple OAuth module on Drupal."
|
|
81
|
+
};
|
|
82
|
+
const TARGET_HINTS = {
|
|
83
|
+
401: `The consumer's Client credentials user must own the consumer, and the token's scope must give the "register consumer node preview targets" permission.`,
|
|
84
|
+
403: `The consumer's Client credentials user must own the consumer, and the token's scope must give the "register consumer node preview targets" permission.`,
|
|
85
|
+
404: "Check that Consumer Node Preview is installed, and that a consumer has this client ID.",
|
|
86
|
+
405: "Update Consumer Node Preview. This module needs a version with the PATCH method.",
|
|
87
|
+
422: "Check the register url and the path option."
|
|
88
|
+
};
|
|
89
|
+
const reason = (data) => {
|
|
90
|
+
const text = [data == null ? void 0 : data.error_description, data == null ? void 0 : data.message, data == null ? void 0 : data.error].find(
|
|
91
|
+
(value) => typeof value === "string" && value.trim()
|
|
92
|
+
);
|
|
93
|
+
return text ? `: ${text.trim().slice(0, 300)}` : "";
|
|
94
|
+
};
|
|
95
|
+
const forBackend = (defaults, options) => {
|
|
96
|
+
const adapter = typeof defaults.adapter === "function" ? defaults.adapter : "http";
|
|
97
|
+
for (const name of Object.keys(defaults)) delete defaults[name];
|
|
98
|
+
Object.assign(defaults, {
|
|
99
|
+
...options,
|
|
100
|
+
adapter,
|
|
101
|
+
headers: {},
|
|
102
|
+
transformRequest: [
|
|
103
|
+
(data) => data === void 0 || typeof data === "string" ? data : JSON.stringify(data)
|
|
104
|
+
],
|
|
105
|
+
transformResponse: [
|
|
106
|
+
(data) => {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(data);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
const send = async (client, config, hints) => {
|
|
117
|
+
const { status, data, headers } = await client.axios.request({
|
|
118
|
+
...config,
|
|
119
|
+
maxRedirects: 0,
|
|
120
|
+
validateStatus: () => true
|
|
121
|
+
});
|
|
122
|
+
if (status === 200) return data;
|
|
123
|
+
if (status >= 300 && status < 400) {
|
|
124
|
+
const location = (headers == null ? void 0 : headers.location) ? ` to ${headers.location}` : "";
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Drupal redirected ${config.url}${location} (HTTP ${status}). Set druxt.baseUrl to the URL Drupal redirects to.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
throw new Error(
|
|
130
|
+
`${config.url} answered HTTP ${status}${reason(data)}. ${hints[status] || ""}`.trim()
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
const registerTarget = async ({
|
|
134
|
+
baseUrl,
|
|
135
|
+
backend,
|
|
136
|
+
token: useToken,
|
|
137
|
+
druxt = {},
|
|
138
|
+
url,
|
|
139
|
+
label,
|
|
140
|
+
consumer,
|
|
141
|
+
secret,
|
|
142
|
+
path,
|
|
143
|
+
layouts
|
|
144
|
+
}) => {
|
|
145
|
+
let token;
|
|
146
|
+
try {
|
|
147
|
+
if (!backend && !baseUrl) throw new Error("druxt.baseUrl is not set.");
|
|
148
|
+
const base = (backend || baseUrl).replace(/\/+$/, "");
|
|
149
|
+
const { protocol, hostname } = new URL(base);
|
|
150
|
+
if (protocol === "http:" && !/^(localhost|127\.\d+\.\d+\.\d+|\[::1\])$|\.localhost$/.test(hostname)) {
|
|
151
|
+
warn$1(
|
|
152
|
+
`${backend ? "The register backend" : "druxt.baseUrl"} uses plain HTTP, so the secret and the token cross the network unencrypted. Use https.`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const { timeout = TIMEOUT, proxy } = druxt.axios || {};
|
|
156
|
+
const client = new DruxtClient(base, {
|
|
157
|
+
...druxt,
|
|
158
|
+
axios: backend ? { timeout } : { timeout: TIMEOUT, ...druxt.axios }
|
|
159
|
+
});
|
|
160
|
+
if (backend) {
|
|
161
|
+
forBackend(client.axios.defaults, {
|
|
162
|
+
timeout,
|
|
163
|
+
...proxy === void 0 ? {} : { proxy }
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const answer = await send(
|
|
167
|
+
client,
|
|
168
|
+
{
|
|
169
|
+
method: "POST",
|
|
170
|
+
url: `${base}/oauth/token`,
|
|
171
|
+
headers: {
|
|
172
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
173
|
+
Accept: "application/json"
|
|
174
|
+
},
|
|
175
|
+
data: new URLSearchParams({
|
|
176
|
+
grant_type: "client_credentials",
|
|
177
|
+
client_id: consumer,
|
|
178
|
+
client_secret: secret
|
|
179
|
+
}).toString()
|
|
180
|
+
},
|
|
181
|
+
TOKEN_HINTS
|
|
182
|
+
);
|
|
183
|
+
token = answer == null ? void 0 : answer.access_token;
|
|
184
|
+
if (!token) throw new Error(`${base}/oauth/token gave no access token.`);
|
|
185
|
+
await send(
|
|
186
|
+
client,
|
|
187
|
+
{
|
|
188
|
+
method: "PATCH",
|
|
189
|
+
url: `${base}/api/consumer-node-preview/targets`,
|
|
190
|
+
headers: {
|
|
191
|
+
Accept: "application/json",
|
|
192
|
+
Authorization: `Bearer ${token}`,
|
|
193
|
+
"Content-Type": "application/json",
|
|
194
|
+
"X-Consumer-ID": consumer
|
|
195
|
+
},
|
|
196
|
+
// Without layouts, Drupal keeps the consumer's own.
|
|
197
|
+
data: {
|
|
198
|
+
target: {
|
|
199
|
+
label,
|
|
200
|
+
url: targetUrl(
|
|
201
|
+
url,
|
|
202
|
+
path,
|
|
203
|
+
layouts,
|
|
204
|
+
Boolean(useToken),
|
|
205
|
+
Boolean(backend)
|
|
206
|
+
)
|
|
207
|
+
},
|
|
208
|
+
...layouts.length ? { layouts: [...new Set(layouts)] } : {}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
TARGET_HINTS
|
|
212
|
+
);
|
|
213
|
+
info(`Registered the preview target "${label}" on "${consumer}".`);
|
|
214
|
+
return "registered";
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const message = [secret, token].filter(Boolean).reduce(
|
|
217
|
+
(text, value) => text.split(value).join("[hidden]"),
|
|
218
|
+
error.message
|
|
219
|
+
);
|
|
220
|
+
warn$1(`Could not register the preview target: ${message}`);
|
|
221
|
+
return "failed";
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const registerOnce = (run) => {
|
|
225
|
+
var _a, _b;
|
|
226
|
+
const key = /* @__PURE__ */ Symbol.for("druxt-node-preview.register");
|
|
227
|
+
if (!process[key]) {
|
|
228
|
+
process[key] = run();
|
|
229
|
+
(_b = (_a = setInterval(run, HEARTBEAT)).unref) == null ? void 0 : _b.call(_a);
|
|
230
|
+
}
|
|
231
|
+
return process[key];
|
|
232
|
+
};
|
|
2
233
|
|
|
3
234
|
const toOrigin = (url) => {
|
|
4
235
|
try {
|
|
5
|
-
const { origin, protocol } = new URL(url);
|
|
6
|
-
|
|
236
|
+
const { origin, protocol, hostname } = new URL(url);
|
|
237
|
+
const plain = /^([a-z0-9_-]+\.)*[a-z0-9_-]+\.?$|^\[[0-9a-f:.]+\]$/;
|
|
238
|
+
return ["http:", "https:"].includes(protocol) && plain.test(hostname) ? origin : null;
|
|
7
239
|
} catch (e) {
|
|
8
240
|
return null;
|
|
9
241
|
}
|
|
10
242
|
};
|
|
243
|
+
const forLog = (url) => {
|
|
244
|
+
try {
|
|
245
|
+
const { protocol, host } = new URL(url);
|
|
246
|
+
return protocol + "//" + host;
|
|
247
|
+
} catch (e) {
|
|
248
|
+
return String(url).replace(/^[^/]*@/, "");
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const originProblem = (url) => {
|
|
252
|
+
try {
|
|
253
|
+
const { hostname, protocol } = new URL(url);
|
|
254
|
+
if (["http:", "https:"].includes(protocol)) {
|
|
255
|
+
return `Its host, "${hostname}", may only have letters, digits, dots, hyphens and _, or be an IPv6 address in brackets.`;
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
}
|
|
259
|
+
return 'Use a full http(s) URL, such as "https://cms.example.com".';
|
|
260
|
+
};
|
|
11
261
|
const warn = (message) => console.warn("[druxt-node-preview] " + message);
|
|
12
262
|
const toInclude = (value) => {
|
|
13
263
|
const paths = (list, where) => {
|
|
@@ -80,6 +330,8 @@ const NuxtModule = function(moduleOptions = {}) {
|
|
|
80
330
|
component: resolveRoute(this.options.buildDir, dst)
|
|
81
331
|
});
|
|
82
332
|
});
|
|
333
|
+
const register = resolveRegister(options.register, process.env);
|
|
334
|
+
const backend = resolveBackend(options.register, process.env);
|
|
83
335
|
if (!Array.isArray(options.origins)) {
|
|
84
336
|
warn(
|
|
85
337
|
"Ignored the origins option. Use a list of full http(s) URLs, such as ['https://cms.example.com']."
|
|
@@ -88,25 +340,48 @@ const NuxtModule = function(moduleOptions = {}) {
|
|
|
88
340
|
const listed = (Array.isArray(options.origins) ? options.origins : []).filter(
|
|
89
341
|
(url) => {
|
|
90
342
|
if (toOrigin(url)) return true;
|
|
91
|
-
warn(
|
|
92
|
-
`Ignored the origin "${url}". Use a full http(s) URL, such as "https://cms.example.com".`
|
|
93
|
-
);
|
|
343
|
+
warn(`Ignored the origin "${forLog(url)}". ${originProblem(url)}`);
|
|
94
344
|
return false;
|
|
95
345
|
}
|
|
96
346
|
);
|
|
97
347
|
const pluginOptions = {
|
|
98
348
|
include: toInclude(options.include),
|
|
99
349
|
origins: [],
|
|
350
|
+
// Druxt's Drupal origin. The page fetches from any other origin without
|
|
351
|
+
// the Druxt client, whose options are for druxt.baseUrl.
|
|
352
|
+
drupal: null,
|
|
100
353
|
layouts: toLayouts(options.layouts)
|
|
101
354
|
};
|
|
355
|
+
let reported;
|
|
102
356
|
const setOrigins = () => {
|
|
103
357
|
const baseUrl = ((this.options || {}).druxt || {}).baseUrl;
|
|
358
|
+
pluginOptions.drupal = toOrigin(baseUrl);
|
|
359
|
+
if (baseUrl && !pluginOptions.drupal && reported !== baseUrl) {
|
|
360
|
+
reported = baseUrl;
|
|
361
|
+
warn(
|
|
362
|
+
`Ignored druxt.baseUrl "${forLog(baseUrl)}" for previews. ${originProblem(baseUrl)}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
104
365
|
pluginOptions.origins = [
|
|
105
|
-
...new Set([baseUrl, ...listed].map(toOrigin).filter(Boolean))
|
|
366
|
+
...new Set([baseUrl, backend, ...listed].map(toOrigin).filter(Boolean))
|
|
106
367
|
];
|
|
107
368
|
};
|
|
108
369
|
setOrigins();
|
|
109
370
|
if (this.nuxt) this.nuxt.hook("modules:done", setOrigins);
|
|
371
|
+
if (register && this.nuxt) {
|
|
372
|
+
this.nuxt.hook(
|
|
373
|
+
"ready",
|
|
374
|
+
() => registerOnce(
|
|
375
|
+
() => registerTarget({
|
|
376
|
+
...register,
|
|
377
|
+
druxt: (this.options || {}).druxt || {},
|
|
378
|
+
baseUrl: ((this.options || {}).druxt || {}).baseUrl,
|
|
379
|
+
path: options.path,
|
|
380
|
+
layouts: pluginOptions.layouts
|
|
381
|
+
})
|
|
382
|
+
)
|
|
383
|
+
);
|
|
384
|
+
}
|
|
110
385
|
this.addPlugin({
|
|
111
386
|
src: resolve(__dirname, "../templates/plugin.js"),
|
|
112
387
|
fileName: "druxt-node-preview.js",
|
package/dist/index.ssr.js
CHANGED
|
@@ -1,15 +1,265 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const druxt = require('druxt');
|
|
5
|
+
|
|
6
|
+
const PREFIX = "[druxt-node-preview] ";
|
|
7
|
+
const TIMEOUT = 5e3;
|
|
8
|
+
const HEARTBEAT = 6 * 60 * 60 * 1e3;
|
|
9
|
+
const warn$1 = (message) => console.warn(PREFIX + message);
|
|
10
|
+
const info = (message) => console.info(PREFIX + message);
|
|
11
|
+
const fromEnv = (env, name) => (env[name] || "").trim() || void 0;
|
|
12
|
+
const toBaseUrl = (value) => {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
return typeof value === "string" && ["http:", "https:"].includes(url.protocol) && !url.search && !url.hash ? (url.origin + url.pathname).replace(/\/+$/, "") : null;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const resolveBackend = (value, env) => {
|
|
21
|
+
const options = registerOptions(value);
|
|
22
|
+
const raw = options && rawBackend(options, env);
|
|
23
|
+
return raw ? toBaseUrl(raw) : null;
|
|
24
|
+
};
|
|
25
|
+
const registerOptions = (value) => {
|
|
26
|
+
if (value === true) return {};
|
|
27
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
28
|
+
};
|
|
29
|
+
const rawBackend = (options, env) => fromEnv(env, "DRUXT_NODE_PREVIEW_REGISTER_BACKEND") || options.backend;
|
|
30
|
+
const resolveRegister = (value, env) => {
|
|
31
|
+
if (value === void 0 || value === null || value === false) return null;
|
|
32
|
+
const options = registerOptions(value);
|
|
33
|
+
if (!options) {
|
|
34
|
+
warn$1("Ignored the register option. Use true or an object.");
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (options.secret !== void 0) {
|
|
38
|
+
warn$1(
|
|
39
|
+
"Ignored druxt.nodePreview.register.secret. Never put the secret in nuxt.config.js."
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const url = toBaseUrl(fromEnv(env, "DRUXT_NODE_PREVIEW_URL") || options.url);
|
|
43
|
+
const consumer = fromEnv(env, "DRUXT_NODE_PREVIEW_CLIENT_ID") || options.consumer;
|
|
44
|
+
const setBackend = rawBackend(options, env);
|
|
45
|
+
const backend = setBackend ? toBaseUrl(setBackend) : null;
|
|
46
|
+
const secretName = setBackend ? "DRUXT_NODE_PREVIEW_REGISTER_SECRET" : "DRUXT_NODE_PREVIEW_CLIENT_SECRET";
|
|
47
|
+
const secret = fromEnv(env, secretName);
|
|
48
|
+
const envToken = fromEnv(env, "DRUXT_NODE_PREVIEW_REGISTER_TOKEN");
|
|
49
|
+
const rawToken = envToken !== void 0 ? envToken : options.token;
|
|
50
|
+
const token = rawToken === void 0 ? Boolean(backend) : (/* @__PURE__ */ new Map([
|
|
51
|
+
["true", true],
|
|
52
|
+
["1", true],
|
|
53
|
+
["false", false],
|
|
54
|
+
["0", false]
|
|
55
|
+
])).get(String(rawToken));
|
|
56
|
+
const problems = [
|
|
57
|
+
!url && "DRUXT_NODE_PREVIEW_URL or register.url must be the frontend's full http(s) URL, with no query or fragment.",
|
|
58
|
+
(typeof consumer !== "string" || !/^[\x21-\x7e]+$/.test(consumer)) && "DRUXT_NODE_PREVIEW_CLIENT_ID or register.consumer must be the consumer's client ID.",
|
|
59
|
+
setBackend && !backend && "DRUXT_NODE_PREVIEW_REGISTER_BACKEND or register.backend must be the Drupal site's full http(s) URL.",
|
|
60
|
+
token === void 0 && "DRUXT_NODE_PREVIEW_REGISTER_TOKEN or register.token must be true or false.",
|
|
61
|
+
!secret && `${secretName} must be the consumer's secret, in the environment that runs Nuxt.`
|
|
62
|
+
].filter(Boolean);
|
|
63
|
+
if (problems.length) {
|
|
64
|
+
warn$1(`Ignored the register option. ${problems.join(" ")}`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const label = typeof options.label === "string" && options.label.trim() || new URL(url).host;
|
|
68
|
+
return {
|
|
69
|
+
url,
|
|
70
|
+
label,
|
|
71
|
+
consumer,
|
|
72
|
+
secret,
|
|
73
|
+
token,
|
|
74
|
+
...backend ? { backend } : {}
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const targetUrl = (url, path, layouts, token = false, backend = false) => url + path + "?vm=[view_mode]" + (layouts.length ? "&layout=[layout]" : "") + (backend ? "#[jsonapi_node_preview]" : "#[jsonapi_node_preview_path]") + (token ? ";token=[preview_token]" : "");
|
|
78
|
+
const CREDENTIALS = "Check the secret, and that the consumer is confidential, allows the Client credentials grant, and has a Client credentials user.";
|
|
79
|
+
const TOKEN_HINTS = {
|
|
80
|
+
400: CREDENTIALS,
|
|
81
|
+
401: CREDENTIALS,
|
|
82
|
+
404: "Install and set up the Simple OAuth module on Drupal."
|
|
83
|
+
};
|
|
84
|
+
const TARGET_HINTS = {
|
|
85
|
+
401: `The consumer's Client credentials user must own the consumer, and the token's scope must give the "register consumer node preview targets" permission.`,
|
|
86
|
+
403: `The consumer's Client credentials user must own the consumer, and the token's scope must give the "register consumer node preview targets" permission.`,
|
|
87
|
+
404: "Check that Consumer Node Preview is installed, and that a consumer has this client ID.",
|
|
88
|
+
405: "Update Consumer Node Preview. This module needs a version with the PATCH method.",
|
|
89
|
+
422: "Check the register url and the path option."
|
|
90
|
+
};
|
|
91
|
+
const reason = (data) => {
|
|
92
|
+
const text = [data == null ? void 0 : data.error_description, data == null ? void 0 : data.message, data == null ? void 0 : data.error].find(
|
|
93
|
+
(value) => typeof value === "string" && value.trim()
|
|
94
|
+
);
|
|
95
|
+
return text ? `: ${text.trim().slice(0, 300)}` : "";
|
|
96
|
+
};
|
|
97
|
+
const forBackend = (defaults, options) => {
|
|
98
|
+
const adapter = typeof defaults.adapter === "function" ? defaults.adapter : "http";
|
|
99
|
+
for (const name of Object.keys(defaults)) delete defaults[name];
|
|
100
|
+
Object.assign(defaults, {
|
|
101
|
+
...options,
|
|
102
|
+
adapter,
|
|
103
|
+
headers: {},
|
|
104
|
+
transformRequest: [
|
|
105
|
+
(data) => data === void 0 || typeof data === "string" ? data : JSON.stringify(data)
|
|
106
|
+
],
|
|
107
|
+
transformResponse: [
|
|
108
|
+
(data) => {
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(data);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
const send = async (client, config, hints) => {
|
|
119
|
+
const { status, data, headers } = await client.axios.request({
|
|
120
|
+
...config,
|
|
121
|
+
maxRedirects: 0,
|
|
122
|
+
validateStatus: () => true
|
|
123
|
+
});
|
|
124
|
+
if (status === 200) return data;
|
|
125
|
+
if (status >= 300 && status < 400) {
|
|
126
|
+
const location = (headers == null ? void 0 : headers.location) ? ` to ${headers.location}` : "";
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Drupal redirected ${config.url}${location} (HTTP ${status}). Set druxt.baseUrl to the URL Drupal redirects to.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
throw new Error(
|
|
132
|
+
`${config.url} answered HTTP ${status}${reason(data)}. ${hints[status] || ""}`.trim()
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
const registerTarget = async ({
|
|
136
|
+
baseUrl,
|
|
137
|
+
backend,
|
|
138
|
+
token: useToken,
|
|
139
|
+
druxt: druxt$1 = {},
|
|
140
|
+
url,
|
|
141
|
+
label,
|
|
142
|
+
consumer,
|
|
143
|
+
secret,
|
|
144
|
+
path,
|
|
145
|
+
layouts
|
|
146
|
+
}) => {
|
|
147
|
+
let token;
|
|
148
|
+
try {
|
|
149
|
+
if (!backend && !baseUrl) throw new Error("druxt.baseUrl is not set.");
|
|
150
|
+
const base = (backend || baseUrl).replace(/\/+$/, "");
|
|
151
|
+
const { protocol, hostname } = new URL(base);
|
|
152
|
+
if (protocol === "http:" && !/^(localhost|127\.\d+\.\d+\.\d+|\[::1\])$|\.localhost$/.test(hostname)) {
|
|
153
|
+
warn$1(
|
|
154
|
+
`${backend ? "The register backend" : "druxt.baseUrl"} uses plain HTTP, so the secret and the token cross the network unencrypted. Use https.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const { timeout = TIMEOUT, proxy } = druxt$1.axios || {};
|
|
158
|
+
const client = new druxt.DruxtClient(base, {
|
|
159
|
+
...druxt$1,
|
|
160
|
+
axios: backend ? { timeout } : { timeout: TIMEOUT, ...druxt$1.axios }
|
|
161
|
+
});
|
|
162
|
+
if (backend) {
|
|
163
|
+
forBackend(client.axios.defaults, {
|
|
164
|
+
timeout,
|
|
165
|
+
...proxy === void 0 ? {} : { proxy }
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const answer = await send(
|
|
169
|
+
client,
|
|
170
|
+
{
|
|
171
|
+
method: "POST",
|
|
172
|
+
url: `${base}/oauth/token`,
|
|
173
|
+
headers: {
|
|
174
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
175
|
+
Accept: "application/json"
|
|
176
|
+
},
|
|
177
|
+
data: new URLSearchParams({
|
|
178
|
+
grant_type: "client_credentials",
|
|
179
|
+
client_id: consumer,
|
|
180
|
+
client_secret: secret
|
|
181
|
+
}).toString()
|
|
182
|
+
},
|
|
183
|
+
TOKEN_HINTS
|
|
184
|
+
);
|
|
185
|
+
token = answer == null ? void 0 : answer.access_token;
|
|
186
|
+
if (!token) throw new Error(`${base}/oauth/token gave no access token.`);
|
|
187
|
+
await send(
|
|
188
|
+
client,
|
|
189
|
+
{
|
|
190
|
+
method: "PATCH",
|
|
191
|
+
url: `${base}/api/consumer-node-preview/targets`,
|
|
192
|
+
headers: {
|
|
193
|
+
Accept: "application/json",
|
|
194
|
+
Authorization: `Bearer ${token}`,
|
|
195
|
+
"Content-Type": "application/json",
|
|
196
|
+
"X-Consumer-ID": consumer
|
|
197
|
+
},
|
|
198
|
+
// Without layouts, Drupal keeps the consumer's own.
|
|
199
|
+
data: {
|
|
200
|
+
target: {
|
|
201
|
+
label,
|
|
202
|
+
url: targetUrl(
|
|
203
|
+
url,
|
|
204
|
+
path,
|
|
205
|
+
layouts,
|
|
206
|
+
Boolean(useToken),
|
|
207
|
+
Boolean(backend)
|
|
208
|
+
)
|
|
209
|
+
},
|
|
210
|
+
...layouts.length ? { layouts: [...new Set(layouts)] } : {}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
TARGET_HINTS
|
|
214
|
+
);
|
|
215
|
+
info(`Registered the preview target "${label}" on "${consumer}".`);
|
|
216
|
+
return "registered";
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const message = [secret, token].filter(Boolean).reduce(
|
|
219
|
+
(text, value) => text.split(value).join("[hidden]"),
|
|
220
|
+
error.message
|
|
221
|
+
);
|
|
222
|
+
warn$1(`Could not register the preview target: ${message}`);
|
|
223
|
+
return "failed";
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const registerOnce = (run) => {
|
|
227
|
+
var _a, _b;
|
|
228
|
+
const key = /* @__PURE__ */ Symbol.for("druxt-node-preview.register");
|
|
229
|
+
if (!process[key]) {
|
|
230
|
+
process[key] = run();
|
|
231
|
+
(_b = (_a = setInterval(run, HEARTBEAT)).unref) == null ? void 0 : _b.call(_a);
|
|
232
|
+
}
|
|
233
|
+
return process[key];
|
|
234
|
+
};
|
|
4
235
|
|
|
5
236
|
const toOrigin = (url) => {
|
|
6
237
|
try {
|
|
7
|
-
const { origin, protocol } = new URL(url);
|
|
8
|
-
|
|
238
|
+
const { origin, protocol, hostname } = new URL(url);
|
|
239
|
+
const plain = /^([a-z0-9_-]+\.)*[a-z0-9_-]+\.?$|^\[[0-9a-f:.]+\]$/;
|
|
240
|
+
return ["http:", "https:"].includes(protocol) && plain.test(hostname) ? origin : null;
|
|
9
241
|
} catch (e) {
|
|
10
242
|
return null;
|
|
11
243
|
}
|
|
12
244
|
};
|
|
245
|
+
const forLog = (url) => {
|
|
246
|
+
try {
|
|
247
|
+
const { protocol, host } = new URL(url);
|
|
248
|
+
return protocol + "//" + host;
|
|
249
|
+
} catch (e) {
|
|
250
|
+
return String(url).replace(/^[^/]*@/, "");
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const originProblem = (url) => {
|
|
254
|
+
try {
|
|
255
|
+
const { hostname, protocol } = new URL(url);
|
|
256
|
+
if (["http:", "https:"].includes(protocol)) {
|
|
257
|
+
return `Its host, "${hostname}", may only have letters, digits, dots, hyphens and _, or be an IPv6 address in brackets.`;
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {
|
|
260
|
+
}
|
|
261
|
+
return 'Use a full http(s) URL, such as "https://cms.example.com".';
|
|
262
|
+
};
|
|
13
263
|
const warn = (message) => console.warn("[druxt-node-preview] " + message);
|
|
14
264
|
const toInclude = (value) => {
|
|
15
265
|
const paths = (list, where) => {
|
|
@@ -82,6 +332,8 @@ const NuxtModule = function(moduleOptions = {}) {
|
|
|
82
332
|
component: resolveRoute(this.options.buildDir, dst)
|
|
83
333
|
});
|
|
84
334
|
});
|
|
335
|
+
const register = resolveRegister(options.register, process.env);
|
|
336
|
+
const backend = resolveBackend(options.register, process.env);
|
|
85
337
|
if (!Array.isArray(options.origins)) {
|
|
86
338
|
warn(
|
|
87
339
|
"Ignored the origins option. Use a list of full http(s) URLs, such as ['https://cms.example.com']."
|
|
@@ -90,25 +342,48 @@ const NuxtModule = function(moduleOptions = {}) {
|
|
|
90
342
|
const listed = (Array.isArray(options.origins) ? options.origins : []).filter(
|
|
91
343
|
(url) => {
|
|
92
344
|
if (toOrigin(url)) return true;
|
|
93
|
-
warn(
|
|
94
|
-
`Ignored the origin "${url}". Use a full http(s) URL, such as "https://cms.example.com".`
|
|
95
|
-
);
|
|
345
|
+
warn(`Ignored the origin "${forLog(url)}". ${originProblem(url)}`);
|
|
96
346
|
return false;
|
|
97
347
|
}
|
|
98
348
|
);
|
|
99
349
|
const pluginOptions = {
|
|
100
350
|
include: toInclude(options.include),
|
|
101
351
|
origins: [],
|
|
352
|
+
// Druxt's Drupal origin. The page fetches from any other origin without
|
|
353
|
+
// the Druxt client, whose options are for druxt.baseUrl.
|
|
354
|
+
drupal: null,
|
|
102
355
|
layouts: toLayouts(options.layouts)
|
|
103
356
|
};
|
|
357
|
+
let reported;
|
|
104
358
|
const setOrigins = () => {
|
|
105
359
|
const baseUrl = ((this.options || {}).druxt || {}).baseUrl;
|
|
360
|
+
pluginOptions.drupal = toOrigin(baseUrl);
|
|
361
|
+
if (baseUrl && !pluginOptions.drupal && reported !== baseUrl) {
|
|
362
|
+
reported = baseUrl;
|
|
363
|
+
warn(
|
|
364
|
+
`Ignored druxt.baseUrl "${forLog(baseUrl)}" for previews. ${originProblem(baseUrl)}`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
106
367
|
pluginOptions.origins = [
|
|
107
|
-
...new Set([baseUrl, ...listed].map(toOrigin).filter(Boolean))
|
|
368
|
+
...new Set([baseUrl, backend, ...listed].map(toOrigin).filter(Boolean))
|
|
108
369
|
];
|
|
109
370
|
};
|
|
110
371
|
setOrigins();
|
|
111
372
|
if (this.nuxt) this.nuxt.hook("modules:done", setOrigins);
|
|
373
|
+
if (register && this.nuxt) {
|
|
374
|
+
this.nuxt.hook(
|
|
375
|
+
"ready",
|
|
376
|
+
() => registerOnce(
|
|
377
|
+
() => registerTarget({
|
|
378
|
+
...register,
|
|
379
|
+
druxt: (this.options || {}).druxt || {},
|
|
380
|
+
baseUrl: ((this.options || {}).druxt || {}).baseUrl,
|
|
381
|
+
path: options.path,
|
|
382
|
+
layouts: pluginOptions.layouts
|
|
383
|
+
})
|
|
384
|
+
)
|
|
385
|
+
);
|
|
386
|
+
}
|
|
112
387
|
this.addPlugin({
|
|
113
388
|
src: path.resolve(__dirname, "../templates/plugin.js"),
|
|
114
389
|
fileName: "druxt-node-preview.js",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@druxt-contrib/node-preview",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.20261109195239",
|
|
4
4
|
"description": "Renders Drupal node previews in a Druxt frontend, fed by JSON:API Node Preview and Consumer Node Preview.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"decoupled",
|
|
@@ -47,10 +47,11 @@
|
|
|
47
47
|
"lint:knip": "knip --include dependencies,unlisted --no-config-hints",
|
|
48
48
|
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
|
49
49
|
"lint:private": "node scripts/lint-private-refs.mjs",
|
|
50
|
-
"lint:prose": "PROSE_LINT_GLOB='!{.changeset
|
|
50
|
+
"lint:prose": "PROSE_LINT_GLOB='!{.changeset/**,example/drupal/**,CHANGELOG.md}' bash .gitlab/scripts/lint-prose.sh --all",
|
|
51
51
|
"lint:prose:install": "bash .gitlab/scripts/install-vale.sh .vale/bin .vale/styles",
|
|
52
52
|
"prepack": "npm run build",
|
|
53
53
|
"prepare": "node scripts/postinstall.mjs",
|
|
54
|
+
"release:version": "changeset version && npm install --package-lock-only --ignore-scripts --no-audit --no-fund",
|
|
54
55
|
"test": "jest",
|
|
55
56
|
"test:e2e": "playwright test",
|
|
56
57
|
"test:e2e:stack": "test/e2e/run.sh",
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"@commitlint/cli": "17.8.1",
|
|
64
65
|
"@commitlint/config-conventional": "17.8.1",
|
|
65
66
|
"@playwright/test": "^1.49.0",
|
|
67
|
+
"axios": "^1.21.0",
|
|
66
68
|
"babel-core": "7.0.0-bridge.0",
|
|
67
69
|
"cspell": "7.3.9",
|
|
68
70
|
"druxt": "0.25.0",
|