@profitlich/template-toolkit 2.4.3 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/deploy.js +88 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profitlich/template-toolkit",
3
- "version": "2.4.3",
3
+ "version": "2.5.0",
4
4
  "description": "Shared SCSS layout system, JS utilities, components and build scripts for profitlich template repos",
5
5
  "type": "module",
6
6
  "exports": {
package/scripts/deploy.js CHANGED
@@ -2,27 +2,50 @@ import ftp from 'basic-ftp';
2
2
  import dotenv from 'dotenv';
3
3
  import { glob } from 'glob';
4
4
  import path from 'path';
5
+ import fs from 'fs/promises';
5
6
  import readline from 'readline';
6
7
  import cliProgress from 'cli-progress';
7
8
 
8
9
  // Helper for making sure the upload progress bars go to 100%
9
10
  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
10
11
 
11
- export async function runDeploy(mode, uploadTasks) {
12
+ async function isNewer(client, localFile, remotePath) {
13
+ const localStat = await fs.stat(localFile);
14
+ try {
15
+ const remoteDate = await client.lastMod(remotePath);
16
+ return localStat.mtimeMs > remoteDate.getTime();
17
+ } catch {
18
+ // File doesn't exist remotely (550) or server doesn't support MDTM — upload it
19
+ return true;
20
+ }
21
+ }
22
+
23
+ async function createClient(accessOptions) {
12
24
  const client = new ftp.Client();
13
25
  client.ftp.verbose = false;
26
+ await client.access(accessOptions);
27
+ return client;
28
+ }
29
+
30
+ export async function runDeploy(mode, uploadTasks, options = {}) {
31
+ const parallel = options.parallel ?? 3;
32
+ const modeUpper = mode.toUpperCase();
33
+
34
+ const accessOptions = {
35
+ host: process.env[`FTP_HOST_${modeUpper}`],
36
+ user: process.env[`FTP_USER_${modeUpper}`],
37
+ password: process.env[`FTP_PASSWORD_${modeUpper}`],
38
+ secure: true
39
+ };
40
+
41
+ const mainClient = new ftp.Client();
42
+ mainClient.ftp.verbose = false;
14
43
  let activeProgressBar = null;
15
44
 
16
45
  try {
17
- const modeUpper = mode.toUpperCase();
18
46
  console.log(`🚀 Starting deployment for: ${modeUpper}`);
19
47
 
20
- await client.access({
21
- host: process.env[`FTP_HOST_${modeUpper}`],
22
- user: process.env[`FTP_USER_${modeUpper}`],
23
- password: process.env[`FTP_PASSWORD_${modeUpper}`],
24
- secure: true
25
- });
48
+ await mainClient.access(accessOptions);
26
49
 
27
50
  for (const task of uploadTasks) {
28
51
  console.log(`\nProcessing Task: ${task.name}`);
@@ -38,24 +61,65 @@ export async function runDeploy(mode, uploadTasks) {
38
61
  continue;
39
62
  }
40
63
 
41
- const newBar = new cliProgress.SingleBar({
64
+ // Determine which files are newer than their remote counterparts
65
+ process.stdout.write(` Checking ${files.length} files...`);
66
+ const filesToUpload = [];
67
+ for (const file of files) {
68
+ const relativeFile = path.relative(task.localBase, file);
69
+ const remotePath = path.join(task.remoteDir, relativeFile).replace(/\\/g, '/');
70
+ if (await isNewer(mainClient, file, remotePath)) {
71
+ filesToUpload.push({ file, remotePath });
72
+ }
73
+ }
74
+ process.stdout.write(` ${filesToUpload.length} changed.\n`);
75
+
76
+ if (filesToUpload.length === 0) {
77
+ console.log(' All files are up to date, skipping.');
78
+ continue;
79
+ }
80
+
81
+ const progressBar = new cliProgress.SingleBar({
42
82
  format: ' Upload |{bar}| {percentage}% {value}/{total} files {duration_formatted}',
43
83
  barCompleteChar: '█',
44
84
  barIncompleteChar: '░',
45
85
  hideCursor: true
46
86
  });
47
87
 
48
- activeProgressBar = newBar;
49
- activeProgressBar.start(files.length, 0);
88
+ activeProgressBar = progressBar;
89
+ progressBar.start(filesToUpload.length, 0);
50
90
 
51
- for (const file of files) {
52
- const relativeFile = path.relative(task.localBase, file);
53
- const remotePath = path.join(task.remoteDir, relativeFile).replace(/\\/g, '/');
54
-
55
- await client.ensureDir(path.dirname(remotePath));
56
- await client.uploadFrom(file, remotePath);
57
-
58
- activeProgressBar.increment();
91
+ if (parallel <= 1) {
92
+ for (const { file, remotePath } of filesToUpload) {
93
+ await mainClient.ensureDir(path.dirname(remotePath));
94
+ await mainClient.uploadFrom(file, remotePath);
95
+ progressBar.increment();
96
+ }
97
+ } else {
98
+ // Pre-create all required remote directories with the main client
99
+ const uniqueDirs = [...new Set(filesToUpload.map(({ remotePath }) => path.dirname(remotePath)))];
100
+ for (const dir of uniqueDirs) {
101
+ await mainClient.ensureDir(dir);
102
+ }
103
+
104
+ // Spawn parallel worker connections
105
+ const workerCount = Math.min(parallel, filesToUpload.length);
106
+ const workers = await Promise.all(
107
+ Array.from({ length: workerCount }, () => createClient(accessOptions))
108
+ );
109
+
110
+ const queue = [...filesToUpload];
111
+ await Promise.all(workers.map(async (workerClient) => {
112
+ try {
113
+ while (true) {
114
+ const item = queue.shift();
115
+ if (!item) break;
116
+ await workerClient.uploadFrom(item.file, item.remotePath);
117
+ progressBar.increment();
118
+ }
119
+ } finally {
120
+ workerClient.close();
121
+ }
122
+ }));
59
123
  }
60
124
 
61
125
  activeProgressBar.stop();
@@ -71,15 +135,16 @@ export async function runDeploy(mode, uploadTasks) {
71
135
  }
72
136
  console.error('Deployment failed:', err);
73
137
  } finally {
74
- client.close();
138
+ mainClient.close();
75
139
  }
76
140
  }
77
141
 
78
142
  /**
79
143
  * Run the deploy script.
80
144
  * @param {Array} uploadTasks - array of { name, localPattern, localBase, remoteDir, ignore? }
145
+ * @param {Object} options - { parallel: number } (default: { parallel: 3 })
81
146
  */
82
- export function run(uploadTasks) {
147
+ export function run(uploadTasks, options = {}) {
83
148
  const mode = process.argv[2];
84
149
  if (!mode || (mode !== 'staging' && mode !== 'production')) {
85
150
  console.error('Error: a mode needs to be given, either "staging" or "production"');
@@ -99,7 +164,7 @@ export function run(uploadTasks) {
99
164
  rl.close();
100
165
  if (answer.toLowerCase() === 'yes') {
101
166
  console.log('Confirmed. Starting upload...');
102
- runDeploy(mode, uploadTasks);
167
+ runDeploy(mode, uploadTasks, options);
103
168
  } else {
104
169
  console.log('❌ Deployment aborted.');
105
170
  process.exit(0);
@@ -107,6 +172,6 @@ export function run(uploadTasks) {
107
172
  });
108
173
  // In all other modes run deploy without prompt
109
174
  } else {
110
- runDeploy(mode, uploadTasks);
175
+ runDeploy(mode, uploadTasks, options);
111
176
  }
112
177
  }