@tramvai/cli 2.98.2 → 2.101.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 (74) hide show
  1. package/lib/api/start-prod/providers/application.js +2 -3
  2. package/lib/api/start-prod/providers/application.js.map +1 -1
  3. package/lib/builder/webpack/tokens.d.ts +3 -0
  4. package/lib/commands/analyze/command.js +7 -1
  5. package/lib/commands/analyze/command.js.map +1 -1
  6. package/lib/commands/build/command.js +2 -0
  7. package/lib/commands/build/command.js.map +1 -1
  8. package/lib/commands/start/command.js +2 -0
  9. package/lib/commands/start/command.js.map +1 -1
  10. package/lib/commands/start-prod/command.js +7 -1
  11. package/lib/commands/start-prod/command.js.map +1 -1
  12. package/lib/commands/static/application.js +1 -1
  13. package/lib/commands/static/application.js.map +1 -1
  14. package/lib/config/configManager.d.ts +1 -0
  15. package/lib/config/configManager.js +23 -1
  16. package/lib/config/configManager.js.map +1 -1
  17. package/lib/di/tokens/config.d.ts +1 -0
  18. package/lib/library/webpack/application/client/common.js +2 -0
  19. package/lib/library/webpack/application/client/common.js.map +1 -1
  20. package/lib/library/webpack/application/server/common.js +2 -0
  21. package/lib/library/webpack/application/server/common.js.map +1 -1
  22. package/lib/library/webpack/blocks/pwa/client.d.ts +4 -0
  23. package/lib/library/webpack/blocks/pwa/client.js +62 -0
  24. package/lib/library/webpack/blocks/pwa/client.js.map +1 -0
  25. package/lib/library/webpack/blocks/pwa/server.d.ts +4 -0
  26. package/lib/library/webpack/blocks/pwa/server.js +9 -0
  27. package/lib/library/webpack/blocks/pwa/server.js.map +1 -0
  28. package/lib/library/webpack/blocks/pwa/shared.d.ts +4 -0
  29. package/lib/library/webpack/blocks/pwa/shared.js +18 -0
  30. package/lib/library/webpack/blocks/pwa/shared.js.map +1 -0
  31. package/lib/library/webpack/plugins/PwaIconsPlugin.d.ts +9 -0
  32. package/lib/library/webpack/plugins/PwaIconsPlugin.js +78 -0
  33. package/lib/library/webpack/plugins/PwaIconsPlugin.js.map +1 -0
  34. package/lib/library/webpack/plugins/WebManifestPlugin.d.ts +8 -0
  35. package/lib/library/webpack/plugins/WebManifestPlugin.js +28 -0
  36. package/lib/library/webpack/plugins/WebManifestPlugin.js.map +1 -0
  37. package/lib/schema/autogeneratedSchema.json +183 -0
  38. package/lib/typings/configEntry/application.d.ts +54 -0
  39. package/lib/typings/public.d.ts +1 -0
  40. package/lib/typings/public.js +2 -0
  41. package/lib/typings/public.js.map +1 -1
  42. package/lib/typings/pwa/index.d.ts +59 -0
  43. package/lib/typings/pwa/index.js +3 -0
  44. package/lib/typings/pwa/index.js.map +1 -0
  45. package/lib/validators/commands/checkPwaDependencies.d.ts +2 -0
  46. package/lib/validators/commands/checkPwaDependencies.js +18 -0
  47. package/lib/validators/commands/checkPwaDependencies.js.map +1 -0
  48. package/package.json +15 -11
  49. package/schema.json +183 -0
  50. package/src/api/start-prod/providers/application.ts +3 -4
  51. package/src/commands/analyze/command.ts +7 -1
  52. package/src/commands/build/command.ts +2 -0
  53. package/src/commands/start/command.ts +2 -0
  54. package/src/commands/start-prod/command.ts +7 -1
  55. package/src/commands/static/application.ts +1 -4
  56. package/src/config/configManager.ts +37 -0
  57. package/src/library/webpack/application/client/common.ts +2 -0
  58. package/src/library/webpack/application/server/common.ts +2 -0
  59. package/src/library/webpack/blocks/pwa/client.ts +83 -0
  60. package/src/library/webpack/blocks/pwa/server.ts +9 -0
  61. package/src/library/webpack/blocks/pwa/shared.ts +26 -0
  62. package/src/library/webpack/plugins/PwaIconsPlugin.ts +87 -0
  63. package/src/library/webpack/plugins/WebManifestPlugin.ts +32 -0
  64. package/src/models/config.spec.ts +54 -0
  65. package/src/schema/autogeneratedSchema.json +183 -0
  66. package/src/schema/tramvai.spec.ts +27 -0
  67. package/src/typings/configEntry/application.ts +55 -0
  68. package/src/typings/public.ts +2 -0
  69. package/src/typings/pwa/index.ts +64 -0
  70. package/src/validators/commands/checkPwaDependencies.ts +17 -0
  71. package/lib/utils/webpackBuild.d.ts +0 -5
  72. package/lib/utils/webpackBuild.js +0 -32
  73. package/lib/utils/webpackBuild.js.map +0 -1
  74. package/src/utils/webpackBuild.ts +0 -38
