@serenity-is/tsbuild 9.1.6 → 10.0.3

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 (3) hide show
  1. package/dist/index.d.ts +69 -34
  2. package/dist/index.js +285 -169
  3. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -1,65 +1,100 @@
1
- export const defaultEntryPointGlobs: string[];
2
- export const importAsGlobalsMapping: Record<string, string>;
3
-
4
- export interface TSBuildOptions {
1
+ import esbuild from "esbuild";
2
+ import { type GlobOptions } from "glob";
3
+ export declare const defaultEntryPointGlobs: string[];
4
+ /**
5
+ * Default mapping for importing modules as globals. corelib, domwise, extensions, pro.extensions, sleekgrid
6
+ * are all mapped to Serenity global namespace */
7
+ export declare const importAsGlobalsMapping: Record<string, string>;
8
+ export declare function safeGlobSync(globs: string[], options?: Omit<GlobOptions, "ignore">): string[];
9
+ /** Default esbuild options used by TSBuild */
10
+ export declare const tsbuildDefaults: Partial<import("esbuild").BuildOptions>;
11
+ export interface TSBuildOptions extends Partial<import("esbuild").BuildOptions> {
12
+ /** Enable building of global iife bundles from Modules/Common/esm/bundles/*-bundle(.css|.ts) files to wwwroot/esm/bundles/. Default is false.
13
+ * If set to an object, uses the passed options for building global bundles. */
14
+ buildGlobalBundles?: boolean | TSBuildOptions;
5
15
  /** Enable bundling of dependencies, default is true */
6
16
  bundle?: boolean;
7
-
8
17
  /** Chunk names to generate when code splitting is enabled. Default is '_chunks/[name]-[hash]' */
9
- chunkNames?: string[];
10
-
18
+ chunkNames?: string;
11
19
  /** True to enable the clean plugin. Default is true if splitting is true. */
12
- clean?: boolean;
13
-
20
+ clean?: boolean | CleanPluginOptions;
21
+ /** Options for compressing output files. If specified, enables compression. Currently only available when writeIfChanged plugin is enabled */
22
+ compress?: CompressOptions;
14
23
  /**
15
- * Determines the set of entry points that should be passed to the esbuild.
24
+ * Determines the set of entry points that should be passed to the esbuild.
16
25
  * Only use to specify full paths of entry points manually if you calculated them yourself.
17
26
  * Prefer specifying entry point globs in sergen.json under TSBuild:EntryPoints which supports
18
27
  * globs and defaults to ['Modules/** /*Page.ts', 'Modules/** /*Page.tsx', 'Modules/** /ScriptInit.ts'] */
19
28
  entryPoints?: string[];
20
-
21
29
  /**
22
- * A set of mappings to pass to the importAsGlobalsPlugin. If this is undefined or any object and the plugins
30
+ * A set of mappings to pass to the importAsGlobalsPlugin. If this is undefined or any object and the plugins
23
31
  * is not specified, importAsGlobals plugin is enabled */
24
- importAsGlobals?: Record<string, string>;
25
-
32
+ importAsGlobals?: Record<string, string> | null;
26
33
  /**
27
34
  * True to enable metafile generation by esbuild. Default is true.
28
35
  * If this is false, clean plugin won't work properly.
29
36
  */
30
37
  metafile?: boolean;
31
-
32
38
  /** True to enable minification. Default is true. */
33
39
  minify?: boolean;
34
-
35
40
  /** False to not call npmCopy automatically for entries in appsettings.bundles.json that start with `~/npm/`. Default is true.*/
36
41
  npmCopy?: boolean;
37
-
38
42
  /** Base directory for calculating output file locations in output directory. Default is "./" */
39
43
  outbase?: string;
40
-
41
44
  /** Base output directory. Default is wwwroot/esm */
42
- outdir?: boolean;
43
-
45
+ outdir?: string;
44
46
  /** True to enable code splitting. Default is true unless --nosplit is passed in process arguments. */
45
47
  splitting?: boolean;
46
-
47
- /** Set of plugins for esbuild */
48
- plugins?: any[];
49
-
50
48
  /** Should source maps be generated. Default is true. */
51
49
  sourcemap?: boolean;
52
-
53
- /* Javascript target for output files. Default is es6. */
54
50
  target?: string;
55
-
56
51
  /** True to watch, default is calculated from process args and true if it contains --watch */
57
52
  watch?: boolean;
53
+ /** Write output files only if contents have changed. Default is true. */
54
+ writeIfChanged?: boolean;
58
55
  }
59
-
60
- /** Processes passed options and converts it to options suitable for esbuild */
61
- export const esbuildOptions: (opt: TSBuildOptions) => any;
62
- export const build: (opt: TSBuildOptions) => Promise<void>;
63
- export function importAsGlobalsPlugin(mapping: Record<string, string>): any;
64
- export function cleanPlugin(): any;
65
- export function npmCopy(paths: string[], outdir?: string): void;
56
+ /** Processes passed TSBuildOptions options and converts it to options suitable for esbuild */
57
+ export declare const esbuildOptions: (opt: TSBuildOptions) => import("esbuild").BuildOptions;
58
+ /** Default options for global iife bundle builds which is used when buildGlobalBundles is true or is an object */
59
+ export declare const tsbuildGlobalBundleDefaults: Partial<TSBuildOptions>;
60
+ /** Calls esbuild with passed options. By default, this is used to generate files under wwwroot/esm/ from entry points under Modules/
61
+ * but this can be changed by passing outdir and outbase, and other options. */
62
+ export declare const build: (opt: TSBuildOptions) => Promise<void>;
63
+ /** Plugin for importing modules as globals */
64
+ export declare function importAsGlobalsPlugin(mapping: Record<string, string>): {
65
+ name: string;
66
+ setup(build: esbuild.PluginBuild): void;
67
+ };
68
+ /** Default options for cleanPlugin */
69
+ export declare const cleanPluginDefaults: {
70
+ globs: string[];
71
+ logDeletedFiles: boolean;
72
+ };
73
+ /** Options for cleanPlugin */
74
+ export interface CleanPluginOptions {
75
+ /** Glob patterns to include for cleaning. Default is ['** /*.js', '** /*.js.map', '** /*.css', '** /*.css.map'] */
76
+ globs?: string[];
77
+ /** Whether to log deleted files to console. Default is true */
78
+ logDeletedFiles?: boolean;
79
+ }
80
+ /** Plugin for cleaning output directory based on passed globs */
81
+ export declare function cleanPlugin(opt: CleanPluginOptions): {
82
+ name: string;
83
+ setup(build: esbuild.PluginBuild): void;
84
+ };
85
+ export interface CompressOptions {
86
+ brotli?: boolean | {
87
+ quality?: number;
88
+ };
89
+ gzip?: boolean | {
90
+ level?: number;
91
+ };
92
+ extensions?: string[];
93
+ }
94
+ /** Plugin for writing files only if changed */
95
+ export declare function writeIfChanged(opt?: CompressOptions): {
96
+ name: string;
97
+ setup(build: esbuild.PluginBuild): void;
98
+ };
99
+ /** Copies files from node_modules to outdir (wwwroot/npm by default). Paths are relative to node_modules. */
100
+ export declare function npmCopy(paths: string[]): void;
package/dist/index.js CHANGED
@@ -1,217 +1,301 @@
1
1
  import esbuild from "esbuild";
2
- import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, rmSync, readFileSync } from "fs";
3
- import { dirname, join, relative, resolve } from "path";
2
+ import { createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
4
3
  import { globSync } from "glob";
4
+ import pipe from "node:stream/promises";
5
+ import { constants, createBrotliCompress, createGzip } from "node:zlib";
6
+ import { dirname, isAbsolute, join, resolve, sep } from "path";
5
7
 
6
- export const defaultEntryPointGlobs = ['Modules/**/*Page.ts', 'Modules/**/*Page.tsx', 'Modules/**/ScriptInit.ts', 'Modules/**/*.mts'];
8
+ export const defaultEntryPointGlobs = [
9
+ "Modules/**/*Page.ts",
10
+ "Modules/**/*Page.tsx",
11
+ "Modules/**/ScriptInit.ts",
12
+ "Modules/**/*.mts"
13
+ ];
7
14
 
8
15
  export const importAsGlobalsMapping = {
9
- "@serenity-is/base": "Serenity",
10
16
  "@serenity-is/corelib": "Serenity",
11
- "@serenity-is/corelib/q": "Q",
12
- "@serenity-is/corelib/slick": "Slick",
13
- "@serenity-is/sleekgrid": "Slick",
14
- "@serenity-is/extensions": "Serenity.Extensions",
15
- "@serenity-is/pro.extensions": "Serenity"
16
- }
17
-
18
- export const esbuildOptions = (opt) => {
17
+ "@serenity-is/domwise": "Serenity",
18
+ "@serenity-is/domwise/jsx-runtime": "Serenity",
19
+ "@serenity-is/extensions": "Serenity",
20
+ "@serenity-is/pro.extensions": "Serenity",
21
+ "@serenity-is/sleekgrid": "Serenity"
22
+ };
19
23
 
20
- opt = Object.assign({}, opt);
24
+ export function safeGlobSync(globs, options = {}) {
25
+ const root = options.root || process.cwd();
26
+ const normalizePattern = (pattern) => {
27
+ let normalized = pattern.replace(/\\/g, "/");
28
+ if (normalized.startsWith("/")) {
29
+ normalized = "." + normalized;
30
+ } else if (!normalized.includes("/")) {
31
+ normalized = "**/" + normalized;
32
+ }
33
+ if (isAbsolute(normalized) || normalized.includes("..") || /^[a-zA-Z]:/.test(normalized)) {
34
+ throw new Error(`Invalid pattern: ${pattern}`);
35
+ }
36
+ return normalized;
37
+ };
38
+ globs = globs.map((x) => x?.trim()).filter((x) => x.length > 0);
39
+ const includes = globs.filter((x) => !x.startsWith("!")).map(normalizePattern);
40
+ const excludes = globs.filter((x) => x.startsWith("!")).map((x) => normalizePattern(x.substring(1)));
41
+ const results = globSync(includes, {
42
+ nodir: true,
43
+ ignore: excludes,
44
+ matchBase: true,
45
+ cwd: root,
46
+ ...options
47
+ });
48
+ const resolvedRoot = resolve(root);
49
+ return results.filter((file) => {
50
+ const fullPath = resolve(root, file);
51
+ return fullPath.startsWith(resolvedRoot + sep) || fullPath === resolvedRoot;
52
+ });
53
+ }
21
54
 
22
- var entryPointsRegEx;
23
- if (opt.entryPointsRegEx !== undefined) {
24
- entryPointsRegEx = opt.entryPointsRegEx;
25
- delete opt.entryPointsRegEx;
26
- }
55
+ export const tsbuildDefaults = {
56
+ assetNames: "assets/[name]-[hash]",
57
+ bundle: true,
58
+ chunkNames: "_chunks/[name]-[hash]",
59
+ color: true,
60
+ format: "esm",
61
+ jsxSideEffects: true,
62
+ keepNames: true,
63
+ loader: {
64
+ ".woff2": "file",
65
+ ".woff": "file",
66
+ ".ttf": "file",
67
+ ".eot": "file",
68
+ ".svg": "file",
69
+ ".png": "file",
70
+ ".jpg": "file",
71
+ ".jpeg": "file",
72
+ ".gif": "file",
73
+ ".webp": "file"
74
+ },
75
+ logLevel: "info",
76
+ metafile: true,
77
+ minify: true,
78
+ outbase: "./",
79
+ outdir: "wwwroot/esm",
80
+ sourcemap: true,
81
+ target: "es2017"
82
+ };
27
83
 
28
- var entryPointRoots = ['Modules'];
29
- if (opt.entryPointRoots !== undefined) {
30
- entryPointRoots = opt.entryPointRoots;
31
- delete opt.entryPointRoots;
84
+ function isSplittingEnabled(opt) {
85
+ if (opt.splitting !== void 0) {
86
+ return !!opt.splitting;
32
87
  }
88
+ return (opt.format == null || opt.format === "esm") && !globalThis.process.argv.slice(2).some((x) => x == "--nosplit");
89
+ }
33
90
 
91
+ function cleanPluginOptions(opt) {
92
+ if (opt.plugins === void 0)
93
+ return null;
94
+ if (opt.clean === void 0 && isSplittingEnabled(opt) || opt.clean)
95
+ return opt.clean === true ? {} : opt.clean ?? {};
96
+ return null;
97
+ }
34
98
 
35
- var entryPoints = opt.entryPoints;
99
+ export const esbuildOptions = (opt) => {
100
+ opt = Object.assign({}, opt);
101
+ if (opt.entryPointsRegex !== void 0 || opt.entryPointRoots !== void 0) {
102
+ throw new Error("TSBuildOptions.entryPointsRegex and entryPointRoots are deprecated, use TSBuild:EntryPoints in sergen.json.");
103
+ }
104
+ let entryPoints = opt.entryPoints;
36
105
  if (entryPoints === void 0) {
37
106
  let globs;
38
- if (existsSync('sergen.json')) {
39
- var json = readFileSync('sergen.json', 'utf8').trim();
40
- var cfg = JSON.parse(json || {});
107
+ if (existsSync("sergen.json")) {
108
+ var json = readFileSync("sergen.json", "utf8").trim();
109
+ var cfg = JSON.parse(json || "{}");
41
110
  globs = cfg?.TSBuild?.EntryPoints;
42
- if (globs != null && globs[0] === '+') {
111
+ if (globs != null && globs[0] === "+") {
43
112
  globs = [...defaultEntryPointGlobs, ...globs.slice(1)];
44
113
  }
45
- if (globs === void 0 &&
46
- typeof cfg.Extends == "string" &&
47
- existsSync(cfg.Extends)) {
48
- json = readFileSync(cfg.Extends, 'utf8').trim();
49
- cfg = JSON.parse(json || {});
114
+ if (globs === void 0 && typeof cfg.Extends == "string" && existsSync(cfg.Extends)) {
115
+ json = readFileSync(cfg.Extends, "utf8").trim();
116
+ cfg = JSON.parse(json || "{}");
50
117
  globs = cfg?.TSBuild?.EntryPoints;
51
- if (globs != null && globs[0] === '+') {
118
+ if (globs != null && globs[0] === "+") {
52
119
  globs = [...defaultEntryPointGlobs, ...globs.slice(1)];
53
120
  }
54
121
  }
55
122
  }
56
-
57
- if (globs == null && !entryPointsRegEx) {
123
+ if (globs == null) {
58
124
  globs = defaultEntryPointGlobs;
59
125
  }
60
-
61
126
  if (globs != null) {
62
- var include = globs.filter(x => !x.startsWith('!'));
63
- var exclude = globs.filter(x => x.startsWith('!')).map(x => x.substring(1));
64
- exclude.push(".git/**");
65
- exclude.push("App_Data/**");
66
- exclude.push("bin/**");
67
- exclude.push("obj/**");
68
- exclude.push("node_modules/**");
69
- exclude.push("**/node_modules/**");
70
-
71
- entryPoints = globSync(include, {
72
- ignore: exclude,
73
- nodir: true,
74
- matchBase: true
75
- });
76
- }
77
- else {
78
- entryPoints = [];
79
- entryPointRoots.forEach(root =>
80
- scanDir(root)
81
- .filter(p => p.match(entryPointsRegEx))
82
- .forEach(p => entryPoints.push(root + '/' + p)));
127
+ globs.push("!.git/**");
128
+ globs.push("!App_Data/**");
129
+ globs.push("!bin/**");
130
+ globs.push("!obj/**");
131
+ globs.push("!node_modules/**");
132
+ entryPoints = safeGlobSync(globs);
83
133
  }
84
134
  }
85
-
86
- var splitting = opt.splitting;
87
- if (splitting === undefined)
88
- splitting = !process.argv.slice(2).some(x => x == "--nosplit");
89
-
90
- var plugins = opt.plugins;
91
- if (plugins === undefined) {
135
+ const splitting = isSplittingEnabled(opt);
136
+ let plugins = opt.plugins;
137
+ if (plugins === void 0) {
92
138
  plugins = [];
93
- if ((opt.clean === undefined && splitting) || opt.clean)
94
- plugins.push(cleanPlugin());
95
- if (opt.importAsGlobals === undefined || opt.importAsGlobals)
139
+ const cleanOpt = cleanPluginOptions(opt);
140
+ if (cleanOpt != null)
141
+ plugins.push(cleanPlugin(cleanOpt));
142
+ if (opt.importAsGlobals === void 0 || opt.importAsGlobals)
96
143
  plugins.push(importAsGlobalsPlugin(opt.importAsGlobals ?? importAsGlobalsMapping));
97
144
  }
98
-
99
- if (opt.write === undefined && opt.writeIfChanged === undefined || opt.writeIfChanged) {
100
- plugins.push(writeIfChanged());
145
+ if (opt.write === void 0 && opt.writeIfChanged === void 0 || opt.writeIfChanged) {
146
+ plugins.push(writeIfChanged(opt.compress));
101
147
  opt.write = false;
102
148
  }
103
-
149
+ delete opt.compress;
104
150
  delete opt.clean;
105
151
  delete opt.importAsGlobals;
106
152
  delete opt.writeIfChanged;
107
-
108
- return Object.assign({
109
- absWorkingDir: resolve('./'),
110
- bundle: true,
111
- chunkNames: '_chunks/[name]-[hash]',
112
- color: true,
113
- entryPoints: entryPoints,
114
- format: 'esm',
115
- keepNames: true,
116
- logLevel: 'info',
117
- metafile: true,
118
- minify: true,
119
- outbase: "./",
120
- outdir: 'wwwroot/esm',
153
+ if (opt.sourceRoot === void 0) {
154
+ if (existsSync("package.json")) {
155
+ let pkgId = JSON.parse(readFileSync("package.json", "utf8").trim() || "{}").name;
156
+ if (pkgId.startsWith("@serenity-is/")) {
157
+ opt.sourceRoot = "https://packages.serenity.is/" + pkgId.substring(13) + "/Modules/";
158
+ }
159
+ }
160
+ opt.sourceRoot ??= "Modules";
161
+ }
162
+ return {
163
+ ...tsbuildDefaults,
164
+ absWorkingDir: resolve("./"),
165
+ entryPoints,
121
166
  plugins,
122
- sourcemap: true,
123
- splitting: splitting,
124
- target: 'es2017',
125
- watch: process.argv.slice(2).some(x => x == "--watch"),
126
- }, opt);
127
- }
167
+ splitting,
168
+ // @ts-ignore
169
+ watch: process.argv.slice(2).some((x) => x == "--watch"),
170
+ ...opt
171
+ };
172
+ };
173
+
174
+ export const tsbuildGlobalBundleDefaults = {
175
+ entryPoints: [
176
+ "Modules/Common/bundles/*-bundle.ts",
177
+ "Modules/Common/bundles/*-bundle.css",
178
+ "Modules/Common/bundles/*-bundle.rtl.css"
179
+ ],
180
+ format: "iife",
181
+ importAsGlobals: null,
182
+ outdir: "wwwroot/esm/bundles/",
183
+ outbase: "Modules/Common/bundles",
184
+ watch: false
185
+ };
128
186
 
129
187
  export const build = async (opt) => {
130
- if (opt?.npmCopy !== false &&
131
- existsSync('appsettings.bundles.json')) {
132
- const bundlesJson = readFileSync('appsettings.bundles.json', 'utf8').trim();
133
- const bundlesCfg = JSON.parse(bundlesJson || {});
188
+ if (opt.buildGlobalBundles) {
189
+ const buildGlobalBundles = {
190
+ ...tsbuildGlobalBundleDefaults,
191
+ ...opt.buildGlobalBundles === true ? {} : opt.buildGlobalBundles
192
+ };
193
+ delete opt.buildGlobalBundles;
194
+ console.log("\x1B[32mBuilding global bundles...\x1B[0m");
195
+ await build(buildGlobalBundles);
196
+ let cleanOpt = cleanPluginOptions(opt);
197
+ if (cleanOpt != null) {
198
+ opt.clean = {
199
+ ...cleanOpt,
200
+ globs: [
201
+ "!./bundles/**",
202
+ ...cleanOpt.globs ?? cleanPluginDefaults.globs
203
+ ]
204
+ };
205
+ }
206
+ }
207
+ if (opt?.npmCopy !== false && existsSync("appsettings.bundles.json")) {
208
+ const bundlesJson = readFileSync("appsettings.bundles.json", "utf8").trim();
209
+ const bundlesCfg = JSON.parse(bundlesJson || "{}");
134
210
  const bundles = Object.values(bundlesCfg?.CssBundling?.Bundles || {}).concat(Object.values(bundlesCfg?.ScriptBundling?.Bundles || {}));
135
211
  let paths = [];
136
- Object.values(bundles).filter(x => x?.length).forEach(bundle => {
137
- paths.push(...bundle.filter(f => f?.startsWith('~/npm/')).map(f => f.substring(5)));
212
+ Object.values(bundles).filter((x) => x?.length).forEach((bundle) => {
213
+ paths.push(...bundle.filter((f) => f?.startsWith("~/npm/")).map((f) => f.substring(5)));
138
214
  });
139
- paths = paths.filter((v, i, a) => a.indexOf(v) === i); // unique
215
+ paths = paths.filter((v, i, a) => a.indexOf(v) === i);
140
216
  if (paths.length) {
141
217
  npmCopy(paths);
142
- }
218
+ }
143
219
  }
144
-
145
220
  delete opt?.npmCopy;
146
-
147
- opt = esbuildOptions(opt);
148
-
149
- if (opt.watch) {
150
- // this somehow resolves the issue that when debugging is stopped
151
- // in Visual Studio, the node process stays alive
221
+ const esopt = esbuildOptions(opt);
222
+ if (esopt.watch) {
152
223
  setInterval(() => {
153
224
  process.stdout.write("");
154
- }, 5000);
155
-
156
- delete opt.watch;
157
- const context = await esbuild.context(opt);
225
+ }, 5e3);
226
+ delete esopt.watch;
227
+ const context = await esbuild.context(esopt);
158
228
  await context.watch();
159
- }
160
- else {
161
- delete opt.watch;
162
- await esbuild.build(opt);
229
+ } else {
230
+ delete esopt.watch;
231
+ await esbuild.build(esopt);
163
232
  }
164
233
  };
165
234
 
166
- function scanDir(dir, org) {
167
- return readdirSync(dir).reduce((files, file) => {
168
- const absolute = join(dir, file);
169
- return [...files, ...(statSync(absolute).isDirectory()
170
- ? scanDir(absolute, org || dir)
171
- : [relative(org || dir, absolute)])]
172
- }, []);
173
- }
174
-
175
- // https://github.com/evanw/esbuild/issues/337
176
235
  export function importAsGlobalsPlugin(mapping) {
177
236
  const escRe = (s) => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
178
- const filter = new RegExp(Object.keys(mapping).map((mod) =>
179
- `^${escRe(mod)}$`).join("|"));
180
-
237
+ const filter = new RegExp(Object.keys(mapping).map((mod) => `^${escRe(mod)}$`).join("|"));
181
238
  return {
182
239
  name: "global-imports",
183
- setup(build) {
184
- build.onResolve({ filter }, (args) => {
240
+ setup(build2) {
241
+ build2.onResolve({ filter }, (args) => {
185
242
  if (!mapping[args.path])
186
243
  throw new Error("Unknown global: " + args.path);
187
244
  return { path: mapping[args.path], namespace: "external-global" };
188
245
  });
189
-
190
- build.onLoad({ filter: /.*/, namespace: "external-global" },
246
+ build2.onLoad(
247
+ { filter: /.*/, namespace: "external-global" },
191
248
  async (args) => {
192
249
  return { contents: `module.exports = ${args.path};`, loader: "js" };
193
- });
250
+ }
251
+ );
194
252
  }
195
253
  };
196
254
  }
197
255
 
198
- export function cleanPlugin() {
256
+ export const cleanPluginDefaults = {
257
+ globs: [
258
+ "*.css",
259
+ "*.css.map",
260
+ "*.js",
261
+ "*.js.map",
262
+ "*.jpg",
263
+ "*.png",
264
+ "*.gif",
265
+ "*.svg",
266
+ "*.woff",
267
+ "*.woff2",
268
+ "*.ttf",
269
+ "*.eot"
270
+ ],
271
+ logDeletedFiles: true
272
+ };
273
+ function cleanPlugin(opt) {
274
+ opt = Object.assign({}, cleanPluginDefaults, opt ?? {});
199
275
  return {
200
- name: 'clean',
201
- setup(build) {
202
- build.onEnd(result => {
276
+ name: "clean",
277
+ setup(build2) {
278
+ build2.onEnd((result) => {
203
279
  try {
280
+ const outdir = build2.initialOptions.outdir;
204
281
  const { outputs } = result.metafile ?? {};
205
- if (!outputs || !existsSync(build.initialOptions.outdir))
282
+ if (!outputs || !existsSync(outdir))
206
283
  return;
207
-
208
- const outputFiles = new Set(Object.keys(outputs));
209
- scanDir(build.initialOptions.outdir).forEach(file => {
210
- if (!file.endsWith('.js') && !file.endsWith('.js.map') && !file.endsWith('.css') && !file.endsWith('.css.map'))
211
- return;
212
- if (!outputFiles.has(join(build.initialOptions.outdir, file).replace(/\\/g, '/'))) {
213
- console.log('esbuild clean: deleting extra file ' + file);
214
- rmSync(join(build.initialOptions.outdir, file));
284
+ const outputFiles = new Set(Object.keys(outputs).map((x) => x.replace(/\\/g, "/")));
285
+ const existingFiles = safeGlobSync(opt.globs || [], {
286
+ cwd: outdir
287
+ }).map((x) => join(outdir, x).replace(/\\/g, "/"));
288
+ existingFiles.forEach((file) => {
289
+ if (!outputFiles.has(file)) {
290
+ if (opt.logDeletedFiles ?? true)
291
+ console.log(`esbuild clean: \x1B[33mdeleting extra file ${file}\x1B[0m`);
292
+ rmSync(file);
293
+ if (existsSync(file + ".gz")) {
294
+ rmSync(file + ".gz");
295
+ }
296
+ if (existsSync(file + ".br")) {
297
+ rmSync(file + ".br");
298
+ }
215
299
  }
216
300
  });
217
301
  } catch (e) {
@@ -219,59 +303,91 @@ export function cleanPlugin() {
219
303
  }
220
304
  });
221
305
  }
222
- }
223
- }
224
-
225
- export function checkIfTrigger() {
226
- // nop
306
+ };
227
307
  }
228
308
 
229
- export function writeIfChanged() {
309
+ export function writeIfChanged(opt) {
310
+ const compressExtensions = opt?.extensions ?? [".css", ".js", ".svg", ".json"];
230
311
  return {
231
312
  name: "write-if-changed",
232
- setup(build) {
233
- build.onEnd(result => {
234
- result.outputFiles?.forEach(file => {
313
+ setup(build2) {
314
+ build2.onEnd(async (result) => {
315
+ const start = (/* @__PURE__ */ new Date()).getTime();
316
+ let compressed = 0;
317
+ let written = 0;
318
+ let checkedFiles = 0;
319
+ let outputFiles = result.outputFiles || [];
320
+ for (const file of outputFiles) {
321
+ checkedFiles++;
322
+ const compressFile = async () => {
323
+ if ((opt?.brotli || opt?.gzip) && compressExtensions.some((ext) => file.path?.endsWith(ext))) {
324
+ if (opt.brotli) {
325
+ await pipe.pipeline(
326
+ createReadStream(file.path),
327
+ createBrotliCompress({
328
+ [constants.BROTLI_PARAM_QUALITY]: typeof opt.brotli === "object" && opt.brotli.quality || 4
329
+ }),
330
+ createWriteStream(`${file.path}.br`)
331
+ );
332
+ compressed++;
333
+ }
334
+ if (opt.gzip) {
335
+ await pipe.pipeline(
336
+ createReadStream(file.path),
337
+ createGzip({ level: typeof opt.gzip === "object" && opt.gzip.level || 7 }),
338
+ createWriteStream(`${file.path}.gz`)
339
+ );
340
+ compressed++;
341
+ }
342
+ }
343
+ };
235
344
  if (existsSync(file.path)) {
236
345
  const old = readFileSync(file.path);
237
- if (old.equals(file.contents))
238
- return;
239
- }
240
- else {
346
+ if (old.equals(file.contents)) {
347
+ if (opt?.brotli && !existsSync(file.path + ".br") || opt?.gzip && !existsSync(file.path + ".gz")) {
348
+ await compressFile();
349
+ }
350
+ continue;
351
+ }
352
+ } else {
241
353
  mkdirSync(dirname(file.path), { recursive: true });
242
354
  }
243
- writeFileSync(file.path, file.text);
244
- });
355
+ writeFileSync(file.path, file.contents);
356
+ written++;
357
+ await compressFile();
358
+ }
359
+ const end = (/* @__PURE__ */ new Date()).getTime();
360
+ if (written > 0 || compressed > 0)
361
+ console.log(`esbuild write: \x1B[32mChecked ${checkedFiles} output files in ${end - start} ms, changed ${written}, compressed ${compressed}\x1B[0m`);
362
+ else
363
+ console.log(`esbuild write: \x1B[32mChecked ${checkedFiles} output files in ${end - start} ms, none changed\x1B[0m`);
364
+ await Promise.resolve();
245
365
  });
246
366
  }
247
367
  };
248
368
  }
249
369
 
250
370
  export function npmCopy(paths) {
251
- paths.forEach(path => {
371
+ paths.forEach((path) => {
252
372
  const srcFile = join("node_modules", path);
253
373
  const dstfile = join("wwwroot/npm", path);
254
374
  if (!existsSync(srcFile)) {
255
375
  console.warn(`Source file not found: ${srcFile}`);
256
376
  return;
257
377
  }
258
-
259
- (function() {
378
+ (function () {
260
379
  const srcContent = readFileSync(srcFile);
261
380
  if (existsSync(dstfile)) {
262
381
  if (readFileSync(dstfile).equals(srcContent))
263
382
  return;
264
- }
265
- else {
383
+ } else {
266
384
  mkdirSync(dirname(dstfile), { recursive: true });
267
385
  }
268
386
  console.log(`Copying ${srcFile} to ${dstfile}`);
269
387
  writeFileSync(dstfile, srcContent);
270
388
  })();
271
-
272
389
  const js = path.endsWith(".js");
273
- if ((js && !path.endsWith(".min.js")) ||
274
- (path.endsWith(".css") && !path.endsWith(".min.css"))) {
390
+ if (js && !path.endsWith(".min.js") || path.endsWith(".css") && !path.endsWith(".min.css")) {
275
391
  const ext = js ? ".min.js" : ".min.css";
276
392
  const srcMinFile = srcFile.substring(0, srcFile.length - (js ? 3 : 4)) + ext;
277
393
  const dstMinFile = dstfile.substring(0, dstfile.length - (js ? 3 : 4)) + ext;
@@ -284,4 +400,4 @@ export function npmCopy(paths) {
284
400
  }
285
401
  }
286
402
  });
287
- }
403
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@serenity-is/tsbuild",
3
- "version": "9.1.6",
3
+ "version": "10.0.3",
4
4
  "author": "Serenity (https://serenity.is)",
5
5
  "bugs": "https://github.com/serenity-is/serenity/issues",
6
6
  "description": "Serenity ESBuild functions",
7
7
  "dependencies": {
8
8
  "esbuild": "0.27.0",
9
- "glob": "12.0.0"
9
+ "glob": "13.0.0"
10
10
  },
11
11
  "exports": {
12
12
  ".": {