@haptiq/kit 0.2.0 → 0.4.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/bin/haptiq.kit.js +43 -4
- package/lib/css.js +5 -4
- package/lib/js.js +8 -6
- package/lib/ship.js +162 -0
- package/package.json +1 -1
package/bin/haptiq.kit.js
CHANGED
|
@@ -21,6 +21,7 @@ import path from 'path';
|
|
|
21
21
|
import packageJson from '../package.json' with { type: 'json' };
|
|
22
22
|
import { buildCSS } from '../lib/css.js';
|
|
23
23
|
import { buildJS } from '../lib/js.js';
|
|
24
|
+
import { ship } from '../lib/ship.js';
|
|
24
25
|
|
|
25
26
|
const { version } = packageJson;
|
|
26
27
|
|
|
@@ -80,6 +81,31 @@ async function loadConfig(verbose = false) {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
// Validate ship config structure if present
|
|
85
|
+
if (config.ship) {
|
|
86
|
+
if (typeof config.ship !== 'object' || Array.isArray(config.ship)) {
|
|
87
|
+
throw new Error('ship configuration must be an object');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (config.ship.targets) {
|
|
91
|
+
if (typeof config.ship.targets !== 'object' || Array.isArray(config.ship.targets)) {
|
|
92
|
+
throw new Error('ship.targets must be an object');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const [targetName, targetConfig] of Object.entries(config.ship.targets)) {
|
|
96
|
+
if (targetConfig.dest && typeof targetConfig.dest !== 'string') {
|
|
97
|
+
throw new Error(`ship.targets.${targetName}.dest must be a string`);
|
|
98
|
+
}
|
|
99
|
+
if (targetConfig.host && typeof targetConfig.host !== 'string') {
|
|
100
|
+
throw new Error(`ship.targets.${targetName}.host must be a string`);
|
|
101
|
+
}
|
|
102
|
+
if (targetConfig.dev !== undefined && typeof targetConfig.dev !== 'boolean') {
|
|
103
|
+
throw new Error(`ship.targets.${targetName}.dev must be a boolean (true or false, not a string)`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
83
109
|
return config;
|
|
84
110
|
} catch (configError) {
|
|
85
111
|
// ERR_MODULE_NOT_FOUND is the ESM equivalent of MODULE_NOT_FOUND
|
|
@@ -107,10 +133,12 @@ async function loadConfig(verbose = false) {
|
|
|
107
133
|
* @returns {Function} Command handler function for commander.js
|
|
108
134
|
*/
|
|
109
135
|
function createCommandHandler(taskName, taskFunction) {
|
|
110
|
-
return async (
|
|
136
|
+
return async (...args) => {
|
|
137
|
+
const options = args[args.length - 1];
|
|
138
|
+
const positionalArgs = args.slice(0, -1);
|
|
111
139
|
try {
|
|
112
140
|
const config = await loadConfig(options.verbose);
|
|
113
|
-
await taskFunction(config, options);
|
|
141
|
+
await taskFunction(config, options, ...positionalArgs);
|
|
114
142
|
} catch (error) {
|
|
115
143
|
console.error(`❌ ${taskName} failed:`, error.message);
|
|
116
144
|
process.exit(1);
|
|
@@ -127,19 +155,30 @@ program
|
|
|
127
155
|
program
|
|
128
156
|
.command('css')
|
|
129
157
|
.description('Build CSS from Sass and CSS files')
|
|
158
|
+
.option('--dev', 'Build without minification')
|
|
130
159
|
.option('--verbose', 'Show detailed output for each file processed')
|
|
131
160
|
.action(createCommandHandler('CSS build', async (config, options) => {
|
|
132
|
-
await buildCSS(config, options.verbose);
|
|
161
|
+
await buildCSS(config, options.verbose, options.dev);
|
|
133
162
|
}));
|
|
134
163
|
|
|
135
164
|
program
|
|
136
165
|
.command('js')
|
|
137
166
|
.description('Bundle JavaScript files with Terser')
|
|
167
|
+
.option('--dev', 'Build without minification')
|
|
138
168
|
.option('--verbose', 'Show detailed output for bundling process')
|
|
139
169
|
.option('--only <name>', 'Only run the named configuration')
|
|
140
170
|
.option('--skip <name>', 'Skip the named configuration')
|
|
141
171
|
.action(createCommandHandler('JavaScript build', async (config, options) => {
|
|
142
|
-
await buildJS(config, options.verbose, { only: options.only, skip: options.skip });
|
|
172
|
+
await buildJS(config, options.verbose, { only: options.only, skip: options.skip, dev: options.dev });
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
program
|
|
176
|
+
.command('ship [target]')
|
|
177
|
+
.description('Build assets and ship to remote server via rsync')
|
|
178
|
+
.option('--dev', 'Build without minification before shipping')
|
|
179
|
+
.option('--verbose', 'Show detailed output')
|
|
180
|
+
.action(createCommandHandler('Ship', async (config, options, target) => {
|
|
181
|
+
await ship(config, options.verbose, { target, dev: options.dev });
|
|
143
182
|
}));
|
|
144
183
|
|
|
145
184
|
program.parse();
|
package/lib/css.js
CHANGED
|
@@ -23,10 +23,11 @@ import haptiqBrowserslistConfig from '@haptiq/browserslist-config';
|
|
|
23
23
|
*
|
|
24
24
|
* @param {Object} config - Configuration object from haptiq.config.js
|
|
25
25
|
* @param {boolean} verbose - Show detailed processing logs
|
|
26
|
+
* @param {boolean} dev - Skip minification (--dev flag)
|
|
26
27
|
* @returns {Promise<void>}
|
|
27
28
|
* @see {@link ../examples/haptiq.config.js} for all available options
|
|
28
29
|
*/
|
|
29
|
-
async function buildCSS(config = {}, verbose = false) {
|
|
30
|
+
async function buildCSS(config = {}, verbose = false, dev = false) {
|
|
30
31
|
// Use project's own browserslist config if present, otherwise fall back to @haptiq/browserslist-config
|
|
31
32
|
const projectConfig = browserslist.loadConfig({ path: process.cwd() });
|
|
32
33
|
const resolvedTargets = browserslistToTargets(
|
|
@@ -35,7 +36,7 @@ async function buildCSS(config = {}, verbose = false) {
|
|
|
35
36
|
|
|
36
37
|
const defaultLightningConfig = {
|
|
37
38
|
targets: resolvedTargets,
|
|
38
|
-
minify:
|
|
39
|
+
minify: !dev,
|
|
39
40
|
sourceMap: true
|
|
40
41
|
};
|
|
41
42
|
|
|
@@ -43,10 +44,10 @@ async function buildCSS(config = {}, verbose = false) {
|
|
|
43
44
|
src: 'src/**/*.{scss,sass,css}',
|
|
44
45
|
dest: 'css',
|
|
45
46
|
...config.css,
|
|
46
|
-
// Merge lightning config properly (after user config)
|
|
47
47
|
lightning: {
|
|
48
48
|
...defaultLightningConfig,
|
|
49
|
-
...config.css?.lightning
|
|
49
|
+
...config.css?.lightning,
|
|
50
|
+
...(dev && { minify: false })
|
|
50
51
|
}
|
|
51
52
|
};
|
|
52
53
|
|
package/lib/js.js
CHANGED
|
@@ -18,7 +18,7 @@ import { minify } from 'terser';
|
|
|
18
18
|
*
|
|
19
19
|
* @param {Object} config - Configuration object from haptiq.config.js
|
|
20
20
|
* @param {boolean} verbose - Show detailed processing logs
|
|
21
|
-
* @param {{ only?: string, skip?: string }} options - CLI filter options
|
|
21
|
+
* @param {{ only?: string, skip?: string, dev?: boolean }} options - CLI filter options
|
|
22
22
|
* @returns {Promise<void>}
|
|
23
23
|
*/
|
|
24
24
|
async function buildJS(config = {}, verbose = false, options = {}) {
|
|
@@ -31,7 +31,7 @@ async function buildJS(config = {}, verbose = false, options = {}) {
|
|
|
31
31
|
if (options.only || options.skip) {
|
|
32
32
|
console.warn('⚠️ --only and --skip have no effect with a single configuration');
|
|
33
33
|
}
|
|
34
|
-
await processSingleConfig(jsConfig, verbose);
|
|
34
|
+
await processSingleConfig(jsConfig, verbose, options.dev ?? false);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
console.log(`✅ JavaScript processing completed.`);
|
|
@@ -43,10 +43,11 @@ async function buildJS(config = {}, verbose = false, options = {}) {
|
|
|
43
43
|
*
|
|
44
44
|
* @param {Object} configs - Object with named configurations
|
|
45
45
|
* @param {boolean} verbose - Show detailed processing logs
|
|
46
|
-
* @param {{ only?: string, skip?: string }} options - CLI filter options
|
|
46
|
+
* @param {{ only?: string, skip?: string, dev?: boolean }} options - CLI filter options
|
|
47
47
|
* @returns {Promise<void>}
|
|
48
48
|
*/
|
|
49
49
|
async function processMultipleConfigs(configs, verbose, options = {}) {
|
|
50
|
+
const dev = options.dev ?? false;
|
|
50
51
|
const configNames = Object.keys(configs);
|
|
51
52
|
|
|
52
53
|
let configsToProcess = configNames;
|
|
@@ -67,7 +68,7 @@ async function processMultipleConfigs(configs, verbose, options = {}) {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
for (const configName of configsToProcess) {
|
|
70
|
-
await processSingleConfig(configs[configName], verbose);
|
|
71
|
+
await processSingleConfig(configs[configName], verbose, dev);
|
|
71
72
|
if (verbose) {
|
|
72
73
|
console.log(` ✅ [${configName}] done`);
|
|
73
74
|
}
|
|
@@ -82,7 +83,7 @@ async function processMultipleConfigs(configs, verbose, options = {}) {
|
|
|
82
83
|
* @param {boolean} verbose - Show detailed processing logs
|
|
83
84
|
* @returns {Promise<void>}
|
|
84
85
|
*/
|
|
85
|
-
async function processSingleConfig(jsConfig, verbose) {
|
|
86
|
+
async function processSingleConfig(jsConfig, verbose, dev = false) {
|
|
86
87
|
const dest = jsConfig.dest || 'js/bundle.js';
|
|
87
88
|
let combine = jsConfig.combine;
|
|
88
89
|
if (combine === undefined) {
|
|
@@ -96,7 +97,8 @@ async function processSingleConfig(jsConfig, verbose) {
|
|
|
96
97
|
combine,
|
|
97
98
|
terser: {
|
|
98
99
|
sourceMap: true,
|
|
99
|
-
...jsConfig.terser
|
|
100
|
+
...jsConfig.terser,
|
|
101
|
+
...(dev && { compress: false, mangle: false, format: { beautify: true } })
|
|
100
102
|
}
|
|
101
103
|
};
|
|
102
104
|
|
package/lib/ship.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ship Module
|
|
3
|
+
*
|
|
4
|
+
* Builds CSS and JS assets then syncs them to a remote or local
|
|
5
|
+
* destination via rsync. Target configuration lives in haptiq.config.js
|
|
6
|
+
* under the `ship` key.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { buildCSS } from './css.js';
|
|
12
|
+
import { buildJS } from './js.js';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Main ship function — builds assets then rsyncs for each target
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} config - Configuration object from haptiq.config.js
|
|
19
|
+
* @param {boolean} verbose - Show detailed output
|
|
20
|
+
* @param {{ target?: string, dev?: boolean }} options - CLI options
|
|
21
|
+
* @returns {Promise<void>}
|
|
22
|
+
*/
|
|
23
|
+
async function ship(config, verbose, options = {}) {
|
|
24
|
+
const { target: targetName, dev: cliDev } = options;
|
|
25
|
+
|
|
26
|
+
const shipConfig = config.ship;
|
|
27
|
+
|
|
28
|
+
const { targets = {}, src = './', exclude, delete: deleteRemoved = false } = shipConfig ?? {};
|
|
29
|
+
|
|
30
|
+
if (!exclude || exclude.length === 0) {
|
|
31
|
+
console.warn('⚠️ No ship.exclude configured — everything in src will be synced including node_modules. Add an exclude list to haptiq.config.js to filter unwanted files.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const configuredTargetNames = Object.keys(targets);
|
|
35
|
+
let targetsToProcess;
|
|
36
|
+
|
|
37
|
+
if (targetName) {
|
|
38
|
+
if (targetName !== 'dist' && !targets[targetName]) {
|
|
39
|
+
const available = [...configuredTargetNames, 'dist'].join(', ');
|
|
40
|
+
throw new Error(`Target "${targetName}" not found. Available: ${available}`);
|
|
41
|
+
}
|
|
42
|
+
targetsToProcess = [targetName];
|
|
43
|
+
} else {
|
|
44
|
+
if (configuredTargetNames.length === 0) {
|
|
45
|
+
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.');
|
|
46
|
+
}
|
|
47
|
+
targetsToProcess = configuredTargetNames;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const name of targetsToProcess) {
|
|
51
|
+
const targetConfig = targets[name] ?? {};
|
|
52
|
+
const dev = cliDev || targetConfig.dev || false;
|
|
53
|
+
|
|
54
|
+
await buildCSS(config, verbose, dev);
|
|
55
|
+
await buildJS(config, verbose, { dev });
|
|
56
|
+
await shipSingleTarget(name, targetConfig, { src, exclude, deleteRemoved }, verbose);
|
|
57
|
+
|
|
58
|
+
console.log(`✅ Shipped to [${name}].`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Rsync a single target
|
|
65
|
+
*
|
|
66
|
+
* @param {string} name - Target name (for error messages)
|
|
67
|
+
* @param {{ dest?: string, host?: string, dev?: boolean }} targetConfig - Target settings. `dest` is required for remote targets; local targets derive it automatically.
|
|
68
|
+
* @param {{ src: string, exclude: string[], deleteRemoved: boolean }} shipConfig - Shared settings
|
|
69
|
+
* @param {boolean} verbose - Stream rsync output to console
|
|
70
|
+
* @returns {Promise<void>}
|
|
71
|
+
*/
|
|
72
|
+
async function shipSingleTarget(name, targetConfig, shipConfig, verbose) {
|
|
73
|
+
const { dest, host } = targetConfig;
|
|
74
|
+
const { src = './', exclude = [], deleteRemoved = false } = shipConfig;
|
|
75
|
+
|
|
76
|
+
let destination;
|
|
77
|
+
|
|
78
|
+
if (host) {
|
|
79
|
+
if (!dest) {
|
|
80
|
+
throw new Error(`ship.targets.${name}: missing required field "dest" for remote target`);
|
|
81
|
+
}
|
|
82
|
+
destination = `${host}:${dest}`;
|
|
83
|
+
} else {
|
|
84
|
+
if (dest) {
|
|
85
|
+
console.warn(`⚠️ ship.targets.${name}: "dest" has no effect on local targets — destination is always derived as ../project-name-dist/.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const projectRoot = process.cwd();
|
|
89
|
+
const projectName = path.basename(projectRoot);
|
|
90
|
+
const localDest = path.resolve(projectRoot, '..', `${projectName}-dist`);
|
|
91
|
+
|
|
92
|
+
if (existsSync(localDest)) {
|
|
93
|
+
throw new Error(`Local destination "${localDest}" already exists. Remove it manually before shipping to avoid overwriting existing content.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
destination = localDest;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const projectRoot = process.cwd();
|
|
100
|
+
const resolvedSrc = path.resolve(projectRoot, src);
|
|
101
|
+
if (resolvedSrc !== projectRoot && !resolvedSrc.startsWith(projectRoot + path.sep)) {
|
|
102
|
+
throw new Error(`ship.src must be inside the project root. Got: ${resolvedSrc}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const normalizedSrc = src.endsWith('/') ? src : `${src}/`;
|
|
106
|
+
|
|
107
|
+
const args = [
|
|
108
|
+
'-az',
|
|
109
|
+
...(verbose ? ['-v'] : []),
|
|
110
|
+
...exclude.flatMap(e => ['--exclude', e]),
|
|
111
|
+
...(deleteRemoved ? ['--delete', '--delete-excluded'] : []),
|
|
112
|
+
normalizedSrc,
|
|
113
|
+
destination,
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
console.log(`🚀 Shipping [${name}] → ${destination}`);
|
|
117
|
+
|
|
118
|
+
await spawnRsync(args, verbose);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Spawn rsync as a child process without shell interpolation
|
|
124
|
+
*
|
|
125
|
+
* @param {string[]} args - rsync argument array
|
|
126
|
+
* @param {boolean} verbose - Pipe stdout to console
|
|
127
|
+
* @returns {Promise<void>}
|
|
128
|
+
*/
|
|
129
|
+
function spawnRsync(args, verbose) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const childProcess = spawn('rsync', args, { shell: false });
|
|
132
|
+
|
|
133
|
+
let stderr = '';
|
|
134
|
+
|
|
135
|
+
if (verbose) {
|
|
136
|
+
childProcess.stdout.on('data', (data) => process.stdout.write(data));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
childProcess.stderr.on('data', (data) => {
|
|
140
|
+
stderr += data.toString();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
childProcess.on('close', (code) => {
|
|
144
|
+
if (code === 0) {
|
|
145
|
+
resolve();
|
|
146
|
+
} else {
|
|
147
|
+
reject(new Error(`rsync failed: ${stderr.trim()}`));
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
childProcess.on('error', (err) => {
|
|
152
|
+
if (err.code === 'ENOENT') {
|
|
153
|
+
reject(new Error('rsync not found. Please ensure rsync is installed on your system.'));
|
|
154
|
+
} else {
|
|
155
|
+
reject(new Error(`Failed to start rsync: ${err.message}`));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
export { ship };
|