package/schema.json CHANGED
@@ -546,6 +546,189 @@
546
546
  ],
547
547
  "type": "string"
548
548
  },
549
+ "pwa": {
550
+ "title": "PWA configuration (works with `TramvaiPwaModule` from `@tramvai/module-progressive-web-app` library)",
551
+ "default": {},
552
+ "type": "object",
553
+ "properties": {
554
+ "sw": {
555
+ "title": "Service-Worker configuration",
556
+ "default": {},
557
+ "type": "object",
558
+ "properties": {
559
+ "src": {
560
+ "title": "Path to sw.ts file (relative to \"root\" directory)",
561
+ "default": "sw.ts",
562
+ "type": "string"
563
+ },
564
+ "dest": {
565
+ "title": "Name of generated SW file (will be placed in \"output.client\" directory)",
566
+ "default": "sw.js",
567
+ "type": "string"
568
+ },
569
+ "scope": {
570
+ "title": "Scope of SW (see https://developers.google.com/web/ilt/pwa/introduction-to-service-worker#registration_and_scope)",
571
+ "default": "/",
572
+ "type": "string"
573
+ }
574
+ },
575
+ "additionalProperties": false
576
+ },
577
+ "workbox": {
578
+ "title": "Workbox configuration",
579
+ "default": {},
580
+ "type": "object",
581
+ "properties": {
582
+ "enabled": {
583
+ "cli_overridable": "",
584
+ "title": "Connect `InjectManifest` from `workbox-webpack-plugin` library",
585
+ "default": false,
586
+ "anyOf": [
587
+ {
588
+ "type": "object",
589
+ "properties": {
590
+ "development": {
591
+ "type": "boolean"
592
+ },
593
+ "production": {
594
+ "type": "boolean"
595
+ }
596
+ },
597
+ "additionalProperties": false
598
+ },
599
+ {
600
+ "type": "boolean"
601
+ }
602
+ ]
603
+ }
604
+ },
605
+ "additionalProperties": false
606
+ },
607
+ "webmanifest": {
608
+ "title": "WebManifest content (manifest.json or webmanifest will be generated based on this options)",
609
+ "default": {},
610
+ "type": "object",
611
+ "properties": {
612
+ "enabled": {
613
+ "title": "Create webmanifest file",
614
+ "default": false,
615
+ "type": "boolean"
616
+ },
617
+ "dest": {
618
+ "title": "Name of generated manifest file (will be placed in \"output.client\" directory). You can use `[hash]` placeholder for manifest cache busting",
619
+ "default": "/manifest.[hash].json",
620
+ "type": "string"
621
+ },
622
+ "scope": {
623
+ "title": "prefer to use \"pwa.sw.scope\" instead, this field will be generated automatically",
624
+ "type": "string"
625
+ },
626
+ "name": {
627
+ "type": "string"
628
+ },
629
+ "short_name": {
630
+ "type": "string"
631
+ },
632
+ "description": {
633
+ "type": "string"
634
+ },
635
+ "start_url": {
636
+ "type": "string"
637
+ },
638
+ "display": {
639
+ "type": "string"
640
+ },
641
+ "theme_color": {
642
+ "title": "prefer to use \"pwa.meta.themeColor\" instead, this field will be generated automatically",
643
+ "type": "string"
644
+ },
645
+ "background_color": {
646
+ "type": "string"
647
+ },
648
+ "icons": {
649
+ "title": "prefer to use \"pwa.icon\" instead, this field will be generated automatically",
650
+ "type": "array",
651
+ "items": {
652
+ "type": "object",
653
+ "properties": {
654
+ "src": {
655
+ "type": "string"
656
+ },
657
+ "sizes": {
658
+ "type": "string"
659
+ },
660
+ "type": {
661
+ "type": "string"
662
+ }
663
+ },
664
+ "additionalProperties": false
665
+ }
666
+ }
667
+ },
668
+ "additionalProperties": false
669
+ },
670
+ "icon": {
671
+ "title": "PWA icons options",
672
+ "default": {},
673
+ "type": "object",
674
+ "properties": {
675
+ "src": {
676
+ "title": "Path to icon file (relative to \"root\" directory)",
677
+ "type": "string"
678
+ },
679
+ "dest": {
680
+ "title": "Folder for generated icons (will be placed in \"output.client\" directory)",
681
+ "default": "pwa-icons",
682
+ "type": "string"
683
+ },
684
+ "sizes": {
685
+ "title": "Icon sizes",
686
+ "default": [
687
+ 36,
688
+ 48,
689
+ 72,
690
+ 96,
691
+ 144,
692
+ 192,
693
+ 512
694
+ ],
695
+ "type": "array",
696
+ "items": {
697
+ "type": "number"
698
+ }
699
+ }
700
+ },
701
+ "additionalProperties": false
702
+ },
703
+ "meta": {
704
+ "title": "PWA meta options",
705
+ "default": {},
706
+ "type": "object",
707
+ "properties": {
708
+ "viewport": {
709
+ "type": "string"
710
+ },
711
+ "themeColor": {
712
+ "type": "string"
713
+ },
714
+ "mobileApp": {
715
+ "type": "string"
716
+ },
717
+ "mobileAppIOS": {
718
+ "type": "string"
719
+ },
720
+ "appleTitle": {
721
+ "type": "string"
722
+ },
723
+ "appleStatusBarStyle": {
724
+ "type": "string"
725
+ }
726
+ },
727
+ "additionalProperties": false
728
+ }
729
+ },
730
+ "additionalProperties": false
731
+ },
549
732
  "webpack": {
550
733
  "title": "experiments configuration for [webpack](https://webpack.js.org/configuration/experiments/)",
551
734
  "default": {},
@@ -46,7 +46,7 @@ export const applicationsProviders: readonly Provider[] = [
46
46
  ).withSettings({
47
47
  buildType: 'server',
48
48
  });
49
- const { debug, port, staticPort, staticHost, output } = serverConfigManager;
49
+ const { debug, port, assetsPrefix } = serverConfigManager;
50
50
  const root = serverConfigManager.buildPath;
51
51
 
52
52
  return fork(path.resolve(root, 'server.js'), [], {
@@ -58,11 +58,10 @@ export const applicationsProviders: readonly Provider[] = [
58
58
  ...env,
59
59
  ...process.env,
60
60
  NODE_ENV: 'production',
61
+ TRAMVAI_CLI_COMMAND: 'start-prod',
61
62
  PORT: `${port}`,
62
63
  PORT_SERVER: `${port}`,
63
- ASSETS_PREFIX:
64
- process.env.ASSETS_PREFIX ??
65
- `http://${staticHost}:${staticPort}/${output.client.replace(/\/$/, '')}/`,
64
+ ASSETS_PREFIX: assetsPrefix,
66
65
  },
67
66
  });
68
67
  },
