build.ts   A
last analyzed

Complexity

Total Complexity 17
Complexity/F 0

Size

Lines of Code 448
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 17
eloc 352
mnd 17
bc 17
fnc 0
dl 0
loc 448
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
rs 10
1
/*!
2
 * Copyright (c) 2022 Pedro José Batista, licensed under the MIT License.
3
 * See the LICENSE.md file in the project root for more information.
4
 */
5
6
/* eslint-disable */
7
// Build routines for decimal.js-i18n (now in TypeScript!)
8
9
//#region Imports and import helpers - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
10
11
import rollupBabel, { getBabelOutputPlugin } from "@rollup/plugin-babel";
12
import rollupNode from "@rollup/plugin-node-resolve";
13
import chalk from "chalk";
14
import {
15
    copyFile,
16
    createWriteStream,
17
    emptyDir,
18
    ensureDir,
19
    move,
20
    readFile,
21
    readJson,
22
    readJsonSync,
23
    writeFile,
24
    writeJson,
25
} from "fs-extra";
26
import JsZip from "jszip";
27
import { dirname, join } from "path";
28
import eslint from "prettier-eslint";
29
import { hrtime, stderr, stdout } from "process";
30
import { OutputAsset, OutputChunk, rollup } from "rollup";
31
import rollupDts from "rollup-plugin-dts";
32
import rollupSourcemaps from "rollup-plugin-sourcemaps";
33
import type { RawSourceMap } from "source-map";
34
import { convertCompilerOptionsFromJson, createProgram } from "typescript";
35
import { minify as uglifyJs } from "uglify-js";
36
import _package from "./package.json";
37
import tsconfig from "./tsconfig.json";
38
39
//#endregion
40
41
//#region Constants- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
42
43
const prettierrc = readJsonSync(join(__dirname, ".prettierrc"));
44
const license = "/*!\n * Copyright (c) 2022 Pedro José Batista, licensed under the MIT License.\n * See the LICENSE.md file in the project root for more information.\n */\n"; // prettier-ignore
45
const version = _package.version;
46
47
const docBlockRegExp = /\/\*\*.*?\*\/\n/gms;
48
const globalRegExp = /declare global {\n {4}export class globalThis {\n {8}\/\*\* Used by the `extend` submodule to prevent it from loading directly from `decimal\.js`\. \*\/\n {8}static __Decimal__Class__Global__: Decimal.Constructor \| undefined;\n {4}\}\n\}/ms; // prettier-ignore
49
const mainCallRegExp = /main\(globalThis\.__Decimal__Class__Global__ \?\? require\("decimal\.js"\)\);/ms;
50
const mainEs5RegExp = /main\(\(_globalThis\$__Decimal = globalThis\.__Decimal__Class__Global__\) !== null && _globalThis\$__Decimal !== void 0 \? _globalThis\$__Decimal : require\("decimal\.js"\)\);/gms; // prettier-ignore
51
const licenseRegExp = /\/\*!\n\s+\* Copyright \(c\) 2022 Pedro José Batista, licensed under the MIT License\.\n\s+\* See the LICENSE\.md file in the project root for more information\.\n\s+\*\//gms; // prettier-ignore
52
53
const paths = {
54
    bundle: join(__dirname, "dist", "bundle"),
55
    dist: join(__dirname, "dist"),
56
    local: join(__dirname, "dist", "local"),
57
    node: join(__dirname, "dist", "node"),
58
    root: __dirname,
59
    source: join(__dirname, "src"),
60
};
61
62
//#endregion
63
64
//#region Helpers- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
65
66
interface CopyAndRenameConfig {
67
    from: string;
68
    to: string;
69
    oldExt?: string;
70
    oldName?: string;
71
    newName?: string;
72
    newExt?: string;
73
    postProcessor?: PostProcessor;
74
}
75
type PostProcessor = (contents: string) => Promise<string>;
76
type Task<T = unknown> = () => Promise<T> | T;
77
78
console.warn = () => {};
79
80
const cleanDocBlocks = (text: string) => text.replaceAll(docBlockRegExp, "");
81
const cleanLicenses = (text: string) => text.replaceAll(licenseRegExp, "");
82
83
const copyAndRename = async ({
84
    from,
85
    to,
86
    oldExt = ".js",
87
    oldName = "decimal-i18n",
88
    newName = "index",
89
    newExt = ".js",
90
    postProcessor,
91
}: CopyAndRenameConfig) => {
92
    const js = await readFile(join(from, oldName + oldExt));
93
    const map = await readJson(join(from, oldName + oldExt + ".map"));
94
95
    const newJs = js
96
        .toString()
97
        .replace(reference(oldName + ".d.ts"), reference(newName + ".d.ts"))
98
        .replace(sourceMappingUrl(oldName + oldExt), sourceMappingUrl(newName + newExt));
99
    map.file = newName + oldExt;
100
101
    await writeFile(
102
        join(to, newName + newExt),
103
        typeof postProcessor === "function" ? await postProcessor(newJs) : newJs,
104
    );
105
    await writeJson(join(to, newName + newExt + ".map"), map);
106
    await copyFile(join(from, oldName + ".d.ts"), join(to, newName + ".d.ts"));
107
};
108
109
const formatCjsExtend = (code: string) =>
110
    prettify(
111
        reference("extend.d.ts") + license + cleanDocBlocks(cleanLicenses(
112
            code
113
                .replace("export const extend", "const extend")
114
                .replace("export default extend;", "module.exports = extend;\nmodule.exports.default = extend;")
115
                .replace(sourceMappingUrl("extend.cjs.js"), sourceMappingUrl("extend.js")),
116
        )), // prettier-ignore
117
    );
