ultimate-jekyll-manager 0.0.213 → 0.0.215

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.
@@ -1,15 +1,13 @@
1
1
  // Libraries
2
2
  const Manager = new (require('../build.js'));
3
3
  const logger = Manager.logger('setup');
4
- const argv = Manager.getArguments();
5
4
  const path = require('path');
6
5
  const jetpack = require('fs-jetpack');
7
6
  const version = require('wonderful-version');
8
7
  const fetch = require('wonderful-fetch');
9
- const { execute, template } = require('node-powertools');
8
+ const { execute } = require('node-powertools');
10
9
  const NPM = require('npm-api');
11
10
  const glob = require('glob').globSync;
12
- const { minimatch } = require('minimatch');
13
11
  const detectGitHubRepository = require('../gulp/tasks/utils/detect-github-repo');
14
12
  const { Octokit } = require('@octokit/rest');
15
13
  const sodium = require('libsodium-wrappers');
@@ -21,7 +19,7 @@ let config = Manager.getConfig('project');
21
19
  const rootPathPackage = Manager.getRootPath('main');
22
20
  const rootPathProject = Manager.getRootPath('project');
23
21
 
24
- // Dependency MAP
22
+ // Map of dependencies that should be installed as devDependencies
25
23
  const DEPENDENCY_MAP = {
26
24
  'gulp': 'dev',
27
25
  }