@@ -1,6 +1,7 @@
1
1
  import { CLICommand } from '../../models/command';
2
2
  import { checkApplication } from '../../validators/commands/checkBuild';
3
3
  import { checkConfigExists } from '../../validators/commands/checkConfigExists';
4
+ import { checkPwaDependencies } from '../../validators/commands/checkPwaDependencies';
4
5
  import { runMigrationsAndCheckVersions } from '../../validators/commands/runMigrationsAndCheckVersions';
5
6
 
6
7
  export type Params = {
@@ -38,7 +39,12 @@ class AnalyzeCommand extends CLICommand<Params> {
38
39
 
39
40
  alias = 'a';
40
41
 
41
- validators = [checkConfigExists, checkApplication, runMigrationsAndCheckVersions];
42
+ validators = [
43
+ checkConfigExists,
44
+ checkApplication,
45
+ runMigrationsAndCheckVersions,
46
+ checkPwaDependencies,
47
+ ];
42
48
 
43
49
  action(parameters: Params) {
44
50
  // used require for lazy code execution
@@ -4,6 +4,7 @@ import { checkConfigExists } from '../../validators/commands/checkConfigExists';
4
4
  import { checkDependencies } from '../../validators/commands/checkDependencies';
5
5
  import { runMigrationsAndCheckVersions } from '../../validators/commands/runMigrationsAndCheckVersions';
6
6
  import type { BuildCommand as BuildCommandType } from '../../api/build';
7
+ import { checkPwaDependencies } from '../../validators/commands/checkPwaDependencies';
7
8
 
8
9
  export type Params = Parameters<BuildCommandType>[0] & {
9
10
  target: string;
@@ -65,6 +66,7 @@ class BuildCommand extends CLICommand<Params> {
65
66
  checkApplication,
66
67
  runMigrationsAndCheckVersions,
67
68
  checkDependencies,
69
+ checkPwaDependencies,
68
70
  ];
69
71
 
70
72
  action(parameters: Params) {
@@ -4,6 +4,7 @@ import { checkConfigExists } from '../../validators/commands/checkConfigExists';
4
4
  import { checkDependencies } from '../../validators/commands/checkDependencies';
5
5
  import { runMigrationsAndCheckVersions } from '../../validators/commands/runMigrationsAndCheckVersions';
6
6
  import type { StartCommand as StartCommandType } from '../../api/start';
7
+ import { checkPwaDependencies } from '../../validators/commands/checkPwaDependencies';
7
8
 
8
9
  export type Params = Parameters<StartCommandType>[0] & {
9
10
  target: string;
@@ -113,6 +114,7 @@ export class StartCommand extends CLICommand<Params> {
113
114
  checkApplication,
114
115
  runMigrationsAndCheckVersions,
115
116
  checkDependencies,
117
+ checkPwaDependencies,
116
118
  ];
117
119
 
118
120
  action(parameters) {
@@ -2,6 +2,7 @@ import { CLICommand } from '../../models/command';
2
2
  import { checkConfigExists } from '../../validators/commands/checkConfigExists';
3
3
  import { checkApplication } from '../../validators/commands/checkBuild';
4
4
  import { runMigrationsAndCheckVersions } from '../../validators/commands/runMigrationsAndCheckVersions';
5
+ import { checkPwaDependencies } from '../../validators/commands/checkPwaDependencies';
5
6
 
6
7
  export interface Params {
7
8
  target: string;
@@ -82,7 +83,12 @@ export class StartProdCommand extends CLICommand<Params> {
82
83
 
83
84
  alias = 'sp';
84
85
 
85
- validators = [checkConfigExists, checkApplication, runMigrationsAndCheckVersions];
86
+ validators = [
87
+ checkConfigExists,
88
+ checkApplication,
89
+ runMigrationsAndCheckVersions,
90
+ checkPwaDependencies,
91
+ ];
86
92
 
87
93
  action(parameters: Params) {
88
94
  // used require for lazy code execution
@@ -72,10 +72,7 @@ export const staticApp = async (
72
72
  });
73
73
 
74
74
  const staticServer = await startStaticServer(clientConfigManager);
75
- const staticAssetsPrefix = `http://${staticHost}:${staticPort}/${output.client.replace(
76
- /\/$/,
77
- ''
78
- )}/`;
75
+ const staticAssetsPrefix = serverConfigManager.assetsPrefix;
79
76
 
80
77
  const server = node(path.resolve(root, 'server.js'), [], {
81
78
  cwd: root,
@@ -86,12 +86,14 @@ export type ConfigManager<
86
86
  buildPath: string;
87
87
  withSettings(settings: Settings<E>): ConfigManager<C, E>;
88
88
  dehydrate(): [C, Settings<E>];
89
+ assetsPrefix?: string;
89
90
  };
90
91
 
91
92
  export const DEFAULT_PORT = 3000;
92
93
  export const DEFAULT_STATIC_PORT = 4000;
93
94
  export const DEFAULT_STATIC_MODULE_PORT = 4040;
94
95
 
96
+ // eslint-disable-next-line max-statements, complexity
95
97
  export const createConfigManager = <C extends ConfigEntry = ConfigEntry, E extends Env = Env>(
96
98
  configEntry: C,
97
99
  settings: Settings<E>
@@ -165,10 +167,45 @@ export const createConfigManager = <C extends ConfigEntry = ConfigEntry, E exten
165
167
  };
166
168
 
167
169
  if (isApplication(config)) {
170
+ config.assetsPrefix =
171
+ process.env.ASSETS_PREFIX && process.env.ASSETS_PREFIX !== 'static'
172
+ ? process.env.ASSETS_PREFIX
173
+ : `http://${config.staticHost}:${config.staticPort}/${config.output.client.replace(
174
+ /\/$/,
175
+ ''
176
+ )}/`;
168
177
  config.buildPath = resolve(
169
178
  rootDir,
170
179
  buildType === 'server' ? config.output.server : config.output.client
171
180
  );
181
+
182
+ const pwa = config.experiments?.pwa;
183
+
184
+ if (pwa.webmanifest?.enabled) {
185
+ pwa.webmanifest = {
186
+ icons: pwa.icon?.src
187
+ ? pwa.icon.sizes.map((size) => ({
188
+ src: `${config.assetsPrefix}${pwa.icon.dest}/${size}x${size}.png`,
189
+ sizes: `${size}x${size}`,
190
+ type: 'image/png',
191
+ }))
192
+ : [],
193
+ ...pwa.webmanifest,
194
+ scope: pwa.webmanifest.scope ?? pwa.sw?.scope,
195
+ name: pwa.webmanifest.name ?? config.name,
196
+ short_name: pwa.webmanifest.name ?? config.name,
197
+ theme_color: pwa.webmanifest.theme_color ?? pwa.meta.themeColor,
198
+ };
199
+
200
+ if (pwa.webmanifest.dest.includes('[hash]')) {
201
+ const crypto = require('crypto');
202
+ const hashSum = crypto.createHash('sha256');
203
+ hashSum.update(JSON.stringify(pwa.webmanifest));
204
+ const currentHash = hashSum.digest('hex');
205
+
206
+ pwa.webmanifest.dest = pwa.webmanifest.dest.replace('[hash]', currentHash.substr(0, 8));
207
+ }
208
+ }
172
209
  } else if (isChildApp(config)) {
173
210
  config.buildPath = resolve(rootDir, ...config.output.split('/'));
174
211
  } else if (isModule(config)) {
@@ -19,6 +19,7 @@ import nodeClient from '../../blocks/nodeClient';
19
19
  import { pagesResolve } from '../../blocks/pagesResolve';
20
20
  import { configToEnv } from '../../blocks/configToEnv';
21
21
  import { DEFAULT_STATS_OPTIONS, DEFAULT_STATS_FIELDS } from '../../constants/stats';
22
+ import { pwaBlock } from '../../blocks/pwa/client';
22
23
 
23
24
  export default (configManager: ConfigManager<ApplicationConfigEntry>) => (config: Config) => {
24
25
  const { polyfill, fileSystemPages } = configManager;
@@ -41,6 +42,7 @@ export default (configManager: ConfigManager<ApplicationConfigEntry>) => (config
41
42
  .batch(css(configManager))
42
43
  .batch(nodeClient(configManager))
43
44
  .batch(postcssAssets(configManager))
45
+ .batch(pwaBlock(configManager))
44
46
  .when(fileSystemPages.enabled, (cfg) => cfg.batch(pagesResolve(configManager)));
45
47
 
46
48
  config
@@ -17,6 +17,7 @@ import { browserslistConfigResolve } from '../../blocks/browserslistConfig';
17
17
  import { configToEnv } from '../../blocks/configToEnv';
18
18
  import { commonApplication } from '../common';
19
19
  import { extractCssPluginFactory } from '../../blocks/extractCssPlugin';
20
+ import { pwaBlock } from '../../blocks/pwa/server';
20
21
 
21
22
  // eslint-disable-next-line import/no-default-export
22
23
  export default (configManager: ConfigManager<ApplicationConfigEntry>) => (config: Config) => {
@@ -45,6 +46,7 @@ export default (configManager: ConfigManager<ApplicationConfigEntry>) => (config
45
46
  })
46
47
  )
47
48
  .batch(css(configManager))
49
+ .batch(pwaBlock(configManager))
48
50
  .when(fileSystemPages.enabled, (cfg) => cfg.batch(pagesResolve(configManager)));
49
51
 
50
52
  config.output
@@ -0,0 +1,83 @@
1
+ import path from 'path';
2
+ import type Config from 'webpack-chain';
3
+ import { InjectManifest } from 'workbox-webpack-plugin';
4
+ import type { ConfigManager } from '../../../../config/configManager';
5
+ import type { ApplicationConfigEntry } from '../../../../typings/configEntry/application';
6
+ import { PwaIconsPlugin } from '../../plugins/PwaIconsPlugin';
7
+ import { WebManifestPlugin } from '../../plugins/WebManifestPlugin';
8
+ import { pwaSharedBlock } from './shared';
9
+
10
+ export const pwaBlock =
11
+ (configManager: ConfigManager<ApplicationConfigEntry>) => (config: Config) => {
12
+ const {
13
+ experiments: { pwa },
14
+ rootDir,
15
+ root,
16
+ output,
17
+ env,
18
+ modern,
19
+ sourceMap,
20
+ assetsPrefix,
21
+ } = configManager;
22
+
23
+ config.batch(pwaSharedBlock(configManager));
24
+
25
+ // @todo check `@tramvai/module-progressive-web-app` is installed
26
+
27
+ if (pwa.workbox?.enabled) {
28
+ // @todo check `sw.ts` exists
29
+ // @todo: static HTML caching ??? full offline mode for tramvai static ???
30
+ const workboxOptions: InjectManifest['config'] = {
31
+ swSrc: path.join(rootDir, root, pwa.sw?.src),
32
+ swDest: path.join(rootDir, output.client, pwa.sw?.dest),
33
+ exclude: [/hmr\.js$/, /\.map$/, /\.hot-update\./],
34
+ // @todo maybe less for production?
35
+ maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
36
+ };
37
+
38
+ if (env === 'production') {
39
+ workboxOptions.modifyURLPrefix = {
40
+ '': assetsPrefix,
41
+ };
42
+ }
43
+
44
+ if (modern) {
45
+ workboxOptions.swDest = workboxOptions.swDest.replace(/\.js$/, '.modern.js');
46
+ }
47
+
48
+ // @todo: break hmr on client when sw.ts is changed - infinity loop !!!
49
+
50
+ const workboxPlugin = new InjectManifest(workboxOptions);
51
+
52
+ // https://github.com/GoogleChrome/workbox/issues/1790#issuecomment-1241356293
53
+ if (env === 'development') {
54
+ Object.defineProperty(workboxPlugin, 'alreadyCalled', {
55
+ get() {
56
+ return false;
57
+ },
58
+ set() {},
59
+ });
60
+ }
61
+
62
+ // Fix `ERROR in Invalid URL` problem
63
+ // https://github.com/webpack/webpack/issues/9570#issuecomment-520713006
64
+ if (sourceMap) {
65
+ config.output.set('devtoolNamespace', 'tramvai');
66
+ }
67
+
68
+ config.plugin('workbox').use(workboxPlugin);
69
+ }
70
+
71
+ if (pwa.webmanifest?.enabled) {
72
+ const webmanifestPlugin = new WebManifestPlugin(pwa.webmanifest);
73
+
74
+ config.plugin('webmanifest').use(webmanifestPlugin);
75
+ }
76
+
77
+ if (pwa.icon?.src) {
78
+ const iconSrc = path.join(rootDir, root, pwa.icon.src);
79
+ const pwaIconsPlugin = new PwaIconsPlugin({ ...pwa.icon, src: iconSrc });
80
+
81
+ config.plugin('pwa-icons').use(pwaIconsPlugin);
82
+ }
83
+ };
@@ -0,0 +1,9 @@
1
+ import type Config from 'webpack-chain';
2
+ import type { ConfigManager } from '../../../../config/configManager';
3
+ import type { ApplicationConfigEntry } from '../../../../typings/configEntry/application';
4
+ import { pwaSharedBlock } from './shared';
5
+
6
+ export const pwaBlock =
7
+ (configManager: ConfigManager<ApplicationConfigEntry>) => (config: Config) => {
8
+ config.batch(pwaSharedBlock(configManager));
9
+ };
@@ -0,0 +1,26 @@
1
+ import path from 'path';
2
+ import type Config from 'webpack-chain';
3
+ import type { ConfigManager } from '../../../../config/configManager';
4
+ import type { ApplicationConfigEntry } from '../../../../typings/configEntry/application';
5
+
6
+ export const pwaSharedBlock =
7
+ (configManager: ConfigManager<ApplicationConfigEntry>) => (config: Config) => {
8
+ const {
9
+ experiments: { pwa },
10
+ rootDir,
11
+ root,
12
+ } = configManager;
13
+
14
+ config.plugin('define').tap((args) => [
15
+ {
16
+ ...args[0],
17
+ 'process.env.TRAMVAI_PWA_WORKBOX_ENABLED': JSON.stringify(pwa.workbox?.enabled),
18
+ // @todo duplicated logic with path.join
19
+ 'process.env.TRAMVAI_PWA_SW_SRC': JSON.stringify(path.join(rootDir, root, pwa.sw?.src)),
20
+ 'process.env.TRAMVAI_PWA_SW_DEST': JSON.stringify(pwa.sw?.dest),
21
+ 'process.env.TRAMVAI_PWA_SW_SCOPE': JSON.stringify(pwa.sw?.scope),
22
+ 'process.env.TRAMVAI_PWA_MANIFEST_ENABLED': JSON.stringify(pwa.webmanifest?.enabled),
23
+ 'process.env.TRAMVAI_PWA_MANIFEST_DEST': JSON.stringify(pwa.webmanifest?.dest),
24
+ },
25
+ ]);
26
+ };
@@ -0,0 +1,87 @@
1
+ import type webpack from 'webpack';
2
+ import type { Compiler } from 'webpack';
3
+ import type Sharp from 'sharp';
4
+ import type { PwaIconOptions } from '../../../typings/pwa';
5
+
6
+ const pluginName = 'PwaIconsPlugin';
7
+
8
+ export class PwaIconsPlugin implements webpack.WebpackPluginInstance {
9
+ private hash: string;
10
+
11
+ constructor(private options: PwaIconOptions) {
12
+ this.options = options;
13
+ }
14
+
15
+ apply(compiler: Compiler) {
16
+ const { webpack } = compiler;
17
+ const { Compilation } = webpack;
18
+ const { RawSource } = webpack.sources;
19
+ const { src, dest, sizes } = this.options;
20
+
21
+ compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
22
+ // watch icon source file
23
+ if (!compilation.fileDependencies.has(src)) {
24
+ compilation.fileDependencies.add(src);
25
+ }
26
+
27
+ compilation.hooks.processAssets.tapPromise(
28
+ {
29
+ name: pluginName,
30
+ stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
31
+ },
32
+ async () => {
33
+ try {
34
+ const sharp: typeof Sharp = require('sharp');
35
+ // check icon source file updates
36
+ const nextHash = await new Promise<string>((resolve) => {
37
+ compilation.fileSystemInfo.getFileHash(src, (_, hash) => {
38
+ resolve(hash);
39
+ });
40
+ });
41
+
42
+ if (!this.hash) {
43
+ this.hash = nextHash;
44
+ } else if (this.hash === nextHash) {
45
+ // skip if icon source file not changed
46
+ return;
47
+ }
48
+
49
+ // @todo persistent cache between builds !!!
50
+ const promises = sizes.map((size) => {
51
+ return sharp(src)
52
+ .resize({ width: size, height: size })
53
+ .png({
54
+ quality: 95,
55
+ compressionLevel: 8,
56
+ adaptiveFiltering: true,
57
+ progressive: true,
58
+ force: true,
59
+ })
60
+ .toBuffer()
61
+ .then((content) => ({
62
+ filename: `${dest}/${size}x${size}.png`,
63
+ content,
64
+ }));
65
+ });
66
+
67
+ const results = await Promise.all(promises);
68
+
69
+ results.forEach(({ filename, content }) => {
70
+ const source = new RawSource(content);
71
+ const asset = compilation.getAsset(filename);
72
+
73
+ if (asset) {
74
+ compilation.updateAsset(filename, source);
75
+ } else {
76
+ compilation.emitAsset(filename, source);
77
+ }
78
+ });
79
+ } catch (e) {
80
+ // @todo: throw error?
81
+ console.error('PWA icons processing error:', e);
82
+ }
83
+ }
84
+ );
85
+ });
86
+ }
87
+ }
@@ -0,0 +1,32 @@
1
+ import type webpack from 'webpack';
2
+ import type { Compiler } from 'webpack';
3
+ import type { WebManifestOptions } from '../../../typings/pwa';
4
+
5
+ const pluginName = 'WebManifestPlugin';
6
+
7
+ export class WebManifestPlugin implements webpack.WebpackPluginInstance {
8
+ constructor(private options: WebManifestOptions) {
9
+ this.options = options;
10
+ }
11
+
12
+ apply(compiler: Compiler) {
13
+ const { webpack } = compiler;
14
+ const { Compilation } = webpack;
15
+ const { RawSource } = webpack.sources;
16
+ const { dest, enabled, ...content } = this.options;
17
+
18
+ compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
19
+ compilation.hooks.processAssets.tap(
20
+ {
21
+ name: pluginName,
22
+ stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
23
+ },
24
+ () => {
25
+ const manifestFilename = dest;
26
+
27
+ compilation.emitAsset(manifestFilename, new RawSource(JSON.stringify(content)));
28
+ }
29
+ );
30
+ });
31
+ }
32
+ }