@serwist/cli 8.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/bin.js +539 -0
  4. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Google LLC, 2019 ShadowWalker w@weiw.io https://weiw.io, 2023 Serwist
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ This module's documentation can be found at https://developers.google.com/web/tools/workbox/modules/workbox-cli
package/dist/bin.js ADDED
@@ -0,0 +1,539 @@
1
+ import meow from 'meow';
2
+ import updateNotifier from 'update-notifier';
3
+ import { injectManifest } from '@serwist/build';
4
+ import assert from 'assert';
5
+ import chokidar from 'chokidar';
6
+ import prettyBytes from 'pretty-bytes';
7
+ import upath from 'upath';
8
+ import { oneLine } from 'common-tags';
9
+ import chalk from 'chalk';
10
+ import { createRequire } from 'node:module';
11
+ import fse from 'fs-extra';
12
+ import stringifyObject from 'stringify-object';
13
+ import inquirer from 'inquirer';
14
+ import { glob } from 'glob';
15
+ import ora from 'ora';
16
+
17
+ /*
18
+ Copyright 2018 Google LLC
19
+
20
+ Use of this source code is governed by an MIT-style
21
+ license that can be found in the LICENSE file or at
22
+ https://opensource.org/licenses/MIT.
23
+ */ const constants = {
24
+ defaultConfigFile: "serwist.config.js",
25
+ ignoredDirectories: [
26
+ "node_modules"
27
+ ],
28
+ ignoredFileExtensions: [
29
+ "map"
30
+ ],
31
+ ignoreURLParametersMatching: [
32
+ /^utm_/,
33
+ /^fbclid$/
34
+ ]
35
+ };
36
+
37
+ const errors = {
38
+ "missing-input": `params.input value was not set properly.`,
39
+ "missing-dest-dir-param": oneLine`Please provide the path to a directory in which
40
+ the libraries will be copied.`,
41
+ "invalid-common-js-module": oneLine`Please pass in a valid CommonJS module that
42
+ exports your configuration.`,
43
+ "config-validation-failed": `Your configuration is invalid:`,
44
+ "serwist-build-runtime-error": `Service worker generation failed:`,
45
+ "unknown-command": `Unknown command:`,
46
+ "no-file-extensions-found": oneLine`No files could be found that are suitable for
47
+ caching.`,
48
+ "no-file-extensions-selected": `Please select at least one file extension.`,
49
+ "invalid-sw-dest": oneLine`Please enter a valid path to use for the service worker
50
+ file that's created.`,
51
+ "glob-directory-invalid": oneLine`The path you entered isn't a valid directory.`,
52
+ "invalid-config-location": oneLine`Please enter a valid path to use for the saved
53
+ configuration file.`,
54
+ "sw-src-missing-injection-point": oneLine`That is not a valid source service worker
55
+ file. Please try again with a file containing
56
+ 'self.__SW_MANIFEST'.`,
57
+ "no-search-parameters-supplied": oneLine`Please provide the url search param(s)
58
+ you would like to ignore.`,
59
+ "invalid-search-parameters-supplied": oneLine`Please provide the valid URL search parameter(s)
60
+ without the leading '/' or '?' (i.e. source,version,language).`
61
+ };
62
+
63
+ const logger = {
64
+ debug: (...args)=>console.log(chalk.gray(...args)),
65
+ log: (...args)=>console.log(...args),
66
+ warn: (...args)=>console.warn(chalk.yellow(...args)),
67
+ error: (...args)=>console.error(chalk.red.bold(...args))
68
+ };
69
+
70
+ const require = createRequire(import.meta.url);
71
+ // A really light wrapper on top of Node's require() to make it easier to stub
72
+ // out reading the configuration during tests.
73
+ function readConfig(configFile) {
74
+ return require(configFile);
75
+ }
76
+
77
+ // The key used for the question/answer.
78
+ const name$3 = "configLocation";
79
+ const configLocationQuestion = {
80
+ name: name$3,
81
+ message: oneLine`Where would you like to save these configuration options?`,
82
+ type: "input",
83
+ default: constants.defaultConfigFile
84
+ };
85
+ /**
86
+ * @returns The answers from inquirer.
87
+ */ function askQuestion$5() {
88
+ return inquirer.prompt([
89
+ configLocationQuestion
90
+ ]);
91
+ }
92
+ async function askConfigLocation() {
93
+ const answers = await askQuestion$5();
94
+ // The value of the answer when the question type is 'input' is String
95
+ // and it has a default value, the casting is safe.
96
+ const configLocation = answers[name$3].trim();
97
+ assert(configLocation, errors["invalid-config-location"]);
98
+ return configLocation;
99
+ }
100
+
101
+ // The key used for the question/answer.
102
+ const name$2 = "globPatterns";
103
+ /**
104
+ * @param globDirectory The directory used for the root of globbing.
105
+ * @returns The unique file extensions corresponding
106
+ * to all of the files under globDirectory.
107
+ */ async function getAllFileExtensions(globDirectory) {
108
+ // Use a pattern to match any file that contains a '.', since that signifies
109
+ // the presence of a file extension.
110
+ const files = await glob("**/*.*", {
111
+ cwd: globDirectory,
112
+ nodir: true,
113
+ ignore: [
114
+ ...constants.ignoredDirectories.map((directory)=>`**/${directory}/**`),
115
+ ...constants.ignoredFileExtensions.map((extension)=>`**/*.${extension}`)
116
+ ]
117
+ });
118
+ const extensions = new Set();
119
+ for (const file of files){
120
+ const extension = upath.extname(file);
121
+ if (extension) {
122
+ // Get rid of the leading . character.
123
+ extensions.add(extension.replace(/^\./, ""));
124
+ }
125
+ }
126
+ return [
127
+ ...extensions
128
+ ];
129
+ }
130
+ /**
131
+ * @param globDirectory The directory used for the root of globbing.
132
+ * @returns The answers from inquirer.
133
+ */ async function askQuestion$4(globDirectory) {
134
+ // We need to get a list of extensions corresponding to files in the directory
135
+ // to use when asking the next question. That could potentially take some
136
+ // time, so we show a spinner and explanatory text.
137
+ const spinner = ora({
138
+ text: `Examining files in ${globDirectory}...`,
139
+ stream: process.stdout
140
+ }).start();
141
+ const fileExtensions = await getAllFileExtensions(globDirectory);
142
+ spinner.stop();
143
+ assert(fileExtensions.length > 0, errors["no-file-extensions-found"]);
144
+ return inquirer.prompt([
145
+ {
146
+ name: name$2,
147
+ message: "Which file types would you like to precache?",
148
+ type: "checkbox",
149
+ choices: fileExtensions,
150
+ default: fileExtensions
151
+ }
152
+ ]);
153
+ }
154
+ async function askExtensionsToCache(globDirectory) {
155
+ const answers = await askQuestion$4(globDirectory);
156
+ // The return value is an array of strings with the selected values
157
+ // and there is a default, the casting is safe.
158
+ const extensions = answers[name$2];
159
+ assert(extensions.length > 0, errors["no-file-extensions-selected"]);
160
+ // glob isn't happy with a single option inside of a {} group, so use a
161
+ // pattern without a {} group when there's only one extension.
162
+ const extensionsPattern = extensions.length === 1 ? extensions[0] : `{${extensions.join(",")}}`;
163
+ return [
164
+ `**/*.${extensionsPattern}`
165
+ ];
166
+ }
167
+
168
+ const ROOT_PROMPT = "Please enter the path to the root of your web app:";
169
+ // The keys used for the questions/answers.
170
+ const questionRootDirectory = "globDirectory";
171
+ const questionManualInput = "manualDirectoryInput";
172
+ /**
173
+ * @returns The subdirectories of the current
174
+ * working directory, with hidden and ignored ones filtered out.
175
+ */ async function getSubdirectories() {
176
+ return await glob("*/", {
177
+ ignore: constants.ignoredDirectories.map((directory)=>`${directory}/`)
178
+ });
179
+ }
180
+ /**
181
+ * @returns The answers from inquirer.
182
+ */ async function askQuestion$3() {
183
+ const subdirectories = await getSubdirectories();
184
+ if (subdirectories.length > 0) {
185
+ const manualEntryChoice = "Manually enter path";
186
+ return inquirer.prompt([
187
+ {
188
+ name: questionRootDirectory,
189
+ type: "list",
190
+ message: oneLine`What is the root of your web app (i.e. which directory do
191
+ you deploy)?`,
192
+ choices: subdirectories.concat([
193
+ new inquirer.Separator(),
194
+ manualEntryChoice
195
+ ])
196
+ },
197
+ {
198
+ name: questionManualInput,
199
+ when: (answers)=>answers.globDirectory === manualEntryChoice,
200
+ message: ROOT_PROMPT
201
+ }
202
+ ]);
203
+ }
204
+ return inquirer.prompt([
205
+ {
206
+ name: questionRootDirectory,
207
+ message: ROOT_PROMPT,
208
+ default: "."
209
+ }
210
+ ]);
211
+ }
212
+ async function askRootOfWebApp() {
213
+ const { manualDirectoryInput, globDirectory } = await askQuestion$3();
214
+ try {
215
+ const stat = await fse.stat(manualDirectoryInput || globDirectory);
216
+ assert(stat.isDirectory());
217
+ } catch (error) {
218
+ throw new Error(errors["glob-directory-invalid"]);
219
+ }
220
+ return manualDirectoryInput || globDirectory;
221
+ }
222
+
223
+ const START_URL_QUERY_PARAMS_PROMPT = "Please enter the search parameter(s) that you would like to ignore (separated by comma):";
224
+ // The keys used for the questions/answers.
225
+ const question_ignoreURLParametersMatching = "ignoreURLParametersMatching";
226
+ const question_shouldAskForIgnoreURLParametersMatching = "shouldAskForIgnoreURLParametersMatching";
227
+ /**
228
+ * @returns The answers from inquirer.
229
+ */ async function askQuestion$2() {
230
+ return inquirer.prompt([
231
+ {
232
+ name: question_shouldAskForIgnoreURLParametersMatching,
233
+ message: oneLine`Does your web app manifest include search parameter(s)
234
+ in the 'start_url', other than 'utm_' or 'fbclid'
235
+ (like '?source=pwa')?`,
236
+ type: "confirm",
237
+ default: false
238
+ },
239
+ {
240
+ name: question_ignoreURLParametersMatching,
241
+ when: (answer)=>answer.shouldAskForIgnoreURLParametersMatching,
242
+ message: START_URL_QUERY_PARAMS_PROMPT,
243
+ type: "input"
244
+ }
245
+ ]);
246
+ }
247
+ async function askQueryParametersInStartUrl(defaultIgnoredSearchParameters = constants.ignoreURLParametersMatching) {
248
+ const { shouldAskForIgnoreURLParametersMatching, ignoreURLParametersMatching = "" } = await askQuestion$2();
249
+ if (!shouldAskForIgnoreURLParametersMatching) {
250
+ return defaultIgnoredSearchParameters;
251
+ }
252
+ assert(ignoreURLParametersMatching.length > 0, errors["no-search-parameters-supplied"]);
253
+ const ignoreSearchParameters = ignoreURLParametersMatching.trim().split(",").filter(Boolean);
254
+ assert(ignoreSearchParameters.length > 0, errors["no-search-parameters-supplied"]);
255
+ assert(ignoreSearchParameters.every((param)=>!param.match(/^[^\w|-]/g)), errors["invalid-search-parameters-supplied"]);
256
+ return defaultIgnoredSearchParameters.concat(ignoreSearchParameters.map((searchParam)=>new RegExp(`^${searchParam}`)));
257
+ }
258
+
259
+ // The key used for the question/answer.
260
+ const name$1 = "swDest";
261
+ /**
262
+ * @param defaultDir
263
+ * @returns The answers from inquirer.
264
+ */ function askQuestion$1(defaultDir) {
265
+ return inquirer.prompt([
266
+ {
267
+ name: name$1,
268
+ message: `Where would you like your service worker file to be saved?`,
269
+ type: "input",
270
+ default: upath.join(defaultDir, "sw.js")
271
+ }
272
+ ]);
273
+ }
274
+ async function askSWDest(defaultDir = ".") {
275
+ const answers = await askQuestion$1(defaultDir);
276
+ // When prompt type is input the return type is string
277
+ // casting is safe
278
+ const swDest = answers[name$1].trim();
279
+ assert(swDest, errors["invalid-sw-dest"]);
280
+ return swDest;
281
+ }
282
+
283
+ // The key used for the question/answer.
284
+ const name = "swSrc";
285
+ /**
286
+ * @returns The answers from inquirer.
287
+ */ function askQuestion() {
288
+ return inquirer.prompt([
289
+ {
290
+ name,
291
+ message: oneLine`Where's your existing service worker file? To be used with
292
+ injectManifest, it should include a call to
293
+ 'self.__SW_MANIFEST'`,
294
+ type: "input"
295
+ }
296
+ ]);
297
+ }
298
+ async function askSWSrc() {
299
+ const answers = await askQuestion();
300
+ // When prompt type is input the return is string or null
301
+ return answers[name] ? answers[name].trim() : null;
302
+ }
303
+
304
+ async function askQuestions(options = {}) {
305
+ const isInjectManifest = "injectManifest" in options;
306
+ const globDirectory = await askRootOfWebApp();
307
+ const globPatterns = await askExtensionsToCache(globDirectory);
308
+ const swSrc = isInjectManifest ? await askSWSrc() : undefined;
309
+ const swDest = await askSWDest(globDirectory);
310
+ const configLocation = await askConfigLocation();
311
+ // See https://github.com/GoogleChrome/workbox/issues/2985
312
+ const ignoreURLParametersMatching = isInjectManifest ? undefined : await askQueryParametersInStartUrl();
313
+ const config = {
314
+ globDirectory,
315
+ globPatterns,
316
+ swDest
317
+ };
318
+ if (swSrc) {
319
+ config.swSrc = swSrc;
320
+ }
321
+ if (ignoreURLParametersMatching) {
322
+ config.ignoreURLParametersMatching = ignoreURLParametersMatching;
323
+ }
324
+ return {
325
+ config,
326
+ configLocation
327
+ };
328
+ }
329
+
330
+ async function runWizard(options = {}) {
331
+ const { configLocation, config } = await askQuestions(options);
332
+ // See https://github.com/GoogleChrome/workbox/issues/2796
333
+ const contents = `module.exports = ${stringifyObject(config)};`;
334
+ await fse.writeFile(configLocation, contents);
335
+ logger.log(`To build your service worker, run
336
+
337
+ serwist injectManifest ${configLocation}
338
+
339
+ as part of a build process. See https://goo.gl/fdTQBf for details.`);
340
+ const configDocsURL = "injectManifest" in options ? "https://goo.gl/8bs14N" : "https://goo.gl/gVo87N";
341
+ logger.log(oneLine`You can further customize your service worker by making changes
342
+ to ${configLocation}. See ${configDocsURL} for details.`);
343
+ }
344
+
345
+ /**
346
+ * Runs the specified build command with the provided configuration.
347
+ *
348
+ * @param options
349
+ */ async function runBuildCommand({ config, watch }) {
350
+ const { count, filePaths, size, warnings } = await injectManifest(config);
351
+ for (const warning of warnings){
352
+ logger.warn(warning);
353
+ }
354
+ if (filePaths.length === 1) {
355
+ logger.log(`The service worker file was written to ${config.swDest}`);
356
+ } else {
357
+ const message = filePaths.sort().map((filePath)=>` • ${filePath}`).join(`\n`);
358
+ logger.log(`The service worker files were written to:\n${message}`);
359
+ }
360
+ logger.log(`The service worker will precache ${count} URLs, ` + `totaling ${prettyBytes(size)}.`);
361
+ if (watch) {
362
+ logger.log(`\nWatching for changes...`);
363
+ }
364
+ }
365
+ const app = async (params)=>{
366
+ // This should not be a user-visible error, unless meow() messes something up.
367
+ assert(params && Array.isArray(params.input), errors["missing-input"]);
368
+ // Default to showing the help message if there's no command provided.
369
+ const [command = "help", option] = params.input;
370
+ switch(command){
371
+ case "wizard":
372
+ {
373
+ await runWizard(params.flags);
374
+ break;
375
+ }
376
+ case "copyLibraries":
377
+ {
378
+ logger.log("This feature is under maintenance. We are really sorry for the inconvenience.");
379
+ break;
380
+ }
381
+ case "injectManifest":
382
+ {
383
+ const configPath = upath.resolve(process.cwd(), option || constants.defaultConfigFile);
384
+ let config;
385
+ try {
386
+ config = readConfig(configPath);
387
+ } catch (error) {
388
+ config = null;
389
+ if (error instanceof Error) {
390
+ logger.error(errors["invalid-common-js-module"]);
391
+ throw error;
392
+ }
393
+ }
394
+ if (config === null) {
395
+ throw logger.error(errors["invalid-config-location"]);
396
+ }
397
+ logger.log(`Using configuration from ${configPath}.`);
398
+ // Determine whether we're in --watch mode, or one-off mode.
399
+ if (params?.flags?.watch) {
400
+ const options = {
401
+ ignoreInitial: true
402
+ };
403
+ if (config.globIgnores) {
404
+ options.ignored = config.globIgnores;
405
+ }
406
+ if (config.globDirectory) {
407
+ options.cwd = config.globDirectory;
408
+ }
409
+ if (config.globPatterns) {
410
+ chokidar.watch(config.globPatterns, options).on("all", async ()=>{
411
+ if (config === null) return;
412
+ await runBuildCommand({
413
+ config,
414
+ watch: true
415
+ });
416
+ }).on("ready", async ()=>{
417
+ if (config === null) return;
418
+ await runBuildCommand({
419
+ config,
420
+ watch: true
421
+ });
422
+ }).on("error", (err)=>{
423
+ logger.error(err.toString());
424
+ });
425
+ }
426
+ } else {
427
+ await runBuildCommand({
428
+ config,
429
+ watch: false
430
+ });
431
+ }
432
+ break;
433
+ }
434
+ case "help":
435
+ {
436
+ params.showHelp();
437
+ }
438
+ default:
439
+ {
440
+ throw new Error(errors["unknown-command"] + ` ` + command);
441
+ }
442
+ }
443
+ };
444
+
445
+ /*
446
+ Copyright 2018 Google LLC
447
+
448
+ Use of this source code is governed by an MIT-style
449
+ license that can be found in the LICENSE file or at
450
+ https://opensource.org/licenses/MIT.
451
+ */ // Helper to parse out less relevant info from an Error's stack trace.
452
+ // Removes the initial portion, since that's obtained from error.message.
453
+ // Removes every stack frame earlier than the last instance of moduleName,
454
+ // since that's just frames related to the Node runtime/loader.
455
+ function cleanupStackTrace(error, moduleName) {
456
+ if (!error.stack) {
457
+ return "";
458
+ }
459
+ const frames = error.stack.split(`\n`);
460
+ let startFrame;
461
+ let lastFrame = 0;
462
+ frames.forEach((frame, index)=>{
463
+ if (startFrame === undefined && frame.includes(` at `)) {
464
+ startFrame = index;
465
+ }
466
+ if (frame.includes(`${moduleName}:`)) {
467
+ lastFrame = index;
468
+ }
469
+ });
470
+ return frames.slice(startFrame, lastFrame + 1).join(`\n`);
471
+ }
472
+
473
+ /*
474
+ Copyright 2018 Google LLC
475
+
476
+ Use of this source code is governed by an MIT-style
477
+ license that can be found in the LICENSE file or at
478
+ https://opensource.org/licenses/MIT.
479
+ */ const helpText = `Usage:
480
+ $ serwist <command> [options]
481
+
482
+ Commands:
483
+ wizard [--injectManifest]
484
+ Runs the configuration wizard, which will generate a
485
+ config file based on answers to questions.
486
+
487
+ injectManifest [<path/to/config.js>] [--watch]
488
+ Takes an existing service worker file and creates a
489
+ copy of it with a precache manifest "injected" into
490
+ it. The precache manifest is generated based on the
491
+ options in the config file (defaults to serwist.config.js).
492
+ If --watch is provided, the CLI will stay running, and will
493
+ rebuild the service worker each time a file in the precache
494
+ manifest changes.
495
+ See https://bit.ly/wb-injectManifest
496
+
497
+ copyLibraries <path/to/parent/dir>
498
+ Makes a local copy of all of the Workbox libraries inside
499
+ a version directory at the location specified. This is intended
500
+ for developers using injectManifest who prefer using local,
501
+ rather than CDN hosted, libraries.
502
+
503
+ Config file:
504
+ In 'injectManifest' mode, the config file should be a
505
+ JavaScript file, in CommonJS module format.
506
+ By default, a config file named serwist.config.js in the current
507
+ directory is assumed, but this can be overridden.
508
+
509
+ Examples:
510
+ $ serwist wizard
511
+ $ serwist wizard --injectManifest
512
+ $ serwist injectManifest configs/serwist-dev-config.js
513
+ $ serwist copyLibraries build/
514
+ `;
515
+
516
+ void (async ()=>{
517
+ const params = meow(helpText, {
518
+ importMeta: import.meta
519
+ });
520
+ updateNotifier({
521
+ pkg: params.pkg
522
+ }).notify();
523
+ try {
524
+ await app(params);
525
+ } catch (error) {
526
+ if (error instanceof Error) {
527
+ // Show the full error and stack trace if we're run with --debug.
528
+ if (params.flags.debug) {
529
+ if (error.stack) {
530
+ logger.error(`\n${error.stack}`);
531
+ }
532
+ } else {
533
+ logger.error(`\n${error.message}`);
534
+ logger.debug(`${cleanupStackTrace(error, "app.js")}\n`);
535
+ }
536
+ }
537
+ process.exit(1);
538
+ }
539
+ })();
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@serwist/cli",
3
+ "version": "8.0.0",
4
+ "type": "module",
5
+ "description": "@serwist/cli is the command line interface for Serwist.",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "keywords": [
10
+ "serwist",
11
+ "serwistjs",
12
+ "service worker",
13
+ "caching",
14
+ "fetch requests",
15
+ "offline",
16
+ "cli"
17
+ ],
18
+ "engines": {
19
+ "node": ">=16.0.0"
20
+ },
21
+ "author": "Google's Web DevRel Team, Serwist's Team",
22
+ "license": "MIT",
23
+ "repository": "serwist/serwist",
24
+ "bugs": "https://github.com/serwist/serwist/issues",
25
+ "homepage": "https://github.com/serwist/serwist",
26
+ "bin": {
27
+ "serwist": "dist/bin.js"
28
+ },
29
+ "dependencies": {
30
+ "chalk": "5.3.0",
31
+ "chokidar": "3.5.3",
32
+ "common-tags": "1.8.2",
33
+ "fs-extra": "11.1.1",
34
+ "glob": "10.3.10",
35
+ "inquirer": "9.2.11",
36
+ "meow": "12.1.1",
37
+ "ora": "7.0.1",
38
+ "pretty-bytes": "6.1.1",
39
+ "stringify-object": "5.0.0",
40
+ "upath": "2.0.1",
41
+ "update-notifier": "6.0.2",
42
+ "@serwist/build": "8.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/common-tags": "1.8.3",
46
+ "@types/fs-extra": "11.0.2",
47
+ "@types/inquirer": "9.0.5",
48
+ "@types/stringify-object": "4.0.3",
49
+ "@types/update-notifier": "6.0.5",
50
+ "rollup": "3.28.1",
51
+ "@serwist/constants": "8.0.0"
52
+ },
53
+ "scripts": {
54
+ "build": "rimraf dist && cross-env NODE_ENV=production rollup --config rollup.config.js",
55
+ "lint": "eslint src --ext ts,tsx,js,jsx,cjs,mjs",
56
+ "typecheck": "tsc"
57
+ }
58
+ }