@haptiq/kit 0.6.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 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
- ## Configuration
55
+ ---
56
56
 
57
- Create a `haptiq.config.js` in your project root. All options are optional.
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
- ### Multiple JS configs
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 js --only app
117
- kit js --skip components
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
@@ -110,6 +110,15 @@ async function loadConfig(verbose = false) {
110
110
  if (targetConfig.dev !== undefined && typeof targetConfig.dev !== 'boolean') {
111
111
  throw new Error(`ship.targets.${targetName}.dev must be a boolean (true or false, not a string)`);
112
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
+ }
113
122
  }
114
123
  }
115
124
  }
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,7 +47,8 @@ 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
- targetsToProcess = configuredTargetNames;
50
+ const selected = await promptTarget(configuredTargetNames);
51
+ targetsToProcess = [selected];
49
52
  }
50
53
 
51
54
  for (const name of targetsToProcess) {
@@ -54,13 +57,86 @@ async function ship(config, verbose, options = {}) {
54
57
 
55
58
  await buildCSS(config, verbose, dev);
56
59
  await buildJS(config, verbose, { dev });
57
- await shipSingleTarget(name, targetConfig, { src, exclude, deleteRemoved }, verbose);
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
  *
@@ -206,15 +282,46 @@ async function confirmDeletion(deletions, destination) {
206
282
  }
207
283
 
208
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
+
209
315
  /**
210
316
  * Read a single line from stdin.
211
317
  *
318
+ * @param {string} prompt - Text to display before waiting for input
212
319
  * @returns {Promise<string>}
213
320
  */
214
- function readLine() {
321
+ function readLine(prompt = '') {
215
322
  return new Promise((resolve) => {
216
323
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
217
- rl.question('', (answer) => {
324
+ rl.question(prompt, (answer) => {
218
325
  rl.close();
219
326
  resolve(answer);
220
327
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haptiq/kit",
3
- "version": "0.6.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",