balm-core 5.2.0 → 6.0.0-alpha.1

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 (301) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.cjs +2625 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +569 -0
  5. package/dist/index.d.ts +569 -0
  6. package/dist/index.js +2527 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +49 -123
  9. package/src/balm.ts +108 -0
  10. package/src/bundler/esbuild.ts +47 -0
  11. package/src/bundler/index.ts +3 -0
  12. package/src/bundler/rollup.ts +45 -0
  13. package/src/bundler/webpack.ts +124 -0
  14. package/src/config/constants.ts +12 -0
  15. package/src/config/defaults.ts +209 -0
  16. package/src/config/index.ts +112 -0
  17. package/src/hooks/index.ts +224 -0
  18. package/src/index.ts +16 -0
  19. package/src/pipeline/file.ts +69 -0
  20. package/src/pipeline/index.ts +10 -0
  21. package/src/pipeline/pipeline.ts +135 -0
  22. package/src/pipeline/transforms/cache.ts +146 -0
  23. package/src/pipeline/transforms/htmlmin.ts +18 -0
  24. package/src/pipeline/transforms/less.ts +75 -0
  25. package/src/pipeline/transforms/postcss.ts +43 -0
  26. package/src/pipeline/transforms/rename.ts +37 -0
  27. package/src/pipeline/transforms/replace.ts +30 -0
  28. package/src/pipeline/transforms/sass.ts +106 -0
  29. package/src/pipeline/transforms/terser.ts +20 -0
  30. package/src/runner/dag.ts +77 -0
  31. package/src/runner/task.ts +38 -0
  32. package/src/shims.d.ts +5 -0
  33. package/src/tasks/cache.ts +72 -0
  34. package/src/tasks/clean.ts +21 -0
  35. package/src/tasks/copy.ts +36 -0
  36. package/src/tasks/ftp.ts +90 -0
  37. package/src/tasks/html.ts +232 -0
  38. package/src/tasks/index.ts +14 -0
  39. package/src/tasks/publish.ts +71 -0
  40. package/src/tasks/pwa.ts +99 -0
  41. package/src/tasks/remove.ts +24 -0
  42. package/src/tasks/replace.ts +34 -0
  43. package/src/tasks/script.ts +28 -0
  44. package/src/tasks/server.ts +183 -0
  45. package/src/tasks/static.ts +104 -0
  46. package/src/tasks/style.ts +57 -0
  47. package/src/tasks/zip.ts +65 -0
  48. package/src/types/index.ts +202 -0
  49. package/src/utilities/file.ts +120 -0
  50. package/src/utilities/logger.ts +63 -0
  51. package/src/utilities/utils.ts +46 -0
  52. package/src/utilities/workspace.ts +38 -0
  53. package/src/watcher/index.ts +100 -0
  54. package/tsconfig.json +8 -0
  55. package/tsup.config.ts +12 -0
  56. package/README.md +0 -196
  57. package/index.d.ts +0 -360
  58. package/lib/balm.js +0 -49
  59. package/lib/bootstrap/check_config.js +0 -43
  60. package/lib/bootstrap/index.js +0 -87
  61. package/lib/bundler/esbuild/build.js +0 -26
  62. package/lib/bundler/esbuild/index.js +0 -7
  63. package/lib/bundler/esbuild/options.js +0 -7
  64. package/lib/bundler/esbuild/transform.js +0 -36
  65. package/lib/bundler/rollup/build.js +0 -52
  66. package/lib/bundler/rollup/entry.js +0 -28
  67. package/lib/bundler/rollup/index.js +0 -20
  68. package/lib/bundler/rollup/output.js +0 -20
  69. package/lib/bundler/rollup/plugins.js +0 -31
  70. package/lib/bundler/webpack/config/common.js +0 -135
  71. package/lib/bundler/webpack/config/dev.js +0 -12
  72. package/lib/bundler/webpack/config/env.js +0 -67
  73. package/lib/bundler/webpack/config/index.js +0 -6
  74. package/lib/bundler/webpack/config/prod.js +0 -39
  75. package/lib/bundler/webpack/config/regex.js +0 -10
  76. package/lib/bundler/webpack/entry.js +0 -65
  77. package/lib/bundler/webpack/index.js +0 -32
  78. package/lib/bundler/webpack/loaders.js +0 -50
  79. package/lib/bundler/webpack/output.js +0 -41
  80. package/lib/bundler/webpack/rules/asset.js +0 -33
  81. package/lib/bundler/webpack/rules/babel.js +0 -19
  82. package/lib/bundler/webpack/rules/css.js +0 -109
  83. package/lib/bundler/webpack/rules/html.js +0 -15
  84. package/lib/bundler/webpack/rules/index.js +0 -10
  85. package/lib/config/assets.js +0 -30
  86. package/lib/config/constants.js +0 -14
  87. package/lib/config/env.js +0 -35
  88. package/lib/config/extras.js +0 -6
  89. package/lib/config/ftp.js +0 -6
  90. package/lib/config/globals.js +0 -43
  91. package/lib/config/html.js +0 -22
  92. package/lib/config/images.js +0 -4
  93. package/lib/config/index.js +0 -63
  94. package/lib/config/logs.js +0 -11
  95. package/lib/config/paths.js +0 -46
  96. package/lib/config/pwa.js +0 -24
  97. package/lib/config/roots.js +0 -9
  98. package/lib/config/scripts.js +0 -162
  99. package/lib/config/server.js +0 -84
  100. package/lib/config/spinner.js +0 -9
  101. package/lib/config/styles.js +0 -67
  102. package/lib/hello.js +0 -19
  103. package/lib/hooks/index.js +0 -94
  104. package/lib/hooks/maker.js +0 -59
  105. package/lib/index.js +0 -5
  106. package/lib/middlewares/history.js +0 -6
  107. package/lib/middlewares/index.js +0 -11
  108. package/lib/middlewares/proxy.js +0 -28
  109. package/lib/middlewares/webpack.js +0 -26
  110. package/lib/plugins/htmlmin.js +0 -39
  111. package/lib/plugins/imagemin.js +0 -95
  112. package/lib/plugins/index.js +0 -27
  113. package/lib/plugins/jsmin.js +0 -37
  114. package/lib/plugins/less.js +0 -97
  115. package/lib/plugins/plumber.js +0 -87
  116. package/lib/plugins/postcss-plugins.js +0 -13
  117. package/lib/plugins/postcss.js +0 -117
  118. package/lib/plugins/rename.js +0 -51
  119. package/lib/plugins/replace.js +0 -91
  120. package/lib/plugins/sass-legacy.js +0 -131
  121. package/lib/plugins/sass.js +0 -134
  122. package/lib/plugins/sftp.js +0 -288
  123. package/lib/tasks/default.js +0 -46
  124. package/lib/tasks/foundation/balm.js +0 -72
  125. package/lib/tasks/foundation/balm_style.js +0 -65
  126. package/lib/tasks/foundation/index.js +0 -10
  127. package/lib/tasks/index.js +0 -48
  128. package/lib/tasks/private/build.js +0 -18
  129. package/lib/tasks/private/cache.js +0 -31
  130. package/lib/tasks/private/clean.js +0 -69
  131. package/lib/tasks/private/end.js +0 -24
  132. package/lib/tasks/private/extra.js +0 -35
  133. package/lib/tasks/private/font.js +0 -15
  134. package/lib/tasks/private/image.js +0 -37
  135. package/lib/tasks/private/index.js +0 -34
  136. package/lib/tasks/private/lint.js +0 -20
  137. package/lib/tasks/private/media.js +0 -15
  138. package/lib/tasks/private/modernizr.js +0 -59
  139. package/lib/tasks/private/pwa-cache.js +0 -32
  140. package/lib/tasks/private/script.js +0 -9
  141. package/lib/tasks/private/sprite.js +0 -105
  142. package/lib/tasks/private/start.js +0 -16
  143. package/lib/tasks/private/style.js +0 -9
  144. package/lib/tasks/private/workbox-sw.js +0 -15
  145. package/lib/tasks/public/copy.js +0 -16
  146. package/lib/tasks/public/esbuild.js +0 -18
  147. package/lib/tasks/public/ftp.js +0 -39
  148. package/lib/tasks/public/html.js +0 -65
  149. package/lib/tasks/public/index.js +0 -38
  150. package/lib/tasks/public/jsmin.js +0 -23
  151. package/lib/tasks/public/less.js +0 -21
  152. package/lib/tasks/public/postcss.js +0 -16
  153. package/lib/tasks/public/publish.js +0 -51
  154. package/lib/tasks/public/pwa.js +0 -84
  155. package/lib/tasks/public/remove.js +0 -46
  156. package/lib/tasks/public/replace.js +0 -26
  157. package/lib/tasks/public/rollup.js +0 -17
  158. package/lib/tasks/public/sass.js +0 -27
  159. package/lib/tasks/public/server.js +0 -130
  160. package/lib/tasks/public/url.js +0 -35
  161. package/lib/tasks/public/version.js +0 -23
  162. package/lib/tasks/public/webpack.js +0 -39
  163. package/lib/tasks/public/zip.js +0 -19
  164. package/lib/utilities/address.js +0 -55
  165. package/lib/utilities/cache-core.js +0 -66
  166. package/lib/utilities/color.js +0 -27
  167. package/lib/utilities/compiling.js +0 -24
  168. package/lib/utilities/detect-port.js +0 -100
  169. package/lib/utilities/escape-string-regexp.js +0 -10
  170. package/lib/utilities/file.js +0 -70
  171. package/lib/utilities/index.js +0 -8
  172. package/lib/utilities/loading.js +0 -37
  173. package/lib/utilities/logger.js +0 -99
  174. package/lib/utilities/merge-stream.js +0 -38
  175. package/lib/utilities/namespace.js +0 -11
  176. package/lib/utilities/public-url.js +0 -37
  177. package/lib/utilities/replacestream.js +0 -106
  178. package/lib/utilities/typeof.js +0 -5
  179. package/lib/utilities/utils.js +0 -43
  180. package/tslib/balm.js +0 -44
  181. package/tslib/bootstrap/check_config.js +0 -36
  182. package/tslib/bootstrap/index.js +0 -82
  183. package/tslib/bundler/esbuild/build.js +0 -30
  184. package/tslib/bundler/esbuild/index.js +0 -11
  185. package/tslib/bundler/esbuild/options.js +0 -7
  186. package/tslib/bundler/esbuild/transform.js +0 -38
  187. package/tslib/bundler/rollup/build.js +0 -17
  188. package/tslib/bundler/rollup/entry.js +0 -24
  189. package/tslib/bundler/rollup/index.js +0 -21
  190. package/tslib/bundler/rollup/output.js +0 -23
  191. package/tslib/bundler/rollup/plugins.js +0 -27
  192. package/tslib/bundler/webpack/config/common.js +0 -130
  193. package/tslib/bundler/webpack/config/dev.js +0 -14
  194. package/tslib/bundler/webpack/config/env.js +0 -44
  195. package/tslib/bundler/webpack/config/index.js +0 -8
  196. package/tslib/bundler/webpack/config/prod.js +0 -43
  197. package/tslib/bundler/webpack/config/regex.js +0 -10
  198. package/tslib/bundler/webpack/entry.js +0 -68
  199. package/tslib/bundler/webpack/index.js +0 -30
  200. package/tslib/bundler/webpack/loaders.js +0 -51
  201. package/tslib/bundler/webpack/output.js +0 -41
  202. package/tslib/bundler/webpack/rules/asset.js +0 -33
  203. package/tslib/bundler/webpack/rules/babel.js +0 -16
  204. package/tslib/bundler/webpack/rules/css.js +0 -95
  205. package/tslib/bundler/webpack/rules/html.js +0 -15
  206. package/tslib/bundler/webpack/rules/index.js +0 -10
  207. package/tslib/config/assets.js +0 -24
  208. package/tslib/config/constants.js +0 -14
  209. package/tslib/config/env.js +0 -43
  210. package/tslib/config/extras.js +0 -6
  211. package/tslib/config/ftp.js +0 -6
  212. package/tslib/config/globals.js +0 -47
  213. package/tslib/config/html.js +0 -17
  214. package/tslib/config/images.js +0 -4
  215. package/tslib/config/index.js +0 -56
  216. package/tslib/config/logs.js +0 -6
  217. package/tslib/config/paths.js +0 -30
  218. package/tslib/config/pwa.js +0 -18
  219. package/tslib/config/roots.js +0 -8
  220. package/tslib/config/scripts.js +0 -93
  221. package/tslib/config/server.js +0 -33
  222. package/tslib/config/spinner.js +0 -10
  223. package/tslib/config/styles.js +0 -37
  224. package/tslib/hello.js +0 -19
  225. package/tslib/hooks/index.js +0 -75
  226. package/tslib/hooks/maker.js +0 -66
  227. package/tslib/index.js +0 -5
  228. package/tslib/middlewares/history.js +0 -12
  229. package/tslib/middlewares/index.js +0 -18
  230. package/tslib/middlewares/proxy.js +0 -27
  231. package/tslib/middlewares/webpack.js +0 -29
  232. package/tslib/plugins/htmlmin.js +0 -38
  233. package/tslib/plugins/imagemin.js +0 -97
  234. package/tslib/plugins/index.js +0 -27
  235. package/tslib/plugins/jsmin.js +0 -39
  236. package/tslib/plugins/less.js +0 -90
  237. package/tslib/plugins/plumber.js +0 -81
  238. package/tslib/plugins/postcss-plugins.js +0 -24
  239. package/tslib/plugins/postcss.js +0 -111
  240. package/tslib/plugins/rename.js +0 -51
  241. package/tslib/plugins/replace.js +0 -72
  242. package/tslib/plugins/sass-legacy.js +0 -95
  243. package/tslib/plugins/sass.js +0 -109
  244. package/tslib/plugins/sftp.js +0 -258
  245. package/tslib/tasks/default.js +0 -59
  246. package/tslib/tasks/foundation/balm.js +0 -75
  247. package/tslib/tasks/foundation/balm_style.js +0 -68
  248. package/tslib/tasks/foundation/index.js +0 -10
  249. package/tslib/tasks/index.js +0 -43
  250. package/tslib/tasks/private/build.js +0 -15
  251. package/tslib/tasks/private/cache.js +0 -48
  252. package/tslib/tasks/private/clean.js +0 -82
  253. package/tslib/tasks/private/end.js +0 -21
  254. package/tslib/tasks/private/extra.js +0 -34
  255. package/tslib/tasks/private/font.js +0 -12
  256. package/tslib/tasks/private/image.js +0 -42
  257. package/tslib/tasks/private/index.js +0 -34
  258. package/tslib/tasks/private/lint.js +0 -17
  259. package/tslib/tasks/private/media.js +0 -12
  260. package/tslib/tasks/private/modernizr.js +0 -54
  261. package/tslib/tasks/private/pwa-cache.js +0 -32
  262. package/tslib/tasks/private/script.js +0 -9
  263. package/tslib/tasks/private/sprite.js +0 -93
  264. package/tslib/tasks/private/start.js +0 -13
  265. package/tslib/tasks/private/style.js +0 -12
  266. package/tslib/tasks/private/workbox-sw.js +0 -15
  267. package/tslib/tasks/public/copy.js +0 -18
  268. package/tslib/tasks/public/esbuild.js +0 -18
  269. package/tslib/tasks/public/ftp.js +0 -39
  270. package/tslib/tasks/public/html.js +0 -75
  271. package/tslib/tasks/public/index.js +0 -38
  272. package/tslib/tasks/public/jsmin.js +0 -24
  273. package/tslib/tasks/public/less.js +0 -21
  274. package/tslib/tasks/public/postcss.js +0 -16
  275. package/tslib/tasks/public/publish.js +0 -50
  276. package/tslib/tasks/public/pwa.js +0 -87
  277. package/tslib/tasks/public/remove.js +0 -44
  278. package/tslib/tasks/public/replace.js +0 -27
  279. package/tslib/tasks/public/rollup.js +0 -17
  280. package/tslib/tasks/public/sass.js +0 -27
  281. package/tslib/tasks/public/server.js +0 -129
  282. package/tslib/tasks/public/url.js +0 -35
  283. package/tslib/tasks/public/version.js +0 -27
  284. package/tslib/tasks/public/webpack.js +0 -41
  285. package/tslib/tasks/public/zip.js +0 -19
  286. package/tslib/utilities/address.js +0 -48
  287. package/tslib/utilities/cache-core.js +0 -57
  288. package/tslib/utilities/color.js +0 -33
  289. package/tslib/utilities/compiling.js +0 -16
  290. package/tslib/utilities/detect-port.js +0 -91
  291. package/tslib/utilities/escape-string-regexp.js +0 -7
  292. package/tslib/utilities/file.js +0 -78
  293. package/tslib/utilities/index.js +0 -8
  294. package/tslib/utilities/loading.js +0 -25
  295. package/tslib/utilities/logger.js +0 -92
  296. package/tslib/utilities/merge-stream.js +0 -34
  297. package/tslib/utilities/namespace.js +0 -12
  298. package/tslib/utilities/public-url.js +0 -31
  299. package/tslib/utilities/replacestream.js +0 -112
  300. package/tslib/utilities/typeof.js +0 -8
  301. package/tslib/utilities/utils.js +0 -38
