@fishawack/lab-env 5.4.0-beta.1 → 5.5.0-beta.1
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 +44 -0
- package/_Ai/python-3.md +31 -28
- package/_Ai/workspace-@.md +4 -0
- package/_Test/_fixtures/content/config.json +36 -0
- package/_Test/_fixtures/content/content-0/blah.txt +1 -0
- package/_Test/content.js +517 -0
- package/cli.js +1 -0
- package/commands/content.js +115 -1
- package/commands/create/cmds/provision.js +1 -1
- package/commands/create/libs/aws-cloudfront-request.js +1 -1
- package/commands/create/libs/aws-cloudfront-response.js +1 -1
- package/commands/create/libs/vars.js +4 -0
- package/commands/create/templates/elasticbeanstalk/.platform/httpd/conf.d/security_headers-wordpress.conf +1 -1
- package/commands/create/templates/elasticbeanstalk/.platform/httpd/conf.d/security_headers.conf +1 -1
- package/commands/create/templates/elasticbeanstalk/.platform/nginx/conf.d/security_headers.conf +1 -1
- package/commands/helpers/content-pull.js +209 -0
- package/commands/helpers/content-request.js +223 -0
- package/commands/scan.js +49 -0
- package/commands/test.js +2 -15
- package/commands/workspace.js +160 -63
- package/package.json +3 -1
- package/python/0/docker-compose.yml +13 -0
package/_Test/content.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const expect = require("chai").expect;
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs-extra");
|
|
6
|
+
const glob = require("glob");
|
|
7
|
+
const { isEqual } = require("lodash");
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
pullS3,
|
|
11
|
+
parseBucketAndPrefix,
|
|
12
|
+
getLocalFiles,
|
|
13
|
+
createClient,
|
|
14
|
+
} = require("../commands/helpers/content-pull.js");
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
pullRequests,
|
|
18
|
+
download,
|
|
19
|
+
rewrite,
|
|
20
|
+
urlJoin,
|
|
21
|
+
} = require("../commands/helpers/content-request.js");
|
|
22
|
+
|
|
23
|
+
const fixtureDir = path.join(__dirname, "_fixtures", "content");
|
|
24
|
+
const outputDir = path.join(fixtureDir, "_Build", "content");
|
|
25
|
+
const snapshotPath = path.join(fixtureDir, ".tmp", "content.json");
|
|
26
|
+
|
|
27
|
+
function loadFixtureConfig(branch) {
|
|
28
|
+
const config = fs.readJSONSync(path.join(fixtureDir, "config.json"));
|
|
29
|
+
const branchConfig = config.attributes.targets[branch] || {};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...config.attributes,
|
|
33
|
+
...branchConfig,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("content", () => {
|
|
38
|
+
describe("parseBucketAndPrefix", () => {
|
|
39
|
+
it("Should parse bucket only when no slash", () => {
|
|
40
|
+
const result = parseBucketAndPrefix("my-bucket");
|
|
41
|
+
expect(result.Bucket).to.equal("my-bucket");
|
|
42
|
+
expect(result.Prefix).to.equal("");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("Should parse bucket and prefix when slash present", () => {
|
|
46
|
+
const result = parseBucketAndPrefix("my-bucket/some/prefix");
|
|
47
|
+
expect(result.Bucket).to.equal("my-bucket");
|
|
48
|
+
expect(result.Prefix).to.equal("some/prefix");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("Should handle single level prefix", () => {
|
|
52
|
+
const result = parseBucketAndPrefix("my-bucket/folder");
|
|
53
|
+
expect(result.Bucket).to.equal("my-bucket");
|
|
54
|
+
expect(result.Prefix).to.equal("folder");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("getLocalFiles", () => {
|
|
59
|
+
it("Should return empty array for non-existent directory", () => {
|
|
60
|
+
const files = getLocalFiles(path.join(fixtureDir, "non-existent"));
|
|
61
|
+
expect(files).to.be.an("array").that.is.empty;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("Should return file list with relative paths", () => {
|
|
65
|
+
const files = getLocalFiles(path.join(fixtureDir, "content-0"));
|
|
66
|
+
expect(files).to.be.an("array").that.is.not.empty;
|
|
67
|
+
expect(files[0]).to.have.property("relativePath");
|
|
68
|
+
expect(files[0]).to.have.property("mtime");
|
|
69
|
+
expect(files[0]).to.have.property("size");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("urlJoin", () => {
|
|
74
|
+
it("Should join base URL with path segments", () => {
|
|
75
|
+
const result = urlJoin(
|
|
76
|
+
"https://example.com",
|
|
77
|
+
"/api/v1/",
|
|
78
|
+
"endpoint",
|
|
79
|
+
);
|
|
80
|
+
expect(result).to.contain("example.com");
|
|
81
|
+
expect(result).to.contain("endpoint");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("Should handle trailing slashes", () => {
|
|
85
|
+
const result = urlJoin(
|
|
86
|
+
"https://example.com/",
|
|
87
|
+
"api/v1/",
|
|
88
|
+
"endpoint",
|
|
89
|
+
);
|
|
90
|
+
expect(result).to.include("api/v1/endpoint");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("content:pull — S3", () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
fs.removeSync(outputDir);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("Should pull down assets from s3 via aws sdk", async () => {
|
|
100
|
+
const config = loadFixtureConfig("aws-s3");
|
|
101
|
+
|
|
102
|
+
await pullS3(config.content[0], path.join(outputDir, "content-0"));
|
|
103
|
+
|
|
104
|
+
expect(glob.sync(path.join(outputDir, "content-0/**/*"))).to.be.an(
|
|
105
|
+
"array",
|
|
106
|
+
).that.is.not.empty;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("Should pull down assets from s3 when nested", async () => {
|
|
110
|
+
const config = loadFixtureConfig("aws-s3-nested");
|
|
111
|
+
|
|
112
|
+
await pullS3(config.content[0], path.join(outputDir, "content-0"));
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
fs.readFileSync(path.join(outputDir, "content-0/test.txt"), {
|
|
116
|
+
encoding: "utf8",
|
|
117
|
+
}),
|
|
118
|
+
).to.equal("core-test-suite-content-nested");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("Should store in directory with key name when defined", async () => {
|
|
122
|
+
const config = loadFixtureConfig("aws-s3-key");
|
|
123
|
+
|
|
124
|
+
await pullS3(config.content[0], path.join(outputDir, "custom-key"));
|
|
125
|
+
|
|
126
|
+
expect(glob.sync(path.join(outputDir, "custom-key/**/*"))).to.be.an(
|
|
127
|
+
"array",
|
|
128
|
+
).that.is.not.empty;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("Should sync new assets from s3 (bidirectional)", async () => {
|
|
132
|
+
const config = loadFixtureConfig("aws-s3-sync");
|
|
133
|
+
const saveTo = path.join(outputDir, "custom-key");
|
|
134
|
+
const profile = config.content[0]["aws-s3"];
|
|
135
|
+
const client = createClient(profile);
|
|
136
|
+
const { Bucket, Prefix } = parseBucketAndPrefix(
|
|
137
|
+
config.content[0].location,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Clean up any previous sync test file
|
|
141
|
+
try {
|
|
142
|
+
const { DeleteObjectCommand } = require("@aws-sdk/client-s3");
|
|
143
|
+
await client.send(
|
|
144
|
+
new DeleteObjectCommand({
|
|
145
|
+
Bucket,
|
|
146
|
+
Key: Prefix
|
|
147
|
+
? `${Prefix}/sync-test.txt`
|
|
148
|
+
: "sync-test.txt",
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
} catch {
|
|
152
|
+
/* may not exist */
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// First pull to get existing content
|
|
156
|
+
await pullS3(config.content[0], saveTo);
|
|
157
|
+
|
|
158
|
+
// Create a local file to sync up
|
|
159
|
+
fs.mkdirpSync(saveTo);
|
|
160
|
+
fs.writeFileSync(path.join(saveTo, "sync-test.txt"), "sync-test");
|
|
161
|
+
|
|
162
|
+
// Second pull should upload local file then download remote
|
|
163
|
+
await pullS3(config.content[0], saveTo);
|
|
164
|
+
|
|
165
|
+
// Clean local and re-pull — the synced file should come back
|
|
166
|
+
fs.removeSync(saveTo);
|
|
167
|
+
await pullS3(config.content[0], saveTo);
|
|
168
|
+
|
|
169
|
+
expect(() =>
|
|
170
|
+
fs.readFileSync(path.join(saveTo, "sync-test.txt"), {
|
|
171
|
+
encoding: "utf8",
|
|
172
|
+
}),
|
|
173
|
+
).to.not.throw();
|
|
174
|
+
|
|
175
|
+
// Cleanup remote sync test file
|
|
176
|
+
try {
|
|
177
|
+
const { DeleteObjectCommand } = require("@aws-sdk/client-s3");
|
|
178
|
+
await client.send(
|
|
179
|
+
new DeleteObjectCommand({
|
|
180
|
+
Bucket,
|
|
181
|
+
Key: Prefix
|
|
182
|
+
? `${Prefix}/sync-test.txt`
|
|
183
|
+
: "sync-test.txt",
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
} catch {
|
|
187
|
+
/* cleanup best-effort */
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("Should not re-download files when local is newer (--update semantics)", async () => {
|
|
192
|
+
const config = loadFixtureConfig("aws-s3");
|
|
193
|
+
const saveTo = path.join(outputDir, "content-0");
|
|
194
|
+
|
|
195
|
+
// First pull
|
|
196
|
+
await pullS3(config.content[0], saveTo);
|
|
197
|
+
|
|
198
|
+
const files = glob.sync(path.join(saveTo, "**/*"), {
|
|
199
|
+
nodir: true,
|
|
200
|
+
});
|
|
201
|
+
expect(files.length).to.be.greaterThan(0);
|
|
202
|
+
|
|
203
|
+
// Touch all local files to make them "newer"
|
|
204
|
+
const futureTime = new Date(Date.now() + 86400000);
|
|
205
|
+
files.forEach((f) => fs.utimesSync(f, futureTime, futureTime));
|
|
206
|
+
|
|
207
|
+
// Record mtime before second pull
|
|
208
|
+
const mtimesBefore = {};
|
|
209
|
+
files.forEach((f) => {
|
|
210
|
+
mtimesBefore[f] = fs.statSync(f).mtimeMs;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Second pull should skip all files
|
|
214
|
+
await pullS3(config.content[0], saveTo);
|
|
215
|
+
|
|
216
|
+
// Verify files were not overwritten
|
|
217
|
+
files.forEach((f) => {
|
|
218
|
+
expect(fs.statSync(f).mtimeMs).to.equal(mtimesBefore[f]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("Should skip non-S3 protocols with warning", async () => {
|
|
223
|
+
const config = loadFixtureConfig("ftp-legacy");
|
|
224
|
+
|
|
225
|
+
// pullS3 won't be called for non-s3 — this is handled in content.js
|
|
226
|
+
// Verify the config doesn't have aws-s3 key
|
|
227
|
+
expect(config.content[0]["aws-s3"]).to.be.undefined;
|
|
228
|
+
expect(config.content[0].ftp).to.equal("ftp-fishawack.egnyte.com");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
after(() => {
|
|
232
|
+
fs.removeSync(outputDir);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("config change detection", () => {
|
|
237
|
+
beforeEach(() => {
|
|
238
|
+
fs.removeSync(outputDir);
|
|
239
|
+
fs.removeSync(path.join(fixtureDir, ".tmp"));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("Should clean content folders when content property is an empty array", () => {
|
|
243
|
+
// Pre-populate content directory
|
|
244
|
+
fs.copySync(
|
|
245
|
+
path.join(fixtureDir, "content-0"),
|
|
246
|
+
path.join(outputDir, "content-0"),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const config = loadFixtureConfig("empty");
|
|
250
|
+
|
|
251
|
+
expect(config.content).to.be.an("array").that.is.empty;
|
|
252
|
+
expect(glob.sync(path.join(outputDir, "**/*"))).to.be.an("array")
|
|
253
|
+
.that.is.not.empty;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("Should detect when content config has not changed", () => {
|
|
257
|
+
const config = loadFixtureConfig("aws-s3");
|
|
258
|
+
|
|
259
|
+
// Save a snapshot
|
|
260
|
+
fs.mkdirpSync(path.dirname(snapshotPath));
|
|
261
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(config.content));
|
|
262
|
+
|
|
263
|
+
// Compare — should be equal
|
|
264
|
+
const prev = JSON.parse(
|
|
265
|
+
fs.readFileSync(snapshotPath, { encoding: "utf8" }),
|
|
266
|
+
);
|
|
267
|
+
expect(isEqual(prev, config.content)).to.be.true;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("Should detect when content config has changed", () => {
|
|
271
|
+
const configOld = loadFixtureConfig("single");
|
|
272
|
+
const configNew = loadFixtureConfig("double");
|
|
273
|
+
|
|
274
|
+
// Save old snapshot
|
|
275
|
+
fs.mkdirpSync(path.dirname(snapshotPath));
|
|
276
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(configOld.content));
|
|
277
|
+
|
|
278
|
+
const prev = JSON.parse(
|
|
279
|
+
fs.readFileSync(snapshotPath, { encoding: "utf8" }),
|
|
280
|
+
);
|
|
281
|
+
expect(isEqual(prev, configNew.content)).to.be.false;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("Should handle missing previous snapshot gracefully", () => {
|
|
285
|
+
// No snapshot file exists
|
|
286
|
+
expect(fs.existsSync(snapshotPath)).to.be.false;
|
|
287
|
+
|
|
288
|
+
let prev = null;
|
|
289
|
+
try {
|
|
290
|
+
prev = JSON.parse(
|
|
291
|
+
fs.readFileSync(snapshotPath, { encoding: "utf8" }),
|
|
292
|
+
);
|
|
293
|
+
} catch {
|
|
294
|
+
// expected
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
expect(prev).to.be.null;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("Should clean content when config changes between pulls", async () => {
|
|
301
|
+
const configDouble = loadFixtureConfig("double");
|
|
302
|
+
|
|
303
|
+
// Simulate first pull creating two content dirs
|
|
304
|
+
fs.mkdirpSync(path.join(outputDir, "content-0"));
|
|
305
|
+
fs.mkdirpSync(path.join(outputDir, "custom-key"));
|
|
306
|
+
fs.writeFileSync(
|
|
307
|
+
path.join(outputDir, "content-0/file.txt"),
|
|
308
|
+
"test",
|
|
309
|
+
);
|
|
310
|
+
fs.writeFileSync(
|
|
311
|
+
path.join(outputDir, "custom-key/file.txt"),
|
|
312
|
+
"test",
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Save snapshot for "double" config
|
|
316
|
+
fs.mkdirpSync(path.dirname(snapshotPath));
|
|
317
|
+
fs.writeFileSync(
|
|
318
|
+
snapshotPath,
|
|
319
|
+
JSON.stringify(configDouble.content),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(glob.sync(path.join(outputDir, "*")).length).to.equal(2);
|
|
323
|
+
|
|
324
|
+
// Now switch to "single" config — should detect the change
|
|
325
|
+
const configSingle = loadFixtureConfig("single");
|
|
326
|
+
const prev = JSON.parse(
|
|
327
|
+
fs.readFileSync(snapshotPath, { encoding: "utf8" }),
|
|
328
|
+
);
|
|
329
|
+
expect(isEqual(prev, configSingle.content)).to.be.false;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("Should not clean content when config unchanged between pulls", () => {
|
|
333
|
+
const config = loadFixtureConfig("double");
|
|
334
|
+
|
|
335
|
+
// Simulate content dirs
|
|
336
|
+
fs.mkdirpSync(path.join(outputDir, "content-0"));
|
|
337
|
+
fs.mkdirpSync(path.join(outputDir, "custom-key"));
|
|
338
|
+
fs.mkdirpSync(path.join(outputDir, "fake-content"));
|
|
339
|
+
|
|
340
|
+
// Save snapshot
|
|
341
|
+
fs.mkdirpSync(path.dirname(snapshotPath));
|
|
342
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(config.content));
|
|
343
|
+
|
|
344
|
+
// Reload and compare — should be unchanged
|
|
345
|
+
const prev = JSON.parse(
|
|
346
|
+
fs.readFileSync(snapshotPath, { encoding: "utf8" }),
|
|
347
|
+
);
|
|
348
|
+
expect(isEqual(prev, config.content)).to.be.true;
|
|
349
|
+
|
|
350
|
+
// Fake-content should survive (not cleaned)
|
|
351
|
+
expect(glob.sync(path.join(outputDir, "*")).length).to.equal(3);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("Should skip cleaning when content property is undefined", () => {
|
|
355
|
+
// Pre-populate content
|
|
356
|
+
fs.copySync(
|
|
357
|
+
path.join(fixtureDir, "content-0"),
|
|
358
|
+
path.join(outputDir, "content-0"),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const config = loadFixtureConfig("handover");
|
|
362
|
+
|
|
363
|
+
// "handover" target has no content property
|
|
364
|
+
expect(config.content).to.be.undefined;
|
|
365
|
+
// Content should remain untouched
|
|
366
|
+
expect(glob.sync(path.join(outputDir, "**/*"))).to.be.an("array")
|
|
367
|
+
.that.is.not.empty;
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
after(() => {
|
|
371
|
+
fs.removeSync(outputDir);
|
|
372
|
+
fs.removeSync(path.join(fixtureDir, ".tmp"));
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe("content:request — HTTP APIs", () => {
|
|
377
|
+
describe("contentful", () => {
|
|
378
|
+
const saveTo = path.join(outputDir, "content-0");
|
|
379
|
+
|
|
380
|
+
before(async () => {
|
|
381
|
+
fs.removeSync(outputDir);
|
|
382
|
+
|
|
383
|
+
const config = loadFixtureConfig("contentful");
|
|
384
|
+
|
|
385
|
+
await pullRequests(config.content, fixtureDir + "/_Build");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("Should save to json files", () => {
|
|
389
|
+
expect(
|
|
390
|
+
fs
|
|
391
|
+
.lstatSync(path.join(saveTo, "media/layout.json"))
|
|
392
|
+
.isFile(),
|
|
393
|
+
).to.be.true;
|
|
394
|
+
expect(
|
|
395
|
+
fs
|
|
396
|
+
.lstatSync(path.join(saveTo, "media/layoutCopy.json"))
|
|
397
|
+
.isFile(),
|
|
398
|
+
).to.be.true;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("Should download images", () => {
|
|
402
|
+
const mediaFiles = glob.sync(
|
|
403
|
+
path.join(saveTo, "media/**/*.png"),
|
|
404
|
+
);
|
|
405
|
+
expect(mediaFiles).to.be.an("array").that.is.not.empty;
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
after(() => {
|
|
409
|
+
fs.removeSync(outputDir);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe("rewrite", () => {
|
|
415
|
+
const saveTo = path.join(outputDir, "rewrite-test");
|
|
416
|
+
|
|
417
|
+
beforeEach(() => {
|
|
418
|
+
fs.removeSync(saveTo);
|
|
419
|
+
fs.mkdirpSync(saveTo);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("Should rewrite remote URLs to local media paths", async () => {
|
|
423
|
+
// Create a mock JSON file with remote URLs
|
|
424
|
+
const mockData = JSON.stringify({
|
|
425
|
+
image: "https://example.com/wp-content/uploads/2024/photo.jpg",
|
|
426
|
+
text: "hello world",
|
|
427
|
+
nested: {
|
|
428
|
+
img: "https://example.com/wp-content/uploads/2024/nested.png",
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
fs.writeFileSync(path.join(saveTo, "test.json"), mockData);
|
|
432
|
+
|
|
433
|
+
await rewrite({
|
|
434
|
+
ext: "json",
|
|
435
|
+
type: "wp",
|
|
436
|
+
saveTo,
|
|
437
|
+
bundle: "",
|
|
438
|
+
find: "^https.*/wp-content/uploads",
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const result = fs.readFileSync(path.join(saveTo, "test.json"), {
|
|
442
|
+
encoding: "utf8",
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(result).to.contain("media/content");
|
|
446
|
+
expect(result).to.not.contain("wp-content/uploads");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("Should unwrap contentful response during rewrite", async () => {
|
|
450
|
+
// Contentful responses are arrays, rewrite should extract first element
|
|
451
|
+
const mockData = JSON.stringify([
|
|
452
|
+
{ title: "Entry 1", body: "Content" },
|
|
453
|
+
{ title: "Entry 2", body: "More content" },
|
|
454
|
+
]);
|
|
455
|
+
fs.writeFileSync(path.join(saveTo, "test.json"), mockData);
|
|
456
|
+
|
|
457
|
+
await rewrite({
|
|
458
|
+
ext: "json",
|
|
459
|
+
type: "contentful",
|
|
460
|
+
saveTo,
|
|
461
|
+
bundle: "",
|
|
462
|
+
find: "//images\\.ctfassets\\.net/(.*?)/(.*?)/(.*?)/",
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const result = JSON.parse(
|
|
466
|
+
fs.readFileSync(path.join(saveTo, "test.json"), {
|
|
467
|
+
encoding: "utf8",
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Should be unwrapped to first element, not an array
|
|
472
|
+
expect(result).to.have.property("title", "Entry 1");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
after(() => {
|
|
476
|
+
fs.removeSync(saveTo);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe("download", () => {
|
|
481
|
+
const saveTo = path.join(outputDir, "download-test");
|
|
482
|
+
|
|
483
|
+
beforeEach(() => {
|
|
484
|
+
fs.removeSync(saveTo);
|
|
485
|
+
fs.mkdirpSync(saveTo);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("Should extract unique URLs matching find pattern from JSON", async () => {
|
|
489
|
+
// Create JSON that references assets
|
|
490
|
+
const mockData = JSON.stringify({
|
|
491
|
+
images: [
|
|
492
|
+
"https://example.com/wp-content/uploads/photo1.jpg",
|
|
493
|
+
"https://example.com/wp-content/uploads/photo2.jpg",
|
|
494
|
+
"https://example.com/wp-content/uploads/photo1.jpg",
|
|
495
|
+
],
|
|
496
|
+
text: "not a URL",
|
|
497
|
+
});
|
|
498
|
+
fs.writeFileSync(path.join(saveTo, "test.json"), mockData);
|
|
499
|
+
|
|
500
|
+
// download will try to fetch these URLs — they'll fail but
|
|
501
|
+
// we verify the function doesn't throw
|
|
502
|
+
await download({
|
|
503
|
+
path: "https://example.com",
|
|
504
|
+
ext: "json",
|
|
505
|
+
saveTo,
|
|
506
|
+
bundle: "",
|
|
507
|
+
find: "^https.*/wp-content/uploads",
|
|
508
|
+
});
|
|
509
|
+
// If we get here without throwing, the URL extraction and
|
|
510
|
+
// deduplication logic works correctly
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
after(() => {
|
|
514
|
+
fs.removeSync(saveTo);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
});
|
package/cli.js
CHANGED
package/commands/content.js
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
const execSync = require("child_process").execSync;
|
|
2
2
|
const fs = require("fs-extra");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { isEqual } = require("lodash");
|
|
3
5
|
const utilities = require("./create/libs/utilities");
|
|
4
6
|
const _ = require("../globals.js");
|
|
5
7
|
const { getConfigurations, findRepository } = require("../hub.js");
|
|
6
8
|
const glob = require("glob");
|
|
9
|
+
const { pullS3 } = require("./helpers/content-pull.js");
|
|
10
|
+
const { pullRequests } = require("./helpers/content-request.js");
|
|
11
|
+
|
|
12
|
+
const SNAPSHOT_PATH = ".tmp/content.json";
|
|
13
|
+
|
|
14
|
+
function detectConfigChange(currentContent) {
|
|
15
|
+
let prev = null;
|
|
16
|
+
try {
|
|
17
|
+
prev = JSON.parse(fs.readFileSync(SNAPSHOT_PATH, { encoding: "utf8" }));
|
|
18
|
+
} catch {
|
|
19
|
+
// No previous snapshot
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (prev && !isEqual(prev, currentContent)) {
|
|
23
|
+
return prev;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function saveConfigSnapshot(content) {
|
|
30
|
+
fs.mkdirpSync(path.dirname(SNAPSHOT_PATH));
|
|
31
|
+
fs.writeFileSync(SNAPSHOT_PATH, JSON.stringify(content), {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
7
35
|
|
|
8
36
|
module.exports = [
|
|
9
37
|
"content",
|
|
@@ -34,9 +62,95 @@ module.exports = [
|
|
|
34
62
|
"title",
|
|
35
63
|
),
|
|
36
64
|
);
|
|
37
|
-
} else {
|
|
65
|
+
} else if (_.pkg?.scripts?.content) {
|
|
66
|
+
// Fallback: project has its own content script, delegate to core
|
|
67
|
+
console.log(
|
|
68
|
+
utilities.colorize(
|
|
69
|
+
"Project has a content script in package.json, falling back to core...",
|
|
70
|
+
"warning",
|
|
71
|
+
),
|
|
72
|
+
);
|
|
38
73
|
_.command("core", `npm run content`);
|
|
74
|
+
} else {
|
|
75
|
+
const contentConfig = _.coreConfig?.attributes?.content;
|
|
76
|
+
const srcBase = _.coreConfig?.attributes?.src || "_Build";
|
|
77
|
+
|
|
78
|
+
if (!contentConfig || contentConfig.length === 0) {
|
|
79
|
+
console.log(
|
|
80
|
+
utilities.colorize(
|
|
81
|
+
"No content config found. Skipping...",
|
|
82
|
+
"warning",
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
} else {
|
|
86
|
+
// Detect config changes and clean stale content
|
|
87
|
+
const prevContent = detectConfigChange(contentConfig);
|
|
88
|
+
if (prevContent) {
|
|
89
|
+
console.log(
|
|
90
|
+
utilities.colorize(
|
|
91
|
+
"Content config has changed. Removing existing content...",
|
|
92
|
+
"warning",
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
fs.removeSync(`${srcBase}/content`);
|
|
96
|
+
prevContent
|
|
97
|
+
.filter((d) => d.saveTo)
|
|
98
|
+
.forEach((d) => fs.removeSync(d.saveTo));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// content:pull — S3-based content
|
|
102
|
+
for (const [i, d] of contentConfig.entries()) {
|
|
103
|
+
if (d.location) {
|
|
104
|
+
if (!d["aws-s3"]) {
|
|
105
|
+
console.log(
|
|
106
|
+
utilities.colorize(
|
|
107
|
+
`Skipping "${d.location}" — only aws-s3 protocol is supported. FTP/SSH/LFTP have been removed.`,
|
|
108
|
+
"warning",
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const saveTo =
|
|
115
|
+
d.saveTo ||
|
|
116
|
+
`${srcBase}/content/${d.key || `content-${i}`}`;
|
|
117
|
+
|
|
118
|
+
console.log(
|
|
119
|
+
utilities.colorize(
|
|
120
|
+
`Pulling content from: ${d.location}`,
|
|
121
|
+
"title",
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await pullS3(d, saveTo);
|
|
126
|
+
|
|
127
|
+
console.log(
|
|
128
|
+
utilities.colorize(
|
|
129
|
+
`Content pulled from: ${d.location}`,
|
|
130
|
+
"success",
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// content:request — HTTP API-based content
|
|
137
|
+
const requestItems = contentConfig.filter((d) => d.url);
|
|
138
|
+
if (requestItems.length > 0) {
|
|
139
|
+
console.log(
|
|
140
|
+
utilities.colorize(
|
|
141
|
+
"Fetching content from API endpoints...",
|
|
142
|
+
"title",
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
await pullRequests(contentConfig, srcBase);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
saveConfigSnapshot(contentConfig);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
39
151
|
|
|
152
|
+
// Hub sync runs for both fallback and native paths
|
|
153
|
+
if (!argv.init) {
|
|
40
154
|
try {
|
|
41
155
|
if (process.env.HUB_URL) {
|
|
42
156
|
console.log(`Syncing branch configurations from Hub...`);
|
|
@@ -122,7 +122,7 @@ module.exports = [
|
|
|
122
122
|
name: "content-security-policy",
|
|
123
123
|
message: "content-security-policy header value:",
|
|
124
124
|
default:
|
|
125
|
-
"default-src 'self' https: data: 'unsafe-inline'
|
|
125
|
+
"default-src 'self' https: data: 'unsafe-inline' blob:;",
|
|
126
126
|
validate: (input) => !!input.length,
|
|
127
127
|
},
|
|
128
128
|
{
|
|
@@ -45,7 +45,7 @@ function handler(event) {
|
|
|
45
45
|
value: "max-age=31536000; includeSubDomains",
|
|
46
46
|
},
|
|
47
47
|
"content-security-policy": {
|
|
48
|
-
value: "default-src 'self' https: data: 'unsafe-inline'
|
|
48
|
+
value: "default-src 'self' https: data: 'unsafe-inline' blob:;",
|
|
49
49
|
},
|
|
50
50
|
"x-content-type-options": { value: "nosniff" },
|
|
51
51
|
"x-frame-options": { value: "sameorigin" },
|
|
@@ -18,7 +18,7 @@ function handler(event) {
|
|
|
18
18
|
value: "max-age=31536000; includeSubDomains",
|
|
19
19
|
};
|
|
20
20
|
headers["content-security-policy"] = {
|
|
21
|
-
value: "default-src 'self' https: data: 'unsafe-inline'
|
|
21
|
+
value: "default-src 'self' https: data: 'unsafe-inline' blob:;",
|
|
22
22
|
};
|
|
23
23
|
headers["x-content-type-options"] = { value: "nosniff" };
|
|
24
24
|
headers["x-frame-options"] = { value: "sameorigin" };
|