118
119
const formatMjsExtend = (code: string) =>
120
    prettify(
121
        reference("extend.d.ts") +
122
            license +
123
            cleanDocBlocks(
124
                cleanLicenses(code.replace(sourceMappingUrl("extend.esm.js"), sourceMappingUrl("extend.mjs"))),
125
            ),
126
    );
127
128
const formatDts = (code: string) => prettify(license + cleanLicenses(code), ".d.ts");
129
130
const formatEs5 = (code: string) =>
131
    prettify(license + cleanDocBlocks(cleanLicenses(code)).replace(mainEs5RegExp, 'main(_decimal["default"]);'));
132
133
const formatEsm = (code: string) =>
134
    prettify(license + cleanDocBlocks(cleanLicenses(code)).replace(mainCallRegExp, "main(Decimal);"));
135
136
const formatReadme = (markdown: string) =>
137
    markdown.replace(
138
        /<h1.*?<\/h1>/ms,
139
        "# [![decimal.js-i18n](https://raw.githubusercontent.com/pjbatista/decimal.js-i18n/main/logo.svg)](https://github.com/pjbatista/decimal.js-i18n)",
140
    );
141
142
const generateOutput = async (
143
    outDir: string,
144
    postProcessor?: PostProcessor | OutputChunk | OutputAsset,
145
    ...chunks: Array<OutputChunk | OutputAsset>
146
) => {
147
    if (postProcessor && typeof postProcessor !== "function") {
148
        chunks.unshift(postProcessor);
149
        postProcessor = undefined;
150
    }
151
152
    for (const chunk of chunks) {
153
        if (chunk.type === "asset") continue;
154
155
        const code = typeof postProcessor === "function" ? await postProcessor(chunk.code) : chunk.code;
156
        const filePath = join(outDir, chunk.fileName.replace(".d.d.ts", ".d.ts"));
157
        const contents =
158
                (chunk.fileName.endsWith(".js")
159
                    ? `/// <reference path="${replaceLast(chunk.fileName, ".js", ".d.ts")}" />\n`
160
                    : chunk.fileName.endsWith(".mjs")
161
                        ? `/// <reference path="${replaceLast(chunk.fileName, ".mjs", ".d.ts")}" />\n`
162
                        : "") + (!chunk.map ? code : code + sourceMappingUrl(chunk.fileName) + "\n"); // prettier-ignore
163
        await ensureDir(dirname(filePath));
164
        await writeFile(filePath, contents);
165
166
        if (chunk.map) await writeFile(filePath + ".map", chunk.map.toString());
167
    }
168
};
169
170
const minify = (code: string, filePath: string, original?: RawSourceMap) =>
171
    uglifyJs(code, {
172
        sourceMap: { filename: filePath, content: original ?? "inline", includeSources: true, url: filePath + ".map" },
173
        toplevel: true,
174
    });
175
176
const prettify = (text: string, extension = ".js") =>
177
    eslint({ filePath: join(paths.source, "/temp." + extension), prettierOptions: prettierrc, text });
178
179
const reference = (path: string) => `/// <reference path="${path}" />\n`;
180
181
const replaceLast = (haystack: string, needle: string, replacement = "") => {
182
    // Adapted from https://stackoverflow.com/a/5497365
183
    const index = haystack.lastIndexOf(needle);
184
185
    if (index === -1) return haystack;
186
187
    return haystack.substring(0, index) + replacement + haystack.substring(index + needle.length);
188
};
189
190
const parallel = (...tasks: Task[]) => Promise.all(tasks.map(fn => fn()));
191
192
const readString = async (path: string) => (await readFile(path)).toString();
193
194
const sourceMappingUrl = (url: string) => `//# sourceMappingURL=${url}.map`;
195
196
const task =
197
    <T>(name: string | Task<T>, task?: Task<T>) =>