@@ -0,0 +1,232 @@
1
+ import path from 'node:path';
2
+ import fg from 'fast-glob';
3
+ import { BaseTask } from '../runner/task.js';
4
+ import { BalmConfig } from '../types/index.js';
5
+ import { Pipeline } from '../pipeline/pipeline.js';
6
+ import { transformHtmlmin } from '../pipeline/transforms/htmlmin.js';
7
+ import { transformReplace } from '../pipeline/transforms/replace.js';
8
+ import { PUBLIC_URL } from '../config/constants.js';
9
+
10
+ const LIVE_RELOAD_SCRIPT = `
11
+ <!-- BalmJS Live Reload -->
12
+ <script>
13
+ (() => {
14
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
15
+ const ws = new WebSocket(\`\${protocol}//\${location.host}/__balm_ws\`);
16
+ ws.onmessage = (e) => {
17
+ try {
18
+ const msg = JSON.parse(e.data);
19
+ if (msg.type === 'reload') {
20
+ console.log('[BalmJS] Reloading page...');
21
+ location.reload();
22
+ } else if (msg.type === 'reload-css') {
23
+ console.log('[BalmJS] Reloading CSS...');
24
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
25
+ for (const link of links) {
26
+ const url = new URL(link.href);
27
+ url.searchParams.set('_balm_t', Date.now().toString());
28
+ link.href = url.href;
29
+ }
30
+ }
31
+ } catch {}
32
+ };
33
+ ws.onclose = () => {
34
+ setTimeout(() => location.reload(), 2000);
35
+ };
36
+ })();
37
+ </script>
38
+ `;
39
+
40
+ export class HtmlTask extends BaseTask {
41
+ constructor() {
42
+ super({ name: 'html', description: 'Process HTML templates' });
43
+ }
44
+
45
+ async run(config: BalmConfig): Promise<void> {
46
+ const customTemplate = config.scripts.htmlPluginOptions?.template;
47
+ let pipeline: Pipeline;
48
+
49
+ if (customTemplate) {
50
+ const templatePath = path.isAbsolute(customTemplate)
51
+ ? customTemplate
52
+ : path.join(config.workspace, customTemplate);
53
+ pipeline = Pipeline.from(templatePath, {
54
+ cwd: config.workspace,
55
+ base: path.dirname(templatePath)
56
+ });
57
+ } else {
58
+ const templateDir = config.scripts.injectHtml
59
+ ? config.src.base
60
+ : config.src.html;
61
+
62
+ const pattern = path.join(templateDir, '*.html');
63
+ pipeline = Pipeline.from(pattern, {
64
+ cwd: config.workspace,
65
+ base: templateDir
66
+ });
67
+ }
68
+
69
+ // Replace %PUBLIC_URL% placeholder
70
+ const publicUrlReplacement = config.assets.virtualDir ? `/${config.assets.virtualDir}` : '';
71
+ pipeline.pipe(transformReplace(new RegExp(PUBLIC_URL, 'g'), publicUrlReplacement));
72
+
73
+ // Ensure output filename is index.html if custom template was used
74
+ if (customTemplate) {
75
+ pipeline.pipe(async (file) => {
76
+ file.basename = 'index.html';
77
+ return file;
78
+ });
79
+ }
80
+
81
+ // In production, map source paths to target paths in HTML
82
+ if (config.env.isProd) {
83
+ pipeline.pipe(async (file) => {
84
+ if (/\.html$/i.test(file.extname)) {
85
+ let content = file.toString();
86
+ if (
87
+ config.paths.source.css &&
88
+ config.paths.target.css &&
89
+ config.paths.source.css !== config.paths.target.css
90
+ ) {
91
+ content = content.replaceAll(
92
+ `/${config.paths.source.css}/`,
93
+ `/${config.paths.target.css}/`
94
+ );
95
+ content = content.replaceAll(
96
+ `"${config.paths.source.css}/`,
97
+ `"${config.paths.target.css}/`
98
+ );
99
+ content = content.replaceAll(
100
+ `'${config.paths.source.css}/`,
101
+ `'${config.paths.target.css}/`
102
+ );
103
+ }
104
+ if (
105
+ config.paths.source.js &&
106
+ config.paths.target.js &&
107
+ config.paths.source.js !== config.paths.target.js
108
+ ) {
109
+ content = content.replaceAll(
110
+ `/${config.paths.source.js}/`,
111
+ `/${config.paths.target.js}/`
112
+ );
113
+ content = content.replaceAll(
114
+ `"${config.paths.source.js}/`,
115
+ `"${config.paths.target.js}/`
116
+ );
117
+ content = content.replaceAll(
118
+ `'${config.paths.source.js}/`,
119
+ `'${config.paths.target.js}/`
120
+ );
121
+ }
122
+ if (
123
+ config.paths.source.img &&
124
+ config.paths.target.img &&
125
+ config.paths.source.img !== config.paths.target.img
126
+ ) {
127
+ content = content.replaceAll(
128
+ `/${config.paths.source.img}/`,
129
+ `/${config.paths.target.img}/`
130
+ );
131
+ content = content.replaceAll(
132
+ `"${config.paths.source.img}/`,
133
+ `"${config.paths.target.img}/`
134
+ );
135
+ content = content.replaceAll(
136
+ `'${config.paths.source.img}/`,
137
+ `'${config.paths.target.img}/`
138
+ );
139
+ }
140
+ if (
141
+ config.paths.source.font &&
142
+ config.paths.target.font &&
143
+ config.paths.source.font !== config.paths.target.font
144
+ ) {
145
+ content = content.replaceAll(
146
+ `/${config.paths.source.font}/`,
147
+ `/${config.paths.target.font}/`
148
+ );
149
+ content = content.replaceAll(
150
+ `"${config.paths.source.font}/`,
151
+ `"${config.paths.target.font}/`
152
+ );
153
+ content = content.replaceAll(
154
+ `'${config.paths.source.font}/`,
155
+ `'${config.paths.target.font}/`
156
+ );
157
+ }
158
+ file.contents = Buffer.from(content);
159
+ }
160
+ return file;
161
+ });
162
+ }
163
+
164
+ // Auto-inject script tags if injectHtml is enabled
165
+ if (config.scripts.injectHtml) {
166
+ pipeline.pipe(async (file) => {
167
+ if (/\.html$/i.test(file.extname)) {
168
+ let content = file.toString();
169
+ const entries =
170
+ typeof config.scripts.entry === 'object' && !Array.isArray(config.scripts.entry)
171
+ ? Object.keys(config.scripts.entry)
172
+ : ['main'];
173
+
174
+ const jsDir = config.env.isProd ? config.paths.target.js : config.paths.tmp.js;
175
+ const jsPathPrefix = config.assets.virtualDir ? `/${config.assets.virtualDir}/${jsDir}` : `/${jsDir}`;
176
+ const destJsFolder = config.dest.js;
177
+
178
+ const scriptTags = entries
179
+ .filter(
180
+ (entryName) =>
181
+ !new RegExp(`<script[^>]+src=["'][^"']*${entryName}[^"']*\\.js["']`, 'i').test(
182
+ content
183
+ )
184
+ )
185
+ .map((entryName) => {
186
+ let actualFilename = `${entryName}.js`;
187
+ try {
188
+ const matched = fg.sync(`${entryName}.*.js`, { cwd: destJsFolder });
189
+ if (matched.length) {
190
+ const valid = matched.find((f) => !f.endsWith('.map') && !f.endsWith('.txt'));
191
+ if (valid) {
192
+ actualFilename = valid;
193
+ }
194
+ }
195
+ } catch {}
196
+ return `<script defer src="${jsPathPrefix}/${actualFilename}"></script>`;
197
+ })
198
+ .join('\n');
199
+
200
+ if (scriptTags) {
201
+ if (content.includes('</body>')) {
202
+ content = content.replace('</body>', `${scriptTags}\n</body>`);
203
+ } else {
204
+ content += `\n${scriptTags}`;
205
+ }
206
+ file.contents = Buffer.from(content);
207
+ }
208
+ }
209
+ return file;
210
+ });
211
+ }
212
+
213
+ if (config.env.isProd) {
214
+ pipeline.pipe(transformHtmlmin(config.html.options));
215
+ } else {
216
+ pipeline.pipe(async (file) => {
217
+ if (/\.html$/i.test(file.extname)) {
218
+ let content = file.toString();
219
+ if (content.includes('</body>')) {
220
+ content = content.replace('</body>', `${LIVE_RELOAD_SCRIPT}\n</body>`);
221
+ } else {
222
+ content += `\n${LIVE_RELOAD_SCRIPT}`;
223
+ }
224
+ file.contents = Buffer.from(content);
225
+ }
226
+ return file;
227
+ });
228
+ }
229
+
230
+ await pipeline.dest(config.dest.html);
231
+ }
232
+ }
@@ -0,0 +1,14 @@
1
+ export * from './clean.js';
2
+ export * from './style.js';
3
+ export * from './script.js';
4
+ export * from './html.js';
5
+ export * from './copy.js';
6
+ export * from './remove.js';
7
+ export * from './replace.js';
8
+ export * from './cache.js';
9
+ export * from './publish.js';
10
+ export * from './pwa.js';
11
+ export * from './zip.js';
12
+ export * from './ftp.js';
13
+ export * from './server.js';
14
+ export * from './static.js';
@@ -0,0 +1,71 @@
1
+ import path from 'node:path';
2
+ import { BaseTask } from '../runner/task.js';
3
+ import { BalmConfig, TemplateOption, RenameOptions } from '../types/index.js';
4
+ import { Pipeline } from '../pipeline/pipeline.js';
5
+ import { transformRename } from '../pipeline/transforms/rename.js';
6
+ import { logger } from '../utilities/logger.js';
7
+
8
+ export interface PublishTaskOptions {
9
+ input?: string | TemplateOption[];
10
+ output?: string;
11
+ renameOptions?: string | Function | RenameOptions;
12
+ }
13
+
14
+ export class PublishTask extends BaseTask {
15
+ private options: PublishTaskOptions;
16
+
17
+ constructor(options: PublishTaskOptions = {}) {
18
+ super({ name: 'publish', description: 'Publish assets or templates' });
19
+ this.options = options;
20
+ }
21
+
22
+ async run(config: BalmConfig): Promise<void> {
23
+ if (!config.env.isProd) {
24
+ logger.warn('publish', '`mix.publish()` is only supported in production mode');
25
+ return;
26
+ }
27
+
28
+ const { input, output, renameOptions } = this.options;
29
+
30
+ if (Array.isArray(input)) {
31
+ for (const item of input) {
32
+ await this.publishItem(config, item.input, item.output, item.renameOptions);
33
+ }
34
+ } else if (typeof input === 'string' && output) {
35
+ await this.publishItem(config, input, output, renameOptions);
36
+ } else {
37
+ // Default: publish all static assets from dest.base to assets.static
38
+ const srcDir = config.dest.base;
39
+ const targetDir = config.assets.static || path.join(config.assets.root, config.assets.mainDir);
40
+
41
+ const pipeline = Pipeline.from(['**/*'], {
42
+ cwd: srcDir,
43
+ base: srcDir
44
+ });
45
+
46
+ await pipeline.dest(path.resolve(config.workspace, targetDir));
47
+ }
48
+ }
49
+
50
+ private async publishItem(
51
+ config: BalmConfig,
52
+ input: string,
53
+ output: string,
54
+ rename?: string | Function | RenameOptions
55
+ ): Promise<void> {
56
+ const srcPath = path.isAbsolute(input) ? input : path.join(config.dest.base, input);
57
+ const destDir = path.isAbsolute(output)
58
+ ? output
59
+ : path.join(config.workspace, config.assets.root, output);
60
+
61
+ const pipeline = Pipeline.from(srcPath, {
62
+ cwd: config.dest.base
63
+ });
64
+
65
+ if (rename) {
66
+ pipeline.pipe(transformRename(rename));
67
+ }
68
+
69
+ await pipeline.dest(destDir);
70
+ }
71
+ }
@@ -0,0 +1,99 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { generateSW, injectManifest } from 'workbox-build';
6
+ import { BaseTask } from '../runner/task.js';
7
+ import { BalmConfig } from '../types/index.js';
8
+ import { file as fsUtil } from '../utilities/file.js';
9
+ import { logger } from '../utilities/logger.js';
10
+
11
+ export interface PwaTaskOptions {
12
+ mode?: 'generateSW' | 'injectManifest';
13
+ options?: Record<string, any>;
14
+ }
15
+
16
+ export class PwaTask extends BaseTask {
17
+ private options: PwaTaskOptions;
18
+
19
+ constructor(options: PwaTaskOptions = {}) {
20
+ super({ name: 'pwa', description: 'Generate Progressive Web App Service Worker' });
21
+ this.options = options;
22
+ }
23
+
24
+ async run(config: BalmConfig): Promise<void> {
25
+ const globDirectory = config.dest.base;
26
+ const swDest = path.join(globDirectory, config.pwa.swDestFilename);
27
+ const swSrc = path.join(config.src.base, config.pwa.swSrcFilename);
28
+
29
+ // 1. Copy workbox-sw.js and workbox-sw.js.map to dest
30
+ const require = createRequire(import.meta.url);
31
+ const candidatePaths: string[] = [
32
+ path.join(config.workspace, 'node_modules/workbox-sw/build/workbox-sw.js'),
33
+ path.join(config.workspace, 'node_modules/workbox-sw/controllers/WorkboxSW.mjs')
34
+ ];
35
+ try {
36
+ const resolved = require.resolve('workbox-sw/build/workbox-sw.js');
37
+ candidatePaths.unshift(resolved);
38
+ } catch {}
39
+ try {
40
+ const resolvedPkg = require.resolve('workbox-sw');
41
+ candidatePaths.push(path.join(path.dirname(resolvedPkg), 'build/workbox-sw.js'));
42
+ } catch {}
43
+
44
+ for (const p of candidatePaths) {
45
+ if (existsSync(p)) {
46
+ const destFile = path.join(globDirectory, 'workbox-sw.js');
47
+ await fs.copyFile(p, destFile);
48
+ const mapFile = `${p}.map`;
49
+ if (existsSync(mapFile)) {
50
+ await fs.copyFile(mapFile, path.join(globDirectory, 'workbox-sw.js.map'));
51
+ }
52
+ break;
53
+ }
54
+ }
55
+
56
+ // 2. Generate or inject manifest
57
+ const mode = this.options.mode || config.pwa.mode || 'generateSW';
58
+ const mergedOptions: any = {
59
+ globDirectory,
60
+ swDest,
61
+ ...config.pwa.options,
62
+ ...this.options.options
63
+ };
64
+
65
+ if (mode === 'injectManifest') {
66
+ mergedOptions.swSrc = mergedOptions.swSrc || swSrc;
67
+ }
68
+
69
+ try {
70
+ if (mode === 'generateSW') {
71
+ const { count, size } = await generateSW(mergedOptions);
72
+ logger.info('pwa', `Generated '${swDest}', precaching ${count} files (${size} bytes)`);
73
+ } else {
74
+ const { count, size } = await injectManifest(mergedOptions);
75
+ logger.info('pwa', `Injected manifest into '${swDest}', precaching ${count} files (${size} bytes)`);
76
+ }
77
+
78
+ // 3. Replace version placeholder in swDest
79
+ if (existsSync(swDest) && config.pwa.version) {
80
+ let swContent = await fs.readFile(swDest, 'utf-8');
81
+ swContent = swContent
82
+ .replaceAll('{{ version }}', config.pwa.version)
83
+ .replaceAll('{{version}}', config.pwa.version);
84
+ await fs.writeFile(swDest, swContent, 'utf-8');
85
+ }
86
+
87
+ // 4. Ensure both service-worker.js and sw.js exist in dest for compatibility
88
+ const swJsDest = path.join(globDirectory, 'sw.js');
89
+ const serviceWorkerJsDest = path.join(globDirectory, 'service-worker.js');
90
+ if (existsSync(serviceWorkerJsDest) && !existsSync(swJsDest)) {
91
+ await fs.copyFile(serviceWorkerJsDest, swJsDest);
92
+ } else if (existsSync(swJsDest) && !existsSync(serviceWorkerJsDest)) {
93
+ await fs.copyFile(swJsDest, serviceWorkerJsDest);
94
+ }
95
+ } catch (err: any) {
96
+ logger.warn('pwa', `Service worker generation failed: ${err.message || err}`);
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,24 @@
1
+ import path from 'node:path';
2
+ import { BaseTask } from '../runner/task.js';
3
+ import { BalmConfig } from '../types/index.js';
4
+ import { file } from '../utilities/file.js';
5
+ import { logger } from '../utilities/logger.js';
6
+
7
+ export class RemoveTask extends BaseTask {
8
+ private target: string | string[];
9
+
10
+ constructor(target: string | string[]) {
11
+ super({ name: 'remove', description: 'Remove files and directories' });
12
+ this.target = target;
13
+ }
14
+
15
+ async run(config: BalmConfig): Promise<void> {
16
+ const targets = Array.isArray(this.target) ? this.target : [this.target];
17
+ const resolvedTargets = targets.map((t) =>
18
+ path.isAbsolute(t) ? t : path.join(config.workspace, t)
19
+ );
20
+
21
+ const deleted = await file.remove(resolvedTargets);
22
+ logger.warn('remove', `Removed: ${deleted.join(', ')}`);
23
+ }
24
+ }
@@ -0,0 +1,34 @@
1
+ import path from 'node:path';
2
+ import { BaseTask } from '../runner/task.js';
3
+ import { BalmConfig, ReplaceOptions } from '../types/index.js';
4
+ import { Pipeline } from '../pipeline/pipeline.js';
5
+ import { transformReplace } from '../pipeline/transforms/replace.js';
6
+
7
+ export interface ReplaceTaskOptions {
8
+ input: string | string[];
9
+ output: string;
10
+ options: ReplaceOptions | ReplaceOptions[];
11
+ }
12
+
13
+ export class ReplaceTask extends BaseTask {
14
+ private taskOptions: ReplaceTaskOptions;
15
+
16
+ constructor(taskOptions: ReplaceTaskOptions) {
17
+ super({ name: 'replace', description: 'Replace content in files' });
18
+ this.taskOptions = taskOptions;
19
+ }
20
+
21
+ async run(config: BalmConfig): Promise<void> {
22
+ const pipeline = Pipeline.from(this.taskOptions.input, {
23
+ cwd: config.workspace
24
+ });
25
+
26
+ pipeline.pipe(transformReplace(this.taskOptions.options));
27
+
28
+ const outDir = path.isAbsolute(this.taskOptions.output)
29
+ ? this.taskOptions.output
30
+ : path.join(config.workspace, this.taskOptions.output);
31
+
32
+ await pipeline.dest(outDir);
33
+ }
34
+ }
@@ -0,0 +1,28 @@
1
+ import { BaseTask } from '../runner/task.js';
2
+ import { BalmConfig, BundlerType } from '../types/index.js';
3
+ import { runWebpack } from '../bundler/webpack.js';
4
+ import { runRollup } from '../bundler/rollup.js';
5
+ import { runEsbuild } from '../bundler/esbuild.js';
6
+
7
+ export class ScriptTask extends BaseTask {
8
+ constructor() {
9
+ super({ name: 'script', description: 'Bundle JavaScript/TypeScript' });
10
+ }
11
+
12
+ async run(config: BalmConfig): Promise<void> {
13
+ const bundler = config.scripts.bundler;
14
+
15
+ switch (bundler) {
16
+ case BundlerType.rollup:
17
+ await runRollup(config);
18
+ break;
19
+ case BundlerType.esbuild:
20
+ await runEsbuild(config);
21
+ break;
22
+ case BundlerType.webpack:
23
+ default:
24
+ await runWebpack(config);
25
+ break;
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,183 @@
1
+ import http from 'node:http';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import connect from 'connect';
5
+ import serveStatic from 'serve-static';
6
+ import { WebSocketServer, WebSocket } from 'ws';
7
+ import { createProxyMiddleware } from 'http-proxy-middleware';
8
+ import { BaseTask } from '../runner/task.js';
9
+ import { BalmConfig } from '../types/index.js';
10
+ import { FileWatcher } from '../watcher/index.js';
11
+ import { logger } from '../utilities/logger.js';
12
+ import pc from 'picocolors';
13
+
14
+ export class ServerTask extends BaseTask {
15
+ private watcher: FileWatcher | null = null;
16
+ private wsServer: WebSocketServer | null = null;
17
+ private clients = new Set<WebSocket>();
18
+
19
+ constructor() {
20
+ super({ name: 'serve', description: 'Start development server with live reload' });
21
+ }
22
+
23
+ broadcast(data: { type: 'reload' | 'reload-css'; file?: string }): void {
24
+ const payload = JSON.stringify(data);
25
+ for (const client of this.clients) {
26
+ if (client.readyState === WebSocket.OPEN) {
27
+ client.send(payload);
28
+ }
29
+ }
30
+ }
31
+
32
+ async run(config: BalmConfig): Promise<void> {
33
+ if (config.env.isProd) {
34
+ logger.warn('server', 'Development server is disabled in production mode');
35
+ return;
36
+ }
37
+
38
+ const app = connect();
39
+ const port = config.server.port || 3000;
40
+ const host = config.server.host || 'localhost';
41
+
42
+ // Rewrite %PUBLIC_URL% in request URLs
43
+ app.use((req: any, _res: any, next: any) => {
44
+ if (req.url?.includes('%PUBLIC_URL%')) {
45
+ req.url = req.url.replace('/%PUBLIC_URL%', '').replace('%PUBLIC_URL%', '') || '/';
46
+ }
47
+ next();
48
+ });
49
+
50
+ // Dynamic manifest.json replacement for %PUBLIC_URL%
51
+ app.use((req: any, res: any, next: any) => {
52
+ if (req.url === '/manifest.json' || req.url?.endsWith('/manifest.json')) {
53
+ const candidates = [
54
+ path.join(config.dest.base, 'manifest.json'),
55
+ path.join(config.src.base, 'manifest.json'),
56
+ path.join(config.workspace, 'manifest.json')
57
+ ];
58
+ const file = candidates.find((f) => fs.existsSync(f));
59
+ if (file) {
60
+ let content = fs.readFileSync(file, 'utf-8');
61
+ const replacement = config.assets.virtualDir ? `/${config.assets.virtualDir}` : '';
62
+ content = content.replaceAll('%PUBLIC_URL%', replacement);
63
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
64
+ return res.end(content);
65
+ }
66
+ }
67
+ next();
68
+ });
69
+
70
+ // Static assets routing
71
+ if (config.paths.source.css) {
72
+ app.use('/' + config.paths.source.css, serveStatic(config.dest.css));
73
+ }
74
+ if (config.paths.tmp.css && config.paths.tmp.css !== config.paths.source.css) {
75
+ app.use('/' + config.paths.tmp.css, serveStatic(config.dest.css));
76
+ }
77
+
78
+ if (config.paths.source.js) {
79
+ app.use('/' + config.paths.source.js, serveStatic(config.dest.js));
80
+ }
81
+ if (config.paths.tmp.js && config.paths.tmp.js !== config.paths.source.js) {
82
+ app.use('/' + config.paths.tmp.js, serveStatic(config.dest.js));
83
+ }
84
+ app.use('/chunk', serveStatic(path.join(config.dest.js, 'chunk')));
85
+ app.use('/chunk', serveStatic(path.join(config.dest.base, 'chunk')));
86
+
87
+ if (config.paths.source.img) {
88
+ app.use(
89
+ '/' + config.paths.source.img,
90
+ serveStatic(path.join(config.src.base, config.paths.source.img))
91
+ );
92
+ }
93
+ if (config.paths.tmp.img) {
94
+ app.use('/' + config.paths.tmp.img, serveStatic(config.dest.img));
95
+ }
96
+ app.use('/img', serveStatic(path.join(config.dest.base, 'img')));
97
+ app.use('/img', serveStatic(path.join(config.dest.js, 'img')));
98
+
99
+ if (config.paths.source.font) {
100
+ app.use(
101
+ '/' + config.paths.source.font,
102
+ serveStatic(path.join(config.src.base, config.paths.source.font))
103
+ );
104
+ }
105
+ if (config.paths.tmp.font) {
106
+ app.use('/' + config.paths.tmp.font, serveStatic(config.dest.font));
107
+ }
108
+ app.use('/font', serveStatic(path.join(config.dest.base, 'font')));
109
+ app.use('/font', serveStatic(path.join(config.dest.js, 'font')));
110
+ app.use('/fonts', serveStatic(path.join(config.src.base, 'fonts')));
111
+ app.use('/fonts', serveStatic(path.join(config.dest.base, 'fonts')));
112
+
113
+ app.use(serveStatic(config.dest.base));
114
+ app.use(serveStatic(config.src.base));
115
+ app.use(serveStatic(config.workspace));
116
+
117
+ // History API fallback for SPA
118
+ if (config.server.historyOptions) {
119
+ app.use((req: any, res: any, next: any) => {
120
+ const isGetOrHead = req.method === 'GET' || req.method === 'HEAD';
121
+ const isHtmlNav = !req.url?.includes('.') || req.headers.accept?.includes('text/html');
122
+ if (isGetOrHead && isHtmlNav) {
123
+ const indexPath = path.join(config.dest.html, 'index.html');
124
+ const srcIndexPath = path.join(config.src.base, 'index.html');
125
+ const fileToServe = fs.existsSync(indexPath) ? indexPath : srcIndexPath;
126
+ if (fs.existsSync(fileToServe)) {
127
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
128
+ if (req.method === 'HEAD') {
129
+ return res.end();
130
+ }
131
+ return fs.createReadStream(fileToServe).pipe(res);
132
+ }
133
+ }
134
+ next();
135
+ });
136
+ }
137
+
138
+ // Proxy middleware
139
+ if (config.server.proxy && typeof config.server.proxy === 'object') {
140
+ const proxyEntries = Array.isArray(config.server.proxy)
141
+ ? config.server.proxy
142
+ : [config.server.proxy];
143
+
144
+ for (const entry of proxyEntries) {
145
+ if (entry.context && entry.options) {
146
+ app.use(createProxyMiddleware(entry.options));
147
+ }
148
+ }
149
+ }
150
+
151
+ const server = http.createServer(app);
152
+
153
+ // WebSocket Live Reload Server
154
+ this.wsServer = new WebSocketServer({ server, path: '/__balm_ws' });
155
+ this.wsServer.on('connection', (socket) => {
156
+ this.clients.add(socket);
157
+ socket.on('close', () => this.clients.delete(socket));
158
+ });
159
+
160
+ // Start File Watcher
161
+ this.watcher = new FileWatcher(config, (data) => {
162
+ this.broadcast(data);
163
+ });
164
+ this.watcher.start();
165
+
166
+ return new Promise((resolve) => {
167
+ server.listen(port, host, () => {
168
+ console.log('');
169
+ logger.success('server', pc.bold('BalmJS Dev Server is ready!'));
170
+ console.log(` ${pc.green('➜')} ${pc.bold('Local:')} ${pc.cyan(`http://${host}:${port}/`)}`);
171
+ console.log(` ${pc.green('➜')} ${pc.bold('Network:')} ${pc.cyan(`http://127.0.0.1:${port}/`)}`);
172
+ console.log('');
173
+
174
+ if (config.env.isTest) {
175
+ this.watcher?.stop();
176
+ this.wsServer?.close();
177
+ server.close();
178
+ resolve();
179
+ }
180
+ });
181
+ });
182
+ }
183
+ }