@@ -31,17 +29,15 @@ module.exports = async function (options) {
31
29
  options = options || {};
32
30
  options.checkManager = options.checkManager !== 'false';
33
31
  options.checkNode = options.checkNode !== 'false';
34
- options.checkBundler = options.checkBundler !== 'false';
35
32
  options.checkRuby = options.checkRuby !== 'false';
33
+ options.checkBundle = options.checkBundle !== 'false';
36
34
  options.checkPeerDependencies = options.checkPeerDependencies !== 'false';
37
35
  options.setupScripts = options.setupScripts !== 'false';
38
36
  options.ensureCoreFiles = options.ensureCoreFiles !== 'false';
39
37
  options.createCname = options.createCname !== 'false';
40
38
  options.fetchFirebaseAuth = options.fetchFirebaseAuth !== 'false';
41
39
  options.checkLocality = options.checkLocality !== 'false';
42
- options.updateBundle = options.updateBundle !== 'false';
43
40
  options.publishGitHubToken = options.publishGitHubToken !== 'false';
44
- options.updateGitHubPages = options.updateGitHubPages !== 'false';
45
41
  options.deduplicatePosts = options.deduplicatePosts !== 'false';
46
42
  options.migrate = options.migrate !== 'false';
47
43
 
@@ -53,139 +49,114 @@ module.exports = async function (options) {
53
49
  project.dependencies = project.dependencies || {};
54
50
  project.devDependencies = project.devDependencies || {};
55
51
 
56
- try {
57
- // Log current working directory
58
- await logCWD();
59
-
60
- // Run migrations
61
- if (options.migrate) {
62
- await migrate();
63
- }
52
+ // Log current working directory
53
+ logger.log('Current working directory:', process.cwd());
64
54
 
65
- // Detect GitHub repository early so it's available to all tasks/functions
66
- await detectGitHubRepository(logger);
55
+ // Run migrations
56
+ if (options.migrate) {
57
+ await migrate();
58
+ }
67
59
 
68
- // Ensure this package is up-to-date
69
- if (options.checkManager) {
70
- await updateManager();
71
- }
60
+ // Detect GitHub repository early so it's available to all tasks/functions
61
+ await detectGitHubRepository(logger);
72
62
 
73
- // Ensure proper node version
74
- if (options.checkNode) {
75
- await ensureNodeVersion();
76
- }
63
+ // --- Version checks ---
77
64
 
78
- // Ensure proper bundler version
79
- if (options.checkBundler) {
80
- await ensureBundlerVersion();
81
- }
65
+ // Ensure this package is up-to-date
66
+ if (options.checkManager) {
67
+ await ensureManagerVersion();
68
+ }
82
69
 
83
- // Ensure proper ruby version
84
- if (options.checkRuby) {
85
- await ensureRubyVersion();
86
- }
70
+ // Ensure proper node version
71
+ if (options.checkNode) {
72
+ await ensureNodeVersion();
73
+ }
87
74
 
88
- // Run the setup
89
- if (options.checkPeerDependencies) {
90
- await ensurePeerDependencies();
91
- }
75
+ // Ensure proper ruby version
76
+ if (options.checkRuby) {
77
+ await ensureRubyVersion();
78
+ }
92
79
 
93
- // Setup scripts
94
- if (options.setupScripts) {
95
- await setupScripts();
96
- }
80
+ // Ensure proper bundler version + install/update gems
81
+ if (options.checkBundle) {
82
+ await ensureBundle();
83
+ }
97
84
 
98
- // Copy all files from src/defaults/dist on first run
99
- // await copyDefaultDistFiles();
85
+ // Ensure peer dependencies are installed
86
+ if (options.checkPeerDependencies) {
87
+ await ensurePeerDependencies();
88
+ }
100
89
 
101
- // Ensure _config.yml exists
102
- if (options.ensureCoreFiles) {
103
- await ensureCoreFiles();
104
- }
90
+ // --- Project setup ---
105
91
 
106
- // Create CNAME
107
- if (options.createCname) {
108
- await createCname();
109
- }
92
+ // Setup scripts in package.json
93
+ if (options.setupScripts) {
94
+ setupScripts();
95
+ }
110
96
 
111
- // Fetch firebase-auth files
112
- if (options.fetchFirebaseAuth) {
113
- await fetchFirebaseAuth(options);
114
- }
97
+ // Ensure _config.yml exists
98
+ if (options.ensureCoreFiles) {
99
+ await ensureCoreFiles();
100
+ }
115
101
 
116
- // Check which locality we are using
117
- if (options.checkLocality) {
118
- await checkLocality();
119
- }
102
+ // Create CNAME file
103
+ if (options.createCname) {
104
+ createCname();
105
+ }
120
106
 
121
- // Publish GH_TOKEN as repository secret
122
- if (options.publishGitHubToken) {
123
- await publishGitHubToken();
124
- }
107
+ // Fetch firebase-auth files
108
+ if (options.fetchFirebaseAuth) {
109
+ await fetchFirebaseAuth();
110
+ }
125
111
 
126
- // Update GitHub Pages settings
127
- // if (options.updateGitHubPages) {
128
- // await updateGitHubPages(options);
129
- // }
112
+ // Warn if using local version
113
+ if (options.checkLocality) {
114
+ checkLocality();
115
+ }
130
116
 
131
- // Check which locality we are using
132
- if (options.updateBundle && !Manager.isServer()) {
133
- await updateBundle();
134
- }
117
+ // Publish GH_TOKEN as repository secret
118
+ if (options.publishGitHubToken) {
119
+ await publishGitHubToken();
120
+ }
135
121
 
136
- // Deduplicate posts (remove duplicate posts with same slug but different dates)
137
- if (options.deduplicatePosts) {
138
- await deduplicatePosts();
139
- }
140
- } catch (e) {
141
- // Throw error
142
- throw e;
122
+ // Deduplicate posts (remove duplicate posts with same slug but different dates)
123
+ if (options.deduplicatePosts) {
124
+ await deduplicatePosts();
143
125
  }
144
126
  };
145
127
 
146
- async function logCWD() {
147
- logger.log('Current working directory:', process.cwd());
148
- // logger.log('Current working directory 2:', await execute('pwd'));
149
- // logger.log('Current working directory 3:', await execute('ls -al'));
150
- }
128
+ // --- Version check functions ---
151
129
 
152
- async function updateManager() {
130
+ async function ensureManagerVersion() {
153
131
  const npm = new NPM();
154
-
155
- // Get the latest version
156
132
  const installedVersion = project.devDependencies[package.name];
157
- const latestVersion = await npm.repo(package.name)
158
- .package()
159
- .then((pkg) => {
160
- return pkg.version;
161
- }, (e) => {
162
- return '0.0.0';
163
- });
164
- const isUpToDate = version.is(installedVersion, '>=', latestVersion);
165
- const levelDifference = version.levelDifference(installedVersion, latestVersion);
166
133
 
167
- // Check if installedVersion is truthy or throw error
168
134
  if (!installedVersion) {
169
135
  throw new Error(`No installed version of ${package.name} found in devDependencies.`);
170
136
  }
171
137
 
172
- // Log
173
- logVersionCheck(package.name, installedVersion, latestVersion, isUpToDate);
174
-
175
- // Quit if local
138
+ // Skip if local
176
139
  if (installedVersion.startsWith('file:')) {
140
+ logVersionCheck(package.name, installedVersion, installedVersion, true);
177
141
  return;
178
142
  }
179
143
 
180
- // Check if we need to update
144
+ const latestVersion = await npm.repo(package.name)
145
+ .package()
146
+ .then((pkg) => pkg.version, () => '0.0.0');
147
+
148
+ const isUpToDate = version.is(installedVersion, '>=', latestVersion);
149
+
150
+ logVersionCheck(package.name, installedVersion, latestVersion, isUpToDate);
151
+
181
152
  if (!isUpToDate) {
182
- // Quit if major version difference
153
+ const levelDifference = version.levelDifference(installedVersion, latestVersion);
154
+
183
155
  if (levelDifference === 'major' && installedVersion !== 'latest') {
184
156
  return logger.error(`Major version difference detected. Please update to ${latestVersion} manually.`);
185
157
  }
186
158
 
187
- // Install the latest version
188
- await install(package.name, latestVersion);
159
+ await npmInstall(package.name, latestVersion);
189
160
  }
190
161
  }
191
162
 
@@ -194,353 +165,235 @@ async function ensureNodeVersion() {
194
165
  const requiredVersion = version.clean(package.engines.node);
195
166
  const isUpToDate = version.is(installedVersion, '>=', requiredVersion);
196
167
 
197
- // Log
198
168
  logVersionCheck('Node.js', installedVersion, requiredVersion, isUpToDate);
199
169
 
200
- // Check if we need to update
201
170
  if (!isUpToDate) {
202
171
  throw new Error(`Node version is out-of-date. Required version is ${requiredVersion}.`);
203
172
  }
204
173
  }
205
174
 
206
- async function ensureBundlerVersion() {
175
+ async function ensureRubyVersion() {
207
176
  const installedVersion = version.clean(
208
- (await execute('bundler -v', { log: false })).match(/(\d+\.\d+\.\d+)/)[0]
177
+ (await execute('ruby -v', { log: false })).match(/(\d+\.\d+\.\d+)/)[0]
209
178
  );
210
- const requiredVersion = version.clean(package.engines.bundler);
179
+ const requiredVersion = version.clean(package.engines.ruby);
211
180
  const isUpToDate = version.is(installedVersion, '>=', requiredVersion);
212
181
 
213
- // Log
214
- logVersionCheck('Bundler', installedVersion, requiredVersion, isUpToDate);
182
+ logVersionCheck('Ruby', installedVersion, requiredVersion, isUpToDate);
215
183
 
216
- // Check if we need to update
217
184
  if (!isUpToDate) {
218
- logger.log(`Bundler is out-of-date. Installing version ${requiredVersion}...`);
219
- await execute(`gem install bundler -v ${requiredVersion}`, { log: true });
185
+ throw new Error(`Ruby version is out-of-date. Required version is ${requiredVersion}.`);
220
186
  }
221
187
  }
222
188
 
223
- async function ensureRubyVersion() {
189
+ async function ensureBundle() {
190
+ // Check bundler gem version
224
191
  const installedVersion = version.clean(
225
- (await execute('ruby -v', { log: false })).match(/(\d+\.\d+\.\d+)/)[0]
192
+ (await execute('bundler -v', { log: false })).match(/(\d+\.\d+\.\d+)/)[0]
226
193
  );
227
- const requiredVersion = version.clean(package.engines.ruby);
194
+ const requiredVersion = version.clean(package.engines.bundler);
228
195
  const isUpToDate = version.is(installedVersion, '>=', requiredVersion);
229
196
 
230
- // Log
231
- logVersionCheck('Ruby', installedVersion, requiredVersion, isUpToDate);
197
+ logVersionCheck('Bundler', installedVersion, requiredVersion, isUpToDate);
232
198
 
233
- // Check if we need to update
199
+ // Install bundler gem + update Gemfile.lock if needed
234
200
  if (!isUpToDate) {
235
- throw new Error(`Ruby version is out-of-date. Required version is ${requiredVersion}.`);
201
+ logger.log(`Bundler is out-of-date. Installing version ${requiredVersion}...`);
202
+ await execute(`gem install bundler -v ${requiredVersion}`, { log: true });
203
+ await execute(`bundle update --bundler`, { log: true });
204
+ }
205
+
206
+ // Skip bundle install/update on server
207
+ if (Manager.isServer()) {
208
+ return;
236
209
  }
210
+
211
+ // Install and update gems
212
+ logger.log('Running bundle install...');
213
+ await execute('bundle install', { log: true });
214
+
215
+ logger.log('Running bundle update...');
216
+ await execute('bundle update --all', { log: true });
237
217
  }
238
218
 
239
219
  async function ensurePeerDependencies() {
240
220
  const requiredPeerDependencies = package.peerDependencies || {};
241
221
 
242
- // Loop through and make sure project has AT LEAST the required version
243
222
  for (let [dependency, ver] of Object.entries(requiredPeerDependencies)) {
244
223
  const projectDependencyVersion = version.clean(project?.dependencies?.[dependency] || project?.devDependencies?.[dependency]);
245
224
  const location = DEPENDENCY_MAP[dependency] === 'dev' ? '--save-dev' : '';
246
- const isUpToDate = version.is(projectDependencyVersion, '>=', ver);
247
225
 
248
- // Clean version if needed
249
226
  ver = version.clean(ver);
250
227
 
251
- // Log
252
- // logger.log('Checking peer dep:', dependency, '-->', projectDependencyVersion, '>=', ver);
228
+ const isUpToDate = version.is(projectDependencyVersion, '>=', ver);
253
229
  logVersionCheck(dependency, projectDependencyVersion, ver, isUpToDate);
254
230
 
255
- // Install if not found
256
- if (
257
- // Not found
258
- !projectDependencyVersion
259
- // Not the right version
260
- || !isUpToDate
261
- ) {
262
- await install(dependency, ver, location);
231
+ if (!projectDependencyVersion || !isUpToDate) {
232
+ await npmInstall(dependency, ver, location);
263
233
  }
264
234
  }
265
235
  }
266
236
 
237
+ // --- Project setup functions ---
238
+
267
239
  function setupScripts() {
268
- // Setup the scripts
269
240
  project.scripts = project.scripts || {};
270
241
 
271
- // Setup the scripts
272
242
  Object.keys(package.projectScripts).forEach((key) => {
273
243
  project.scripts[key] = package.projectScripts[key];
274
244
  });
275
245
 
276
- // Save the project
277
246
  jetpack.write(path.join(process.cwd(), 'package.json'), project);
278
247
  }
279
248
 
280
249
  async function ensureCoreFiles() {
281
- // Ensure src/_config.yml exists
282
- if (!jetpack.exists('src/_config.yml')) {
283
- // Log
284
- logger.log('No src/_config.yml found. Creating default config file...');
285
-
286
- // Copy default _config.yml
287
- const sourcePath = path.join(rootPathPackage, 'dist/defaults/src/_config.yml');
288
- const targetPath = path.join(rootPathProject, 'src/_config.yml');
289
-
290
- jetpack.copy(sourcePath, targetPath);
291
- logger.log(`Copied default _config.yml to src/_config.yml`);
292
-
293
- // Inject new config into config variable
294
- config = Manager.getConfig('project');
295
-
296
- // Run gulp defaults task since this is likely the first run
297
- await execute('UJ_BUILD_MODE=true npm run gulp -- defaults', { log: true });
298
- }
299
- }
300
-
301
- function checkLocality() {
302
- const installedVersion = project.devDependencies[package.name];
303
-
304
- // Check if installedVersion is truthy or throw error
305
- if (!installedVersion) {
306
- throw new Error(`No installed version of ${package.name} found in devDependencies.`);
307
- }
308
-
309
- // Log if local
310
- if (installedVersion.startsWith('file:')) {
311
- logger.warn(`⚠️⚠️⚠️ You are using the local version of ${package.name}. This WILL NOT WORK when published. ⚠️⚠️⚠️`);
250
+ if (jetpack.exists('src/_config.yml')) {
251
+ return;
312
252
  }
313
- }
314
-
315
- async function updateBundle() {
316
- // Log
317
- logger.log('Running bundle install...');
318
- await execute('bundle install', { log: true })
319
-
320
- // Log
321
- logger.log('Running bundle update...');
322
- await execute('bundle update --all', { log: true })
323
- }
324
253
 
325
- function install(package, ver, location) {
326
- // Default to latest
327
- ver || 'latest';
254
+ logger.log('No src/_config.yml found. Creating default config file...');
328
255
 
329
- // Clean version if needed
330
- ver = ver === 'latest' ? ver : version.clean(ver);
331
-
332
- // Build the command
333
- let command = `npm install ${package}@${ver} ${location || '--save'}`;
334
-
335
- // Log
336
- logger.log('Installing:', command);
256
+ const sourcePath = path.join(rootPathPackage, 'dist/defaults/src/_config.yml');
257
+ const targetPath = path.join(rootPathProject, 'src/_config.yml');
337
258
 
338
- // Execute
339
- return execute(command, { log: true })
340
- .then(async () => {
341
- // Read new project
342
- const projectUpdated = jetpack.read(path.join(process.cwd(), 'package.json'), 'json');
259
+ jetpack.copy(sourcePath, targetPath);
260
+ logger.log(`Copied default _config.yml to src/_config.yml`);
343
261
 
344
- // Log
345
- logger.log('Installed:', package, ver);
262
+ // Inject new config into config variable
263
+ config = Manager.getConfig('project');
346
264
 
347
- // Update package object
348
- project.dependencies = projectUpdated.dependencies;
349
- project.devDependencies = projectUpdated.devDependencies;
350
- });
265
+ // Run gulp defaults task since this is likely the first run
266
+ await execute('UJ_BUILD_MODE=true npm run gulp -- defaults', { log: true });
351
267
  }
352
268
 
353
- // Create CNAME
354
- async function createCname() {
355
- // Get the CNAME
269
+ function createCname() {
356
270
  const url = config.url || 'https://ultimate-jekyll.itwcreativeworks.com';
357
- const host = new URL(url).host
271
+ const host = new URL(url).host;
358
272
 
359
- // Write to file
360
273
  jetpack.write('dist/CNAME', host);
361
-
362
- // Log
363
274
  logger.log('Created CNAME');
364
275
  }
365
276
 
366
- // Fetch firebase-auth files
367
277
  async function fetchFirebaseAuth() {
368
278
  const app = config.web_manager.firebase.app.config || {};
369
- const base = `https://${app.projectId}.firebaseapp.com`;
370
279
 
371
- // Throw error if no project ID
372
280
  if (!app.projectId) {
373
- // if (options.skipFirebaseIdCheck) {
374
- // return;
375
- // }
376
- // throw new Error('No Firebase project ID found in config.web_manager.firebase.app.config.projectId');
377
- logger.warn('⚠️ Skipping fetchFirebaseAuth due to missing Firebase project ID.');
281
+ logger.warn('Skipping fetchFirebaseAuth due to missing Firebase project ID.');
378
282
  return;
379
283
  }
380
284
 
285
+ const base = `https://${app.projectId}.firebaseapp.com`;
286
+ const output = './dist';
287
+
381
288
  const files = [
382
- {
383
- remote: '__/auth/handler',
384
- filename: 'handler.html',
385
- },
386
- {
387
- remote: '__/auth/handler.js',
388
- },
389
- {
390
- remote: '__/auth/experiments.js',
391
- },
289
+ { remote: '__/auth/handler', filename: 'handler.html' },
290
+ { remote: '__/auth/handler.js' },
291
+ { remote: '__/auth/experiments.js' },
392
292
  {
393
293
  remote: '__/auth/iframe',
394
294
  filename: 'iframe.html',
395
- replace: (content) => {
396
- return content.replace('src="iframe.js"', 'src="iframe.js?cb={{ site.uj.cache_breaker }}"');
397
- }
398
- },
399
- {
400
- remote: '__/auth/iframe.js',
295
+ replace: (content) => content.replace('src="iframe.js"', 'src="iframe.js?cb={{ site.uj.cache_breaker }}"'),
401
296
  },
402
- {
403
- remote: '__/firebase/init.json',
404
- }
405
- ]
406
- const promises = [];
407
- const output = './dist';
297
+ { remote: '__/auth/iframe.js' },
298
+ { remote: '__/firebase/init.json' },
299
+ ];
408
300
 
409
- // Loop through files
410
- files.forEach((file) => {
411
- // Get the remote URL
412
- const url = `${base}/${file.remote}`;
301
+ logger.log('Fetching firebase-auth files...');
413
302
 
414
- // Get the local path
303
+ const promises = files.map((file) => {
304
+ const url = `${base}/${file.remote}`;
415
305
  const fileName = file.filename
416
306
  ? path.basename(file.filename)
417
307
  : path.basename(file.remote);
418
308
  const filePath = path.join(path.dirname(file.remote), fileName);
419
309
  const finalPath = path.join(output, filePath);
420
310
 
421
- // Push to promises
422
- promises.push(
423
- fetch(url, {
424
- response: 'text',
425
- tries: 3,
426
- // log: true,
427
- })
311
+ return fetch(url, { response: 'text', tries: 3 })
428
312
  .then((r) => {
429
- // Apply replace function if it exists
430
- if (file.replace && typeof file.replace === 'function') {
313
+ if (file.replace) {
431
314
  r = file.replace(r);
432
315
  }
433
316
 
434
- // Log success for this file
435
317
  logger.log(`Fetched: ${file.remote}`);
436
318
 
437
- // Write to file
438
319
  jetpack.write(finalPath,
439
320
  '---\n'
440
321
  + `permalink: /${file.remote}\n`
441
322
  + '---\n'
442
323
  + '\n'
443
324
  + r
444
- )
325
+ );
445
326
  })
446
327
  .catch((error) => {
447
- // Only log the filename that failed, not the error content
448
- logger.error(`❌ Failed to fetch: ${file.remote}`);
328
+ logger.error(`Failed to fetch: ${file.remote}`);
449
329
  logger.error(` URL: ${url}`);
450
330
 
451
- // Check if it's a specific type of error (not HTML response)
452
- // Use regex to check case-insensitive for HTML content
453
331
  const htmlPattern = /<!doctype|<html|<head|<body/i;
454
332
  if (error.message && !htmlPattern.test(error.message)) {
455
333
  logger.error(` Error: ${error.message}`);
456
334
  }
457
335
 
458
- // Re-throw to make Promise.all fail
459
336
  throw new Error(`Failed to fetch Firebase auth file: ${file.remote}`);
460
- })
461
- );
337
+ });
462
338
  });
463
339
 
464
- // Log
465
- logger.log('Fetching firebase-auth files...');
466
-
467
340
  try {
468
- // Await all promises
469
341
  await Promise.all(promises);
470
-
471
- // Log success
472
- logger.log('✅ Fetched firebase-auth files');
342
+ logger.log('Fetched firebase-auth files');
473
343
  } catch (error) {
474
- // Check if we should skip Firebase auth errors
475
344
  if (process.env.UJ_SKIP_FIREBASE_AUTH_ERRORS === 'true') {
476
- logger.warn('⚠️ Failed to fetch some Firebase auth files, but continuing due to UJ_SKIP_FIREBASE_AUTH_ERRORS=true');
345
+ logger.warn('Failed to fetch some Firebase auth files, but continuing due to UJ_SKIP_FIREBASE_AUTH_ERRORS=true');
477
346
  return;
478
347
  }
479
348
 
480
- // Error already logged above, just throw to stop execution
481
349
  throw new Error('Failed to fetch one or more Firebase auth files. Please check your Firebase project configuration.');
482
350
  }
483
351
  }
484
352
 
485
- function logVersionCheck(name, installedVersion, latestVersion, isUpToDate) {
486
- // Quit if local
487
- if (installedVersion.startsWith('file:')) {
488
- isUpToDate = true;
353
+ function checkLocality() {
354
+ const installedVersion = project.devDependencies[package.name];
355
+
356
+ if (!installedVersion) {
357
+ throw new Error(`No installed version of ${package.name} found in devDependencies.`);
489
358
  }
490
359
 
491
- // Log
492
- logger.log(`Checking if ${name} is up to date (${logger.format.bold(installedVersion)} >= ${logger.format.bold(latestVersion)}): ${isUpToDate ? logger.format.green('Yes') : logger.format.red('No')}`);
360
+ if (installedVersion.startsWith('file:')) {
361
+ logger.warn(`You are using the local version of ${package.name}. This WILL NOT WORK when published.`);
362
+ }
493
363
  }
494
364
 
495
- // Publish GH_TOKEN as repository secret
496
365
  async function publishGitHubToken() {
497
- // Check if GH_TOKEN is available
498
366
  if (!process.env.GH_TOKEN) {
499
- logger.warn('⚠️ GH_TOKEN not found in environment variables. Skipping secret publication.');
367
+ logger.warn('GH_TOKEN not found in environment variables. Skipping secret publication.');
500
368
  return;
501
369
  }
502
370
 
503
- // Check if GITHUB_REPOSITORY is available
504
371
  if (!process.env.GITHUB_REPOSITORY) {
505
- logger.warn('⚠️ GITHUB_REPOSITORY not detected. Skipping secret publication.');
372
+ logger.warn('GITHUB_REPOSITORY not detected. Skipping secret publication.');
506
373
  return;
507
374
  }
508
375
 
509
- // Quit if in build mode
510
376
  if (Manager.isBuildMode()) {
511
- logger.log('⚠️ Skipping GH_TOKEN publication in build mode.');
377
+ logger.log('Skipping GH_TOKEN publication in build mode.');
512
378
  return;
513
379
  }
514
380
 
515
381
  try {
516
- // Parse owner and repo
517
382
  const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
518
383
 
519
- // Initialize Octokit
520
- const octokit = new Octokit({
521
- auth: process.env.GH_TOKEN,
522
- });
384
+ const octokit = new Octokit({ auth: process.env.GH_TOKEN });
523
385
 
524
- logger.log(`🔐 Publishing GH_TOKEN as repository secret for ${owner}/${repo}...`);
386
+ logger.log(`Publishing GH_TOKEN as repository secret for ${owner}/${repo}...`);
525
387
 
526
- // Initialize sodium
527
388
  await sodium.ready;
528
389
 
529
- // Get repository public key for encrypting secrets
530
- const { data: publicKeyData } = await octokit.actions.getRepoPublicKey({
531
- owner,
532
- repo,
533
- });
390
+ const { data: publicKeyData } = await octokit.actions.getRepoPublicKey({ owner, repo });
534
391
 
535
- // Convert secret to Uint8Array
536
392
  const secretBytes = Buffer.from(process.env.GH_TOKEN);
537
393
  const keyBytes = Buffer.from(publicKeyData.key, 'base64');
538
-
539
- // Encrypt the secret using libsodium
540
394
  const encryptedBytes = sodium.crypto_box_seal(secretBytes, keyBytes);
541
395
  const encryptedValue = Buffer.from(encryptedBytes).toString('base64');
542
396
 
543
- // Create or update the repository secret
544
397
  await octokit.actions.createOrUpdateRepoSecret({
545
398
  owner,
546
399
  repo,
@@ -549,139 +402,16 @@ async function publishGitHubToken() {
549
402
  key_id: publicKeyData.key_id,
550
403
  });
551
404
 
552
- logger.log(`✅ Successfully published GH_TOKEN as repository secret`);
553
- } catch (error) {
554
- logger.error(`❌ Failed to publish GH_TOKEN as repository secret: ${error.message}`);
555
- // Don't throw - this is not critical for setup to continue
556
- }
557
- }
558
-
559
- // Update GitHub Pages settings
560
- async function updateGitHubPages(options) {
561
- options = options || {};
562
-
563
- // Check if GH_TOKEN is available
564
- if (!process.env.GH_TOKEN) {
565
- logger.warn('⚠️ GH_TOKEN not found in environment variables. Skipping GitHub Pages update.');
566
- return;
567
- }
568
-
569
- // Check if GITHUB_REPOSITORY is available
570
- if (!process.env.GITHUB_REPOSITORY) {
571
- logger.warn('⚠️ GITHUB_REPOSITORY not detected. Skipping GitHub Pages update.');
572
- return;
573
- }
574
-
575
- // Quit if in build mode
576
- if (Manager.isBuildMode()) {
577
- logger.log('⚠️ Skipping GitHub Pages update in build mode.');
578
- return;
579
- }
580
-
581
- try {
582
- // Parse owner and repo
583
- const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
584
-
585
- // Initialize Octokit
586
- const octokit = new Octokit({
587
- auth: process.env.GH_TOKEN,
588
- });
589
-
590
- logger.log(`📄 Configuring GitHub Pages for ${owner}/${repo}...`);
591
-
592
- // Get current repository info to check if Pages is already enabled
593
- let pagesInfo;
594
- try {
595
- const { data } = await octokit.repos.getPages({
596
- owner,
597
- repo,
598
- });
599
- pagesInfo = data;
600
- logger.log('GitHub Pages already enabled, updating configuration...');
601
- } catch (error) {
602
- if (error.status === 404) {
603
- logger.log('GitHub Pages not yet enabled, creating Pages site...');
604
- pagesInfo = null;
605
- } else {
606
- throw error;
607
- }
608
- }
609
-
610
- // Determine the source configuration
611
- const sourceConfig = options.source || { branch: 'gh-pages', path: '/' };
612
-
613
- // If custom domain is provided in config, use it
614
- const customDomain = options.customDomain || (config.url ? new URL(config.url).host : null);
615
-
616
- // Build options for API call
617
- const pagesOptions = {
618
- owner,
619
- repo,
620
- source: sourceConfig,
621
- };
622
-
623
- // Add custom domain if available and not github.io domain
624
- if (customDomain && !customDomain.includes('github.io')) {
625
- pagesOptions.cname = customDomain;
626
- }
627
-
628
- // Add HTTPS enforcement (recommended)
629
- if (options.httpsEnforced !== false) {
630
- pagesOptions.https_enforced = true;
631
- }
632
-
633
- // Create or update Pages configuration
634
- if (!pagesInfo) {
635
- // Create new Pages site
636
- const { data } = await octokit.repos.createPagesSite(pagesOptions);
637
- logger.log(`✅ GitHub Pages enabled successfully!`);
638
- logger.log(` URL: ${data.html_url}`);
639
- if (data.cname) {
640
- logger.log(` Custom domain: ${data.cname}`);
641
- }
642
- } else {
643
- // Update existing Pages site
644
- const updateOptions = {
645
- owner,
646
- repo,
647
- };
648
-
649
- // Only include fields that can be updated
650
- if (customDomain && !customDomain.includes('github.io')) {
651
- updateOptions.cname = customDomain;
652
- }
653
-
654
- if (options.httpsEnforced !== false) {
655
- updateOptions.https_enforced = true;
656
- }
657
-
658
- // Update source if different
659
- if (JSON.stringify(pagesInfo.source) !== JSON.stringify(sourceConfig)) {
660
- updateOptions.source = sourceConfig;
661
- }
662
-
663
- const { data } = await octokit.repos.updateInformationAboutPagesSite(updateOptions);
664
- logger.log(`✅ GitHub Pages configuration updated successfully!`);
665
- logger.log(` URL: ${data.html_url}`);
666
- if (data.cname) {
667
- logger.log(` Custom domain: ${data.cname}`);
668
- }
669
- }
670
-
405
+ logger.log(`Successfully published GH_TOKEN as repository secret`);
671
406
  } catch (error) {
672
- logger.error(`❌ Failed to update GitHub Pages: ${error.message}`);
673
- // Don't throw - this is not critical for setup to continue
407
+ logger.error(`Failed to publish GH_TOKEN as repository secret: ${error.message}`);
674
408
  }
675
409
  }
676
410
 
677
- // Deduplicate posts - removes duplicate posts with same slug but different dates
678
- // Keeps the ORIGINAL (oldest) post and removes newer duplicates
679
411
  async function deduplicatePosts() {
680
- logger.log('🔍 Checking for duplicate posts...');
412
+ logger.log('Checking for duplicate posts...');
681
413
 
682
- // Find all post files in src/_posts
683
- const postsGlob = 'src/_posts/**/*.{md,markdown,html}';
684
- const postFiles = glob(postsGlob, { nodir: true });
414
+ const postFiles = glob('src/_posts/**/*.{md,markdown,html}', { nodir: true });
685
415
 
686
416
  if (postFiles.length === 0) {
687
417
  logger.log('No posts found');
@@ -695,8 +425,6 @@ async function deduplicatePosts() {
695
425
 
696
426
  for (const filePath of postFiles) {
697
427
  const filename = path.basename(filePath);
698
-
699
- // Jekyll post format: YYYY-MM-DD-slug.ext
700
428
  const match = filename.match(/^(\d{4}-\d{2}-\d{2})-(.+)\.(md|markdown|html)$/);
701
429
 
702
430
  if (!match) {
@@ -704,20 +432,12 @@ async function deduplicatePosts() {
704
432
  }
705
433
 
706
434
  const [, dateStr, slug, ext] = match;
707
- const date = new Date(dateStr);
708
435
 
709
436
  if (!postsBySlug[slug]) {
710
437
  postsBySlug[slug] = [];
711
438
  }
712
439
 
713
- postsBySlug[slug].push({
714
- filePath,
715
- filename,
716
- date,
717
- dateStr,
718
- slug,
719
- ext,
720
- });
440
+ postsBySlug[slug].push({ filePath, filename, date: new Date(dateStr), dateStr, slug, ext });
721
441
  }
722
442
 
723
443
  // Find duplicates and keep only the ORIGINAL (oldest)
@@ -732,25 +452,22 @@ async function deduplicatePosts() {
732
452
  // Sort by date ascending (oldest first)
733
453
  posts.sort((a, b) => a.date - b.date);
734
454
 
735
- // Keep the oldest (original), mark the rest for removal
736
455
  const [original, ...newer] = posts;
737
456
 
738
457
  logger.log(`Found ${posts.length} posts with slug "${slug}":`);
739
- logger.log(` Keeping original: ${original.filename} (${original.dateStr})`);
458
+ logger.log(` Keeping original: ${original.filename} (${original.dateStr})`);
740
459
 
741
460
  for (const post of newer) {
742
- logger.log(` Removing duplicate: ${post.filename} (${post.dateStr})`);
461
+ logger.log(` Removing duplicate: ${post.filename} (${post.dateStr})`);
743
462
 
744
- // Remove the duplicate post file
745
463
  try {
746
464
  jetpack.remove(post.filePath);
747
465
  removedCount++;
748
466
 
749
- // Also remove the associated images folder (src/assets/images/blog/post-SLUG)
750
467
  const imageFolder = `src/assets/images/blog/post-${post.slug}`;
751
468
  if (jetpack.exists(imageFolder)) {
752
469
  jetpack.remove(imageFolder);
753
- logger.log(` 🗑️ Removed image folder: ${imageFolder}`);
470
+ logger.log(` Removed image folder: ${imageFolder}`);
754
471
  }
755
472
 
756
473
  duplicates.push({
@@ -765,11 +482,9 @@ async function deduplicatePosts() {
765
482
  }
766
483
  }
767
484
 
768
- // Log summary
769
485
  if (removedCount > 0) {
770
- logger.log(`✅ Removed ${removedCount} duplicate post(s)`);
486
+ logger.log(`Removed ${removedCount} duplicate post(s)`);
771
487
 
772
- // Save report
773
488
  const reportDir = path.join(rootPathProject, '.temp/deduplicate');
774
489
  jetpack.dir(reportDir);
775
490
 
@@ -784,30 +499,27 @@ async function deduplicatePosts() {
784
499
 
785
500
  logger.log(`Report saved to: ${reportPath}`);
786
501
  } else {
787
- logger.log('No duplicate posts found');
502
+ logger.log('No duplicate posts found');
788
503
  }
789
504
  }
790
505
 
791
- // Run migrations based on installed version
506
+ // --- Migration functions ---
507
+
792
508
  async function migrate() {
793
509
  const installedVersion = project.devDependencies[package.name] || '0.0.0';
794
510
 
795
- // Skip if using local version
796
511
  if (installedVersion.startsWith('file:')) {
797
512
  return;
798
513
  }
799
514
 
800
- // Migrate hooks to nested structure (introduced in 0.0.185)
801
515
  if (version.is(installedVersion, '<=', '1.0.0')) {
802
516
  await migrateHooksToNestedStructure();
803
517
  }
804
518
  }
805
519
 
806
- // Migrate old hook files to new nested structure
807
520
  async function migrateHooksToNestedStructure() {
808
521
  const hooksDir = path.join(rootPathProject, 'hooks');
809
522
 
810
- // Map of old file names to new paths
811
523
  const migrations = [
812
524
  { old: 'build:post.js', new: 'build/post.js' },
813
525
  { old: 'build:pre.js', new: 'build/pre.js' },
@@ -820,23 +532,49 @@ async function migrateHooksToNestedStructure() {
820
532
  const oldPath = path.join(hooksDir, migration.old);
821
533
  const newPath = path.join(hooksDir, migration.new);
822
534
 
823
- // Check if old file exists
824
535
  if (!jetpack.exists(oldPath)) {
825
536
  continue;
826
537
  }
827
538
 
828
- // Check if new file already exists
829
539
  if (jetpack.exists(newPath)) {
830
- logger.warn(`⚠️ Migrate ${migration.old}: ${migration.new} already exists`);
540
+ logger.warn(`Migrate ${migration.old}: ${migration.new} already exists`);
831
541
  }
832
542
 
833
- // Move the file
834
543
  jetpack.move(oldPath, newPath, { overwrite: true });
835
- logger.log(`✅ Migrated hook: ${migration.old} ${migration.new}`);
544
+ logger.log(`Migrated hook: ${migration.old} -> ${migration.new}`);
836
545
  migratedCount++;
837
546
  }
838
547
 
839
548
  if (migratedCount > 0) {
840
- logger.log(`✅ Migrated ${migratedCount} hook file(s) to new nested structure`);
549
+ logger.log(`Migrated ${migratedCount} hook file(s) to new nested structure`);
550
+ }
551
+ }
552
+
553
+ // --- Utility functions ---
554
+
555
+ function npmInstall(pkg, ver, location) {
556
+ ver = ver || 'latest';
557
+ ver = ver === 'latest' ? ver : version.clean(ver);
558
+
559
+ const command = `npm install ${pkg}@${ver} ${location || '--save'}`;
560
+
561
+ logger.log('Installing:', command);
562
+
563
+ return execute(command, { log: true })
564
+ .then(() => {
565
+ const projectUpdated = jetpack.read(path.join(process.cwd(), 'package.json'), 'json');
566
+
567
+ logger.log('Installed:', pkg, ver);
568
+
569
+ project.dependencies = projectUpdated.dependencies;
570
+ project.devDependencies = projectUpdated.devDependencies;
571
+ });
572
+ }
573
+
574
+ function logVersionCheck(name, installedVersion, latestVersion, isUpToDate) {
575
+ if (installedVersion.startsWith('file:')) {
576
+ isUpToDate = true;
841
577
  }
578
+
579
+ logger.log(`Checking if ${name} is up to date (${logger.format.bold(installedVersion)} >= ${logger.format.bold(latestVersion)}): ${isUpToDate ? logger.format.green('Yes') : logger.format.red('No')}`);
842
580
  }
@@ -85,3 +85,25 @@ exclude:
85
85
  include:
86
86
  - .well-known
87
87
  - __
88
+
89
+ # Jekyll collections
90
+ collections:
91
+ team:
92
+ title: "Team Members"
93
+ output: true
94
+ updates:
95
+ title: "Updates"
96
+ output: true
97
+
98
+ # Jekyll defaults
99
+ defaults:
100
+ - scope:
101
+ path: ""
102
+ type: "team"
103
+ values:
104
+ permalink: "/team/:title"
105
+ - scope:
106
+ path: ""
107
+ type: "updates"
108
+ values:
109
+ permalink: "/updates/:title"
@@ -243,25 +243,3 @@ pagination:
243
243
  title: ":title - page :num"
244
244
  sort_field: "date"
245
245
  sort_reverse: true
246
-
247
- # Jekyll collections
248
- collections:
249
- team:
250
- title: "Team Members"
251
- output: true
252
- updates:
253
- title: "Updates"
254
- output: true
255
-
256
- # Jekyll defaults
257
- defaults:
258
- - scope:
259
- path: ""
260
- type: "team"
261
- values:
262
- permalink: "/team/:title"
263
- - scope:
264
- path: ""
265
- type: "updates"
266
- values:
267
- permalink: "/updates/:title"
@@ -7,6 +7,7 @@ const glob = require('glob').globSync;
7
7
  const path = require('path');
8
8
  const { execute, template } = require('node-powertools');
9
9
  const jetpack = require('fs-jetpack');
10
+ const mergeJekyllConfigs = require('./utils/merge-jekyll-configs');
10
11
 
11
12
  // Templates
12
13
  const JSONP_TEMPLATE = `
@@ -79,6 +80,14 @@ async function jekyll(complete) {
79
80
  // Run buildpre hook
80
81
  await hook('build/pre', index);
81
82
 
83
+ // Merge UJM collections/defaults with project collections/defaults
84
+ const mergedConfigPath = mergeJekyllConfigs(
85
+ `./node_modules/${package.name}/dist/config/_config_default.yml`,
86
+ 'dist/_config.yml',
87
+ '.temp/_config_collections.yml',
88
+ logger,
89
+ );
90
+
82
91
  // Build Jekyll
83
92
  const command = [
84
93
  // Enable Ruby YJIT for faster builds
@@ -95,13 +104,15 @@ async function jekyll(complete) {
95
104
  `./node_modules/${package.name}/dist/config/_config_default.yml`,
96
105
  // This is the user's project config file
97
106
  'dist/_config.yml',
107
+ // Merged collections/defaults (deep merge of UJM + project)
108
+ mergedConfigPath || '',
98
109
  // Add browsersync config IF BUILD_MODE is not true
99
110
  Manager.isBuildMode() ? '' : '.temp/_config_browsersync.yml',
100
111
  // Add development config IF BUILD_MODE is not true
101
112
  Manager.isBuildMode() ? '' : `./node_modules/${package.name}/dist/config/_config_development.yml`,
102
113
  // Add project-level dev config IF it exists and BUILD_MODE is not true
103
114
  (!Manager.isBuildMode() && jetpack.exists('dist/_config.dev.yml')) ? 'dist/_config.dev.yml' : '',
104
- ].join(','),
115
+ ].filter(Boolean).join(','),
105
116
  '--incremental',
106
117
  (Manager.isBuildMode() || argv.profile) ? ' --profile' : '',
107
118
  // Limit posts in development for faster builds (use --all-posts to disable)
@@ -0,0 +1,71 @@
1
+ // Libraries
2
+ const jetpack = require('fs-jetpack');
3
+ const yaml = require('js-yaml');
4
+
5
+ /**
6
+ * Merges Jekyll collections and defaults from UJM's config with the
7
+ * consuming project's config. Jekyll's --config does a shallow merge
8
+ * of top-level keys, so a project defining its own `collections` would
9
+ * completely override UJM's. This performs a deep merge so both coexist.
10
+ */
11
+ function mergeJekyllConfigs(ujmConfigPath, projectConfigPath, outputPath, logger) {
12
+ // Read configs
13
+ const ujmContent = jetpack.read(ujmConfigPath);
14
+ if (!ujmContent) {
15
+ return null;
16
+ }
17
+
18
+ const ujmConfig = yaml.load(ujmContent) || {};
19
+ const projectContent = jetpack.read(projectConfigPath);
20
+ const projectConfig = projectContent ? (yaml.load(projectContent) || {}) : {};
21
+
22
+ // Extract collections and defaults from both
23
+ const ujmCollections = ujmConfig.collections || {};
24
+ const ujmDefaults = ujmConfig.defaults || [];
25
+ const projectCollections = projectConfig.collections || {};
26
+ const projectDefaults = projectConfig.defaults || [];
27
+
28
+ // Nothing to merge
29
+ if (!Object.keys(ujmCollections).length && !ujmDefaults.length) {
30
+ return null;
31
+ }
32
+
33
+ const merged = {};
34
+
35
+ // Merge collections (object spread: UJM base + project overrides/additions)
36
+ if (Object.keys(ujmCollections).length || Object.keys(projectCollections).length) {
37
+ merged.collections = { ...ujmCollections, ...projectCollections };
38
+ }
39
+
40
+ // Merge defaults (concat + dedup by scope key, project wins on conflict)
41
+ if (ujmDefaults.length || projectDefaults.length) {
42
+ const defaultsMap = new Map();
43
+
44
+ // UJM defaults first (base)
45
+ for (const entry of ujmDefaults) {
46
+ defaultsMap.set(getDefaultsKey(entry), entry);
47
+ }
48
+
49
+ // Project defaults override for same scope
50
+ for (const entry of projectDefaults) {
51
+ defaultsMap.set(getDefaultsKey(entry), entry);
52
+ }
53
+
54
+ merged.defaults = Array.from(defaultsMap.values());
55
+ }
56
+
57
+ // Write merged config
58
+ const content = `# Auto-generated merged config for collections and defaults\n# DO NOT EDIT - this file is regenerated on every build\n\n${yaml.dump(merged, { lineWidth: -1, noRefs: true })}`;
59
+
60
+ jetpack.write(outputPath, content);
61
+ logger.log(`Merged collections config written to ${outputPath}`);
62
+
63
+ return outputPath;
64
+ }
65
+
66
+ function getDefaultsKey(entry) {
67
+ const scope = entry.scope || {};
68
+ return `${scope.path || ''}::${scope.type || ''}`;
69
+ }
70
+
71
+ module.exports = mergeJekyllConfigs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "0.0.213",
3
+ "version": "0.0.215",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {