@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/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
- return ["http:", "https:"].includes(protocol) ? origin : null;
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
- return ["http:", "https:"].includes(protocol) ? origin : null;
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.20261030024907",
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,example/drupal}/**' bash .gitlab/scripts/lint-prose.sh --all",
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",