198
    /* eslint-disable @typescript-eslint/indent */
199
    async () => {
200
        if (typeof name === "function") {
201
            task = name;
202
            name = "";
203
        }
204
        const time0 = hrtime.bigint();
205
        name.length && print("Task ", chalk.magenta(name), " started");
206
207
        const result = await task!();
208
209
        // Calculating time of task and parsing for better info
210
        let total = Number(process.hrtime.bigint() - time0) / 1e6;
211
        let unit = "ms";
212
        if (total >= 1000) (total /= 1e3), (unit = "s");
213
        if (total >= 1000) (total /= 60), (unit = "m");
214
215
        name.length && print("Task ", chalk.magenta(name), " ended (", chalk.grey(total.toFixed(2), unit), ")");
216
        return result;
217
    }; /* eslint-enable @typescript-eslint/indent */
218
219
const print = (...text: string[]) => stdout.write(text.join("") + "\n");
220
const printError = (...text: string[]) => stderr.write(text.join("") + "\n");
221
222
const sendError = (error: unknown) => {
223
    printError(chalk.bold("Build: "), chalk.red("Failed"), " because of ", error as string);
224
    process.exit(1);
225
};
226
227
const sendSuccess = () => {
228
    print(chalk.bold("Build: "), chalk.green("Successful!"));
229
    process.exit(0);
230
};
231
232
//#endregion
233
234
//#region Tasks- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
235
236
// 0: Cleanup
237
const clean = task("clean", async () => {
238
    await emptyDir(paths.dist);
239
});
240
241
// 1: Transpile the source into an EcmaScript module
242
const transpile = task("transpile", () => {
243
    const { errors, options } = convertCompilerOptionsFromJson(
244
        {
245
            ...tsconfig.compilerOptions,
246
            module: "esnext",
247
            noEmit: false,
248
            outDir: paths.local,
249
            rootDir: paths.source,
250
        },
251
        paths.source,
252
    );
253
254
    if (errors.length) {
255
        throw errors;
256
    }
257
258
    const program = createProgram({
259
        options,
260
        rootNames: ["./src/index.ts", "./src/extend.cjs.ts", "./src/extend.esm.ts"],
261
    });
262
    const { diagnostics, emitSkipped, emittedFiles } = program.emit();
263
264
    if (emitSkipped) {
265
        throw diagnostics;
266
    }
267
});
268
269
// 2: Bundle the transpiled code
270
const bundleScript = task("bundle:script", async () => {
271
    const bundle = await rollup({
272
        external: ["decimal.js"],
273
        input: join(paths.local, "index.js"),
274
        plugins: [rollupNode(), rollupBabel({ babelHelpers: "bundled" }), rollupSourcemaps()],
275
    });
276
277
    // ES5 (via babel) to be used in node modules and .js distributions
278
    const es5 = await bundle.generate({
279
        amd: { id: "decimal.js-i18n" },
280
        file: "decimal-i18n.js",
281
        exports: "named",
282
        format: "esm",
283
        globals: { "decimal.js": "Decimal" },
284
        name: "decimal-i18n.js",
285
        plugins: [
286
            getBabelOutputPlugin({
287
                presets: ["@babel/preset-env"],
288
                plugins: ["@babel/transform-modules-umd"],
289
            }),
290
        ],
291
        sourcemap: true,
292
    });
293
294
    // ES modules
295
    const esm = await bundle.generate({
296
        file: "decimal-i18n.mjs",
297
        format: "esm",
298
        name: "decimal-i18n.mjs",
299
        sourcemap: true,
300
    });
301
302
    await generateOutput(paths.bundle, formatEs5, ...es5.output);
303
    await generateOutput(paths.bundle, formatEsm, ...esm.output);
304
});
305
306
// 3. Bundle the type declarations
307
const bundleType = task("bundle:type", async () => {
308
    const bundle = await rollup({
309
        external: ["decimal.js"],
310
        input: join(paths.local, "index.d.ts"),
311
        plugins: [rollupDts()],
312
    });
313
314
    const { output } = await bundle.generate({ format: "es" });
315
    await generateOutput(paths.bundle, formatDts, ...output);
316
317
    // Rewrite and copy to fix type declarations
318
    const dts = await readString(join(paths.bundle, "index.d.ts"));
319
    const noGlobalDts = dts.replace(globalRegExp, "");
320
321
    await writeFile(join(paths.bundle, "index.d.ts"), dts);
322
    await writeFile(join(paths.bundle, "decimal-i18n.d.ts"), noGlobalDts);
323
});
324
325
// 4. Copy node base files
326
const makeNodeFiles = task("make:node:files", async () => {
327
    await ensureDir(paths.node);
328
    await move(join(paths.bundle, "index.d.ts"), join(paths.node, "index.d.ts"));
329
330
    // Reading extend versions and rewiring them accordingly
331
    await copyAndRename({
332
        from: paths.local,
333
        to: paths.node,
334
        oldName: "extend.cjs",
335
        newName: "extend",
336
        postProcessor: formatCjsExtend,
337
    });
338
    await copyAndRename({
339
        from: paths.local,
340
        to: paths.node,
341
        oldName: "extend.esm",
342
        newName: "extend",
343
        newExt: ".mjs",
344
        postProcessor: formatMjsExtend,
345
    });
346
347
    // Copy scripts
348
    await copyAndRename({ from: paths.bundle, to: paths.node });
349
    await copyAndRename({ from: paths.bundle, to: paths.node, oldExt: ".mjs", newExt: ".mjs" });
350
351
    // Copy other files
352
    await copyFile(join(paths.root, "LICENSE.md"), join(paths.node, "LICENSE.md"));
353
});
354
355
// 5. Create a production package.json
356
const makeNodePackage = task("make:node:package", async () => {
357
    const nodePackage = {
358
        ..._package,
359
        name: "decimal.js-i18n",
360
        engines: { node: ">=12" },
361
        main: "index",
362
        browser: "index.js",
363
        module: "index.mjs",
364
        types: "index.d.ts",
365
        files: [
366
            "extend.d.ts",
367
            "extend.js",
368
            "extend.js.map",
369
            "extend.mjs",
370
            "extend.mjs.map",
371
            "index.d.ts",
372
            "index.js",
373
            "index.js.map",
374
            "index.mjs",
375
            "index.mjs.map",
376
        ],
377
    } as any;
378
    delete nodePackage.devDependencies;
379
    delete nodePackage.private;
380
381
    await writeJson(join(paths.node, "package.json"), nodePackage);
382
});
383
384
// 6. Tweak README.md to better fit https://npmjs.org's style
385
const makeNodeReadme = task("make:node:readme", async () => {
386
    const readme = await readString(join(paths.root, "README.md"));
387
    await writeFile(join(paths.node, "README.md"), formatReadme(readme));
388
});
389
390
// 7. Create minified (production-ready) scripts
391
const makeMinified = task("make:minified", async () => {
392
    const basePath = join(paths.bundle, "decimal-i18n");
393
394
    const js = await readString(basePath + ".js");
395
    const mjs = await readString(basePath + ".mjs");
396
397
    const jsMap: RawSourceMap = await readJson(basePath + ".js.map");
398
    const mjsMap: RawSourceMap = await readJson(basePath + ".mjs.map");
399
400
    const minJs = minify(js, "decimal-i18n.min.js", jsMap);
401
    const minMjs = minify(mjs, "decimal-i18n.min.mjs", mjsMap);
402
403
    await writeFile(basePath + ".min.js", minJs.code);
404
    await writeFile(basePath + ".min.js.map", minJs.map);
405
    await writeFile(basePath + ".min.mjs", minMjs.code);
406
    await writeFile(basePath + ".min.mjs.map", minMjs.map);
407
});
408
409
// 8. Create distributable compacted files
410
const makeDistributable = task("make:distributable", async () => {
411
    const baseName = "decimal-i18n";
412
    const basePath = join(paths.bundle, baseName);
413
    const dts = await readString(basePath + ".d.ts");
414
415
    const createZip = async (suffix: string) => {
416
        const zip = new JsZip();
417
        zip.file(baseName + ".d.ts", dts);
418
        zip.file(baseName + suffix, await readString(basePath + suffix));
419
        zip.file(baseName + suffix + ".map", await readString(basePath + suffix + ".map"));
420
421
        return new Promise(resolve =>
422
            zip
423
                .generateNodeStream({ streamFiles: true })
424
                .pipe(createWriteStream(basePath + suffix + "-v" + version + ".zip"))
425
                .on("finish", resolve),
426
        );
427
    };
428
429
    await createZip(".js");
430
    await createZip(".min.js");
431
    await createZip(".mjs");
432
    await createZip(".min.mjs");
433
});
434
435
//#endregion
436
437
//#region Program- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
438
439
const build = task("build", async () => {
440
    await clean();
441
    await transpile();
442
    await parallel(bundleScript, bundleType);
443
    await parallel(makeNodeFiles, makeNodePackage, makeNodeReadme, makeMinified);
444
    await makeDistributable();
445
});
446
447
build().then(sendSuccess).catch(sendError);
448
449
//#endregion
450