@haptiq/kit 0.5.0 → 0.7.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.
- package/README.md +26 -59
- package/bin/haptiq.kit.js +19 -2
- package/lib/ship.js +119 -6
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -52,71 +52,38 @@ kit js --skip <name> # skip a named config (multi-config mode)
|
|
|
52
52
|
|
|
53
53
|
**Defaults:** reads `src/**/*.js`, writes to `js/bundle.js`.
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
---
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```js
|
|
60
|
-
module.exports = {
|
|
61
|
-
css: {
|
|
62
|
-
src: 'assets/**/*.{scss,sass,css}', // default: 'src/**/*.{scss,sass,css}'
|
|
63
|
-
dest: 'public/css', // default: 'css'
|
|
64
|
-
sass: {
|
|
65
|
-
style: 'expanded',
|
|
66
|
-
includePaths: ['node_modules'],
|
|
67
|
-
},
|
|
68
|
-
lightning: {
|
|
69
|
-
targets: { chrome: 90, firefox: 88, safari: 14 },
|
|
70
|
-
minify: true,
|
|
71
|
-
sourceMap: true,
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
// Combine mode — all files into one output
|
|
76
|
-
js: {
|
|
77
|
-
src: 'assets/**/*.js', // default: 'src/**/*.js'
|
|
78
|
-
dest: 'public/js/bundle.js', // default: 'js/bundle.js'
|
|
79
|
-
terser: {
|
|
80
|
-
compress: {
|
|
81
|
-
drop_console: true,
|
|
82
|
-
drop_debugger: true,
|
|
83
|
-
},
|
|
84
|
-
format: {
|
|
85
|
-
comments: false,
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
```
|
|
57
|
+
### `kit ship [target]`
|
|
91
58
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Use `js.configs` to define several named configurations:
|
|
95
|
-
|
|
96
|
-
```js
|
|
97
|
-
module.exports = {
|
|
98
|
-
js: {
|
|
99
|
-
configs: {
|
|
100
|
-
'app': {
|
|
101
|
-
src: 'src/app/**/*.js',
|
|
102
|
-
dest: 'public/js/app.bundle.js',
|
|
103
|
-
},
|
|
104
|
-
'components': {
|
|
105
|
-
src: 'src/components/**/*.js',
|
|
106
|
-
dest: 'public/js/components/',
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Then use `--only` / `--skip` to target specific configs:
|
|
59
|
+
Builds CSS and JS assets, then syncs them to a destination via rsync or packages them as a zip archive.
|
|
114
60
|
|
|
115
61
|
```sh
|
|
116
|
-
kit
|
|
117
|
-
kit
|
|
62
|
+
kit ship # prompts you to choose a target
|
|
63
|
+
kit ship staging # ship to a specific named target
|
|
64
|
+
kit ship dist # built-in local target (always available, no config needed)
|
|
65
|
+
kit ship --dev # build without minification before shipping
|
|
66
|
+
kit ship --verbose # show detailed rsync/zip output
|
|
118
67
|
```
|
|
119
68
|
|
|
69
|
+
When called without a target name, the command lists all configured targets and asks you to choose one before proceeding. To skip the prompt, pass the target name directly.
|
|
70
|
+
|
|
71
|
+
`kit ship dist` syncs the project to a sibling directory (`../project-name-dist/`) and is always available without any configuration.
|
|
72
|
+
|
|
73
|
+
**Target types**
|
|
74
|
+
|
|
75
|
+
| Type | Config | Behaviour |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Remote | `host` + `dest` | rsyncs `src` to `host:dest` |
|
|
78
|
+
| Local | neither | rsyncs `src` to `../project-name-dist/` (same as `kit ship dist`) |
|
|
79
|
+
| Zip | `zip` path | packages `src` into a zip archive |
|
|
80
|
+
|
|
81
|
+
`zip` is mutually exclusive with `host` and `dest`. The zip destination directory must exist; the command aborts if the archive already exists (remove it manually to re-ship).
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
Create a `haptiq.config.js` in your project root. All options are optional. See [`examples/haptiq.config.js`](examples/haptiq.config.js) for a full annotated reference.
|
|
86
|
+
|
|
120
87
|
## License
|
|
121
88
|
|
|
122
89
|
GPL-2.0-or-later
|
package/bin/haptiq.kit.js
CHANGED
|
@@ -87,6 +87,14 @@ async function loadConfig(verbose = false) {
|
|
|
87
87
|
throw new Error('ship configuration must be an object');
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
if (config.ship.src !== undefined && (typeof config.ship.src !== 'string' || config.ship.src.trim() === '')) {
|
|
91
|
+
throw new Error('ship.src must be a non-empty string');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (config.ship.exclude !== undefined && !Array.isArray(config.ship.exclude)) {
|
|
95
|
+
throw new Error('ship.exclude must be an array');
|
|
96
|
+
}
|
|
97
|
+
|
|
90
98
|
if (config.ship.targets) {
|
|
91
99
|
if (typeof config.ship.targets !== 'object' || Array.isArray(config.ship.targets)) {
|
|
92
100
|
throw new Error('ship.targets must be an object');
|
|
@@ -102,6 +110,15 @@ async function loadConfig(verbose = false) {
|
|
|
102
110
|
if (targetConfig.dev !== undefined && typeof targetConfig.dev !== 'boolean') {
|
|
103
111
|
throw new Error(`ship.targets.${targetName}.dev must be a boolean (true or false, not a string)`);
|
|
104
112
|
}
|
|
113
|
+
if (targetConfig.zip !== undefined && typeof targetConfig.zip !== 'string') {
|
|
114
|
+
throw new Error(`ship.targets.${targetName}.zip must be a string`);
|
|
115
|
+
}
|
|
116
|
+
if (targetConfig.zip && targetConfig.host) {
|
|
117
|
+
throw new Error(`ship.targets.${targetName}: "zip" and "host" are mutually exclusive`);
|
|
118
|
+
}
|
|
119
|
+
if (targetConfig.zip && targetConfig.dest) {
|
|
120
|
+
throw new Error(`ship.targets.${targetName}: "zip" and "dest" are mutually exclusive`);
|
|
121
|
+
}
|
|
105
122
|
}
|
|
106
123
|
}
|
|
107
124
|
}
|
|
@@ -134,8 +151,8 @@ async function loadConfig(verbose = false) {
|
|
|
134
151
|
*/
|
|
135
152
|
function createCommandHandler(taskName, taskFunction) {
|
|
136
153
|
return async (...args) => {
|
|
137
|
-
const options = args[args.length -
|
|
138
|
-
const positionalArgs = args.slice(0, -
|
|
154
|
+
const options = args[args.length - 2];
|
|
155
|
+
const positionalArgs = args.slice(0, -2);
|
|
139
156
|
try {
|
|
140
157
|
const config = await loadConfig(options.verbose);
|
|
141
158
|
await taskFunction(config, options, ...positionalArgs);
|
package/lib/ship.js
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* under the `ship` key.
|
|
7
7
|
*/
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
|
-
import { existsSync } from 'fs';
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
10
10
|
import readline from 'readline';
|
|
11
11
|
import path from 'path';
|
|
12
|
+
import { glob } from 'glob';
|
|
13
|
+
import { zipSync } from 'fflate';
|
|
12
14
|
import { buildCSS } from './css.js';
|
|
13
15
|
import { buildJS } from './js.js';
|
|
14
16
|
|
|
@@ -45,22 +47,96 @@ async function ship(config, verbose, options = {}) {
|
|
|
45
47
|
if (configuredTargetNames.length === 0) {
|
|
46
48
|
throw new Error('No ship targets configured. Add a ship.targets object to haptiq.config.js, or run `kit ship dist` for a local build.');
|
|
47
49
|
}
|
|
48
|
-
|
|
50
|
+
const selected = await promptTarget(configuredTargetNames);
|
|
51
|
+
targetsToProcess = [selected];
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
for (const name of targetsToProcess) {
|
|
52
55
|
const targetConfig = targets[name] ?? {};
|
|
53
|
-
const dev = cliDev
|
|
56
|
+
const dev = (cliDev ?? targetConfig.dev) ?? false;
|
|
54
57
|
|
|
55
58
|
await buildCSS(config, verbose, dev);
|
|
56
59
|
await buildJS(config, verbose, { dev });
|
|
57
|
-
|
|
60
|
+
|
|
61
|
+
if (targetConfig.zip) {
|
|
62
|
+
await zipSingleTarget(name, targetConfig, { src, exclude }, verbose);
|
|
63
|
+
} else {
|
|
64
|
+
await shipSingleTarget(name, targetConfig, { src, exclude, deleteRemoved }, verbose);
|
|
65
|
+
}
|
|
58
66
|
|
|
59
67
|
console.log(`✅ Shipped to [${name}].`);
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Zip a single target — builds a zip archive at the configured path.
|
|
74
|
+
* Aborts if the destination directory does not exist or the zip file already exists.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} name - Target name (for log messages)
|
|
77
|
+
* @param {{ zip: string }} targetConfig - Target settings
|
|
78
|
+
* @param {{ src: string, exclude: string[] }} shipConfig - Shared settings
|
|
79
|
+
* @param {boolean} verbose - List each file added to the archive
|
|
80
|
+
* @returns {Promise<void>}
|
|
81
|
+
*/
|
|
82
|
+
async function zipSingleTarget(name, targetConfig, shipConfig, verbose) {
|
|
83
|
+
const { zip: zipPathTemplate } = targetConfig;
|
|
84
|
+
const { src = './', exclude = [] } = shipConfig;
|
|
85
|
+
|
|
86
|
+
const projectRoot = process.cwd();
|
|
87
|
+
const projectName = path.basename(projectRoot);
|
|
88
|
+
|
|
89
|
+
let version = '0.0.0';
|
|
90
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
91
|
+
if (existsSync(pkgPath)) {
|
|
92
|
+
try {
|
|
93
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
94
|
+
version = pkg.version ?? version;
|
|
95
|
+
} catch { /* use default */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const resolvedZipPath = zipPathTemplate
|
|
99
|
+
.replace(/\{name\}/g, projectName)
|
|
100
|
+
.replace(/\{version\}/g, version);
|
|
101
|
+
|
|
102
|
+
const zipDir = path.dirname(resolvedZipPath);
|
|
103
|
+
if (!existsSync(zipDir)) {
|
|
104
|
+
throw new Error(`Destination directory does not exist: ${zipDir}\nCreate it manually before shipping.`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (existsSync(resolvedZipPath)) {
|
|
108
|
+
throw new Error(`Zip archive already exists: ${resolvedZipPath}\nRemove it manually before shipping to avoid accidental overwrites.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const resolvedSrc = path.resolve(projectRoot, src);
|
|
112
|
+
|
|
113
|
+
const globIgnore = exclude.flatMap(e => {
|
|
114
|
+
const stripped = e.replace(/\/$/, '');
|
|
115
|
+
return [stripped, `${stripped}/**`];
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
console.log(`📦 Packing [${name}] → ${resolvedZipPath}`);
|
|
119
|
+
|
|
120
|
+
const files = await glob('**', {
|
|
121
|
+
cwd: resolvedSrc,
|
|
122
|
+
dot: true,
|
|
123
|
+
nodir: true,
|
|
124
|
+
ignore: globIgnore,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const zipData = {};
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
if (verbose) {
|
|
130
|
+
process.stdout.write(` ${file}\n`);
|
|
131
|
+
}
|
|
132
|
+
zipData[`${projectName}/${file}`] = readFileSync(path.join(resolvedSrc, file));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const zipped = zipSync(zipData);
|
|
136
|
+
writeFileSync(resolvedZipPath, zipped);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
64
140
|
/**
|
|
65
141
|
* Rsync a single target
|
|
66
142
|
*
|
|
@@ -143,11 +219,13 @@ function rsyncDryRun(args) {
|
|
|
143
219
|
|
|
144
220
|
let stdout = '';
|
|
145
221
|
let stderr = '';
|
|
222
|
+
let rejected = false;
|
|
146
223
|
|
|
147
224
|
childProcess.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
148
225
|
childProcess.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
149
226
|
|
|
150
227
|
childProcess.on('close', (code) => {
|
|
228
|
+
if (rejected) return;
|
|
151
229
|
if (code === 0) {
|
|
152
230
|
const deletions = stdout
|
|
153
231
|
.split('\n')
|
|
@@ -160,6 +238,7 @@ function rsyncDryRun(args) {
|
|
|
160
238
|
});
|
|
161
239
|
|
|
162
240
|
childProcess.on('error', (err) => {
|
|
241
|
+
rejected = true;
|
|
163
242
|
if (err.code === 'ENOENT') {
|
|
164
243
|
reject(new Error('rsync not found. Please ensure rsync is installed on your system.'));
|
|
165
244
|
} else {
|
|
@@ -203,15 +282,46 @@ async function confirmDeletion(deletions, destination) {
|
|
|
203
282
|
}
|
|
204
283
|
|
|
205
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Prompt the user to choose a ship target from the available options.
|
|
287
|
+
* Accepts either the target name or its list number.
|
|
288
|
+
*
|
|
289
|
+
* @param {string[]} configuredTargetNames - Target names from haptiq.config.js
|
|
290
|
+
* @returns {Promise<string>} The chosen target name
|
|
291
|
+
*/
|
|
292
|
+
async function promptTarget(configuredTargetNames) {
|
|
293
|
+
const options = [...configuredTargetNames, 'dist'];
|
|
294
|
+
|
|
295
|
+
console.log('\nNo target specified. Choose one of the available targets to ship to:\n');
|
|
296
|
+
options.forEach((name, i) => {
|
|
297
|
+
console.log(` ${i + 1}) ${name}`);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const answer = (await readLine('\nTarget: ')).trim();
|
|
301
|
+
|
|
302
|
+
const num = parseInt(answer, 10);
|
|
303
|
+
if (!isNaN(num) && num >= 1 && num <= options.length) {
|
|
304
|
+
return options[num - 1];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (options.includes(answer)) {
|
|
308
|
+
return answer;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
throw new Error(`"${answer}" is not a valid target. Run \`kit ship <target>\` to skip this prompt.`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
206
315
|
/**
|
|
207
316
|
* Read a single line from stdin.
|
|
208
317
|
*
|
|
318
|
+
* @param {string} prompt - Text to display before waiting for input
|
|
209
319
|
* @returns {Promise<string>}
|
|
210
320
|
*/
|
|
211
|
-
function readLine() {
|
|
321
|
+
function readLine(prompt = '') {
|
|
212
322
|
return new Promise((resolve) => {
|
|
213
323
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
214
|
-
rl.question(
|
|
324
|
+
rl.question(prompt, (answer) => {
|
|
215
325
|
rl.close();
|
|
216
326
|
resolve(answer);
|
|
217
327
|
});
|
|
@@ -231,6 +341,7 @@ function spawnRsync(args, verbose) {
|
|
|
231
341
|
const childProcess = spawn('rsync', args, { shell: false });
|
|
232
342
|
|
|
233
343
|
let stderr = '';
|
|
344
|
+
let rejected = false;
|
|
234
345
|
|
|
235
346
|
if (verbose) {
|
|
236
347
|
childProcess.stdout.on('data', (data) => process.stdout.write(data));
|
|
@@ -241,6 +352,7 @@ function spawnRsync(args, verbose) {
|
|
|
241
352
|
});
|
|
242
353
|
|
|
243
354
|
childProcess.on('close', (code) => {
|
|
355
|
+
if (rejected) return;
|
|
244
356
|
if (code === 0) {
|
|
245
357
|
resolve();
|
|
246
358
|
} else {
|
|
@@ -249,6 +361,7 @@ function spawnRsync(args, verbose) {
|
|
|
249
361
|
});
|
|
250
362
|
|
|
251
363
|
childProcess.on('error', (err) => {
|
|
364
|
+
rejected = true;
|
|
252
365
|
if (err.code === 'ENOENT') {
|
|
253
366
|
reject(new Error('rsync not found. Please ensure rsync is installed on your system.'));
|
|
254
367
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haptiq/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Build tools for Haptiq projects.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"build",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"@haptiq/browserslist-config": "^0.1.0",
|
|
25
25
|
"browserslist": "^4.28.2",
|
|
26
26
|
"commander": "^15.0.0",
|
|
27
|
+
"fflate": "^0.8.0",
|
|
27
28
|
"glob": "^13.0.6",
|
|
28
29
|
"lightningcss": "^1.32.0",
|
|
29
30
|
"sass": "^1.101.0",
|