@shoplflow/extension 0.0.2

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 (76) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +172 -0
  4. package/dist/assets/css/contentStyle17107484687.chunk.css +1 -0
  5. package/dist/assets/css/optionsIndex.chunk.css +1 -0
  6. package/dist/assets/css/popupIndex.chunk.css +1 -0
  7. package/dist/assets/js/index.a235624f.js +1 -0
  8. package/dist/assets/js/jsx-runtime.ea866ad2.js +40 -0
  9. package/dist/assets/js/twind.60e4b726.js +1 -0
  10. package/dist/icon-128.png +0 -0
  11. package/dist/icon-34.png +0 -0
  12. package/dist/manifest.json +45 -0
  13. package/dist/src/pages/background/index.js +1 -0
  14. package/dist/src/pages/content/index.js +1 -0
  15. package/dist/src/pages/options/index.html +15 -0
  16. package/dist/src/pages/options/index.js +1 -0
  17. package/dist/src/pages/popup/index.html +16 -0
  18. package/dist/src/pages/popup/index.js +918 -0
  19. package/manifest.ts +37 -0
  20. package/package.json +57 -0
  21. package/public/icon-128.png +0 -0
  22. package/public/icon-34.png +0 -0
  23. package/public/manifest.json +45 -0
  24. package/src/Components/ElementListCard.tsx +35 -0
  25. package/src/assets/img/logo.svg +7 -0
  26. package/src/environment.d.ts +10 -0
  27. package/src/global.d.ts +37 -0
  28. package/src/pages/background/index.ts +11 -0
  29. package/src/pages/content/components/Demo/app.tsx +93 -0
  30. package/src/pages/content/components/Demo/index.tsx +27 -0
  31. package/src/pages/content/index.ts +7 -0
  32. package/src/pages/content/style.scss +7 -0
  33. package/src/pages/options/Options.css +8 -0
  34. package/src/pages/options/Options.tsx +8 -0
  35. package/src/pages/options/index.css +0 -0
  36. package/src/pages/options/index.html +12 -0
  37. package/src/pages/options/index.tsx +18 -0
  38. package/src/pages/panel/Panel.css +7 -0
  39. package/src/pages/panel/Panel.tsx +65 -0
  40. package/src/pages/panel/index.css +0 -0
  41. package/src/pages/panel/index.html +12 -0
  42. package/src/pages/panel/index.tsx +20 -0
  43. package/src/pages/popup/Popup.css +44 -0
  44. package/src/pages/popup/Popup.tsx +123 -0
  45. package/src/pages/popup/index.css +13 -0
  46. package/src/pages/popup/index.html +12 -0
  47. package/src/pages/popup/index.tsx +20 -0
  48. package/src/shared/hoc/withErrorBoundary.tsx +42 -0
  49. package/src/shared/hoc/withSuspense.tsx +14 -0
  50. package/src/shared/hooks/useStorage.tsx +47 -0
  51. package/src/shared/storages/base.ts +75 -0
  52. package/src/shared/storages/componentsInfoStorage.ts +46 -0
  53. package/src/shared/style/twind.ts +13 -0
  54. package/src/vite-env.d.ts +1 -0
  55. package/tsconfig.json +29 -0
  56. package/twind.config.ts +7 -0
  57. package/utils/checkShopl.ts +6 -0
  58. package/utils/log.ts +52 -0
  59. package/utils/manifest-parser/index.ts +35 -0
  60. package/utils/plugins/add-hmr.ts +46 -0
  61. package/utils/plugins/custom-dynamic-import.ts +34 -0
  62. package/utils/plugins/make-manifest.ts +49 -0
  63. package/utils/plugins/watch-rebuild.ts +16 -0
  64. package/utils/reload/constant.ts +5 -0
  65. package/utils/reload/initReloadClient.ts +52 -0
  66. package/utils/reload/initReloadServer.js +72 -0
  67. package/utils/reload/initReloadServer.ts +68 -0
  68. package/utils/reload/injections/script.js +60 -0
  69. package/utils/reload/injections/script.ts +12 -0
  70. package/utils/reload/injections/view.js +74 -0
  71. package/utils/reload/injections/view.ts +29 -0
  72. package/utils/reload/interpreter/index.ts +13 -0
  73. package/utils/reload/interpreter/types.ts +15 -0
  74. package/utils/reload/rollup.config.ts +28 -0
  75. package/utils/reload/utils.ts +9 -0
  76. package/vite.config.ts +89 -0
@@ -0,0 +1,123 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import withSuspense from '@src/shared/hoc/withSuspense';
3
+ import withErrorBoundary from '@src/shared/hoc/withErrorBoundary';
4
+ import '@shoplflow/base/styles';
5
+ import { ShoplflowProvider, Stack, Text, ScrollArea, colorTokens } from '@shoplflow/base';
6
+ import { ArcElement, Chart as ChartJS, Tooltip } from 'chart.js';
7
+ import { Doughnut } from 'react-chartjs-2';
8
+ import styled from '@emotion/styled';
9
+ import ElementListCard from '@src/Components/ElementListCard';
10
+
11
+ ChartJS.register(ArcElement, Tooltip);
12
+
13
+ const Card = styled.div`
14
+ display: flex;
15
+ flex-direction: column;
16
+ height: 100%;
17
+ gap: 20px;
18
+ width: fit-content;
19
+ background: ${colorTokens.neutral150};
20
+ padding: 20px;
21
+ border-radius: 10px;
22
+ `;
23
+
24
+ const TableWrapper = styled(Card)`
25
+ width: 100%;
26
+ padding: 12px 8px;
27
+ `;
28
+
29
+ const Popup = () => {
30
+ const [{ systems, components, systemElement }, setData] = useState({
31
+ systems: 0,
32
+ components: 0,
33
+ systemElement: {},
34
+ });
35
+
36
+ useEffect(() => {
37
+ (async () => {
38
+ await chrome.storage.local.get(['systems', 'components', 'systemElement'], (result) => setData(result));
39
+ })();
40
+ }, []);
41
+
42
+ const percentage = (systems / components) * 100;
43
+
44
+ const data = {
45
+ labels: ['HTML 요소', 'shoplflow'],
46
+ datasets: [
47
+ {
48
+ label: '# 컴포넌트 개수',
49
+ data: [components - systems, systems],
50
+ backgroundColor: ['#3eb0b8', '#84c2fc'],
51
+ borderColor: ['#2f839f', '#3299fe'],
52
+ borderWidth: 1,
53
+ },
54
+ ],
55
+ };
56
+ const sortedSystemElement = Object.keys(systemElement).sort((a, b) => systemElement[b] - systemElement[a]);
57
+
58
+ return (
59
+ <ShoplflowProvider>
60
+ <Stack.Vertical width={'500px'} height={'300px'} align={'center'} spacing={'spacing20'}>
61
+ <Stack.Horizontal height={'100%'} width={'100%'} justify={'space-between'} spacing={'spacing16'}>
62
+ <Card>
63
+ <Doughnut data={data} height={150} width={150} />
64
+ <Stack.Vertical width={'100%'} height={'100%'} spacing={'spacing12'} justify={'center'} align={'center'}>
65
+ <Text typography={'body1_700'} whiteSpace={'nowrap'}>
66
+ 디자인시스템 교체율 {percentage.toFixed(2)}%
67
+ </Text>
68
+ <Stack.Vertical width={'100%'} height={'100%'} spacing={'spacing06'} justify={'center'} align={'center'}>
69
+ <Stack.Horizontal width={'100%'} justify={'center'} align={'center'} spacing={'spacing04'}>
70
+ <Text typography={'caption_400'}>전체 요소</Text>
71
+ <Text typography={'caption_700'} color={'navy300'}>
72
+ {components}
73
+ </Text>
74
+ </Stack.Horizontal>
75
+ <Stack.Horizontal width={'100%'} justify={'center'} align={'center'} spacing={'spacing04'}>
76
+ <Text typography={'caption_400'}>시스템 요소</Text>
77
+ <Text typography={'caption_700'} color={'primary300'}>
78
+ {systems}
79
+ </Text>
80
+ </Stack.Horizontal>
81
+ </Stack.Vertical>
82
+ </Stack.Vertical>
83
+ </Card>
84
+ <TableWrapper>
85
+ <Text
86
+ typography={'body1_700'}
87
+ whiteSpace={'nowrap'}
88
+ style={{
89
+ padding: '4px 12px',
90
+ }}
91
+ >
92
+ 시스템 구성 요소
93
+ </Text>
94
+ <ScrollArea>
95
+ <Stack.Vertical spacing={'spacing04'}>
96
+ {sortedSystemElement?.map((key, index) => {
97
+ const color = {
98
+ 0: 'primary300',
99
+ 1: 'green300',
100
+ 2: 'yellow300',
101
+ };
102
+ return <ElementListCard key={key} title={key} value={systemElement[key]} valueColor={color[index]} />;
103
+ })}
104
+ </Stack.Vertical>
105
+ </ScrollArea>
106
+ </TableWrapper>
107
+ </Stack.Horizontal>
108
+ </Stack.Vertical>
109
+ </ShoplflowProvider>
110
+ );
111
+ };
112
+
113
+ export default withErrorBoundary(
114
+ withSuspense(Popup, <div> Loading ... </div>),
115
+ <div>
116
+ <Text typography={'body2_700'} whiteSpace={'nowrap'}>
117
+ 샤플 서비스가 아니거나 에러가 발생했어요.
118
+ </Text>
119
+ <Text typography={'body2_700'} whiteSpace={'nowrap'}>
120
+ shoplworks 도메인으로 접속해주세요.
121
+ </Text>
122
+ </div>,
123
+ );
@@ -0,0 +1,13 @@
1
+ body {
2
+
3
+ padding: 16px;
4
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
5
+ 'Droid Sans', 'Helvetica Neue', sans-serif;
6
+ -webkit-font-smoothing: antialiased;
7
+ -moz-osx-font-smoothing: grayscale;
8
+ position: relative;
9
+ }
10
+
11
+ code {
12
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
13
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Popup</title>
6
+ </head>
7
+
8
+ <body>
9
+ <div id="app-container"></div>
10
+ <script type="module" src="./index.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import '@pages/popup/index.css';
4
+ import Popup from '@pages/popup/Popup';
5
+ import refreshOnUpdate from 'virtual:reload-on-update-in-view';
6
+ import { attachTwindStyle } from '@src/shared/style/twind';
7
+
8
+ refreshOnUpdate('pages/popup');
9
+
10
+ function init() {
11
+ const appContainer = document.querySelector('#app-container');
12
+ if (!appContainer) {
13
+ throw new Error('Can not find #app-container');
14
+ }
15
+ attachTwindStyle(appContainer, document);
16
+ const root = createRoot(appContainer);
17
+ root.render(<Popup />);
18
+ }
19
+
20
+ init();
@@ -0,0 +1,42 @@
1
+ import { Component, ComponentType, ReactElement } from 'react';
2
+
3
+ class ErrorBoundary extends Component<
4
+ {
5
+ children: ReactElement;
6
+ fallback: ReactElement;
7
+ },
8
+ {
9
+ hasError: boolean;
10
+ }
11
+ > {
12
+ state = { hasError: false };
13
+
14
+ static getDerivedStateFromError() {
15
+ return { hasError: true };
16
+ }
17
+
18
+ componentDidCatch(error, errorInfo) {
19
+ console.error(error, errorInfo);
20
+ }
21
+
22
+ render() {
23
+ if (this.state.hasError) {
24
+ return this.props.fallback;
25
+ }
26
+
27
+ return this.props.children;
28
+ }
29
+ }
30
+
31
+ export default function withErrorBoundary<T extends Record<string, unknown>>(
32
+ Component: ComponentType<T>,
33
+ ErrorComponent: ReactElement,
34
+ ) {
35
+ return function WithErrorBoundary(props: T) {
36
+ return (
37
+ <ErrorBoundary fallback={ErrorComponent}>
38
+ <Component {...props} />
39
+ </ErrorBoundary>
40
+ );
41
+ };
42
+ }
@@ -0,0 +1,14 @@
1
+ import { ComponentType, ReactElement, Suspense } from 'react';
2
+
3
+ export default function withSuspense<T extends Record<string, unknown>>(
4
+ Component: ComponentType<T>,
5
+ SuspenseComponent: ReactElement,
6
+ ) {
7
+ return function WithSuspense(props: T) {
8
+ return (
9
+ <Suspense fallback={SuspenseComponent}>
10
+ <Component {...props} />
11
+ </Suspense>
12
+ );
13
+ };
14
+ }
@@ -0,0 +1,47 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import type { BaseStorage } from '@src/shared/storages/base';
3
+
4
+ type WrappedPromise = ReturnType<typeof wrapPromise>;
5
+ const storageMap: Map<BaseStorage<unknown>, WrappedPromise> = new Map();
6
+ function wrapPromise<R>(promise: Promise<R>) {
7
+ let status = 'pending';
8
+ let result: R;
9
+ const suspender = promise.then(
10
+ (r) => {
11
+ status = 'success';
12
+ result = r;
13
+ },
14
+ (e) => {
15
+ status = 'error';
16
+ result = e;
17
+ },
18
+ );
19
+
20
+ return {
21
+ read() {
22
+ if (status === 'pending') {
23
+ throw suspender;
24
+ } else if (status === 'error') {
25
+ throw result;
26
+ } else if (status === 'success') {
27
+ return result;
28
+ }
29
+ },
30
+ };
31
+ }
32
+
33
+ export default function useStorage<
34
+ Storage extends BaseStorage<Data>,
35
+ Data = Storage extends BaseStorage<infer Data> ? Data : unknown,
36
+ >(storage: Storage) {
37
+ const _data = useSyncExternalStore<Data | null>(storage.subscribe, storage.getSnapshot);
38
+
39
+ if (!storageMap.has(storage)) {
40
+ storageMap.set(storage, wrapPromise(storage.get()));
41
+ }
42
+ if (_data !== null) {
43
+ storageMap.set(storage, { read: () => _data });
44
+ }
45
+
46
+ return _data ?? (storageMap.get(storage)!.read() as Data);
47
+ }
@@ -0,0 +1,75 @@
1
+ export enum StorageType {
2
+ Local = 'local',
3
+ Sync = 'sync',
4
+ Managed = 'managed',
5
+ Session = 'session',
6
+ }
7
+
8
+ type ValueOrUpdate<D> = D | ((prev: D) => Promise<D> | D);
9
+
10
+ export type BaseStorage<D> = {
11
+ get: () => Promise<D>;
12
+ set: (value: ValueOrUpdate<D>) => Promise<void>;
13
+ getSnapshot: () => D | null;
14
+ subscribe: (listener: () => void) => () => void;
15
+ };
16
+
17
+ export function createStorage<D>(key: string, fallback: D, config?: { storageType?: StorageType }): BaseStorage<D> {
18
+ let cache: D | null = null;
19
+ let listeners: Array<() => void> = [];
20
+ const storageType = config?.storageType ?? StorageType.Local;
21
+
22
+ const _getDataFromStorage = async (): Promise<D> => {
23
+ if (chrome.storage[storageType] === undefined) {
24
+ throw new Error(`Check your storage permission into manifest.json: ${storageType} is not defined`);
25
+ }
26
+ const value = await chrome.storage[storageType].get([key]);
27
+ return value[key] ?? fallback;
28
+ };
29
+
30
+ const _emitChange = () => {
31
+ listeners.forEach(listener => listener());
32
+ };
33
+
34
+ const set = async (valueOrUpdate: ValueOrUpdate<D>) => {
35
+ if (typeof valueOrUpdate === 'function') {
36
+ // eslint-disable-next-line no-prototype-builtins
37
+ if (valueOrUpdate.hasOwnProperty('then')) {
38
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
39
+ // @ts-ignore
40
+ cache = await valueOrUpdate(cache);
41
+ } else {
42
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
43
+ // @ts-ignore
44
+ cache = valueOrUpdate(cache);
45
+ }
46
+ } else {
47
+ cache = valueOrUpdate;
48
+ }
49
+ await chrome.storage[storageType].set({ [key]: cache });
50
+ _emitChange();
51
+ };
52
+
53
+ const subscribe = (listener: () => void) => {
54
+ listeners = [...listeners, listener];
55
+ return () => {
56
+ listeners = listeners.filter(l => l !== listener);
57
+ };
58
+ };
59
+
60
+ const getSnapshot = () => {
61
+ return cache;
62
+ };
63
+
64
+ _getDataFromStorage().then(data => {
65
+ cache = data;
66
+ _emitChange();
67
+ });
68
+
69
+ return {
70
+ get: _getDataFromStorage,
71
+ set,
72
+ getSnapshot,
73
+ subscribe,
74
+ };
75
+ }
@@ -0,0 +1,46 @@
1
+ import type { BaseStorage } from '@src/shared/storages/base';
2
+ import { createStorage, StorageType } from '@src/shared/storages/base';
3
+
4
+ type ComponentsInfo = {
5
+ components: number;
6
+ systems: number;
7
+ refresh: number;
8
+ };
9
+
10
+ type ThemeStorage = BaseStorage<ComponentsInfo> & {
11
+ setStorage: (value: ComponentsInfo) => void;
12
+ refreshItems: (refresh: number) => void;
13
+ };
14
+
15
+ const storage = createStorage<ComponentsInfo>(
16
+ 'components-info',
17
+ {
18
+ systems: 0,
19
+ components: 0,
20
+ refresh: 0,
21
+ },
22
+ {
23
+ storageType: StorageType.Local,
24
+ },
25
+ );
26
+
27
+ const componentsInfoStorage: ThemeStorage = {
28
+ ...storage,
29
+ setStorage: ({ systems, components, refresh }) => {
30
+ storage.set((value) => {
31
+ return {
32
+ ...value,
33
+ refresh,
34
+ systems,
35
+ components,
36
+ };
37
+ });
38
+ },
39
+ refreshItems: (refresh) => {
40
+ storage.set((prev) => {
41
+ return { ...prev, refresh: refresh };
42
+ });
43
+ },
44
+ };
45
+
46
+ export default componentsInfoStorage;
@@ -0,0 +1,13 @@
1
+ import { twind, cssom, observe } from '@twind/core';
2
+ import 'construct-style-sheets-polyfill';
3
+ import config from '@root/twind.config';
4
+
5
+ export function attachTwindStyle<T extends { adoptedStyleSheets: unknown }>(
6
+ observedElement: Element,
7
+ documentOrShadowRoot: T,
8
+ ) {
9
+ const sheet = cssom(new CSSStyleSheet());
10
+ const tw = twind(config, sheet);
11
+ observe(tw, observedElement);
12
+ documentOrShadowRoot.adoptedStyleSheets = [sheet.target];
13
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ "noEmit": true,
4
+ "baseUrl": ".",
5
+ "allowJs": false,
6
+ "target": "esnext",
7
+ "module": "esnext",
8
+ "jsx": "react-jsx",
9
+ "skipLibCheck": true,
10
+ "esModuleInterop": true,
11
+ "resolveJsonModule": true,
12
+ "moduleResolution": "node",
13
+ "types": ["vite/client", "node"],
14
+ "noFallthroughCasesInSwitch": true,
15
+ "allowSyntheticDefaultImports": true,
16
+ "lib": ["dom", "dom.iterable", "esnext"],
17
+ "forceConsistentCasingInFileNames": true,
18
+ "typeRoots": ["./src/global.d.ts"],
19
+ "paths": {
20
+ "@root/*": ["./*"],
21
+ "@src/*": ["src/*"],
22
+ "@assets/*": ["src/assets/*"],
23
+ "@pages/*": ["src/pages/*"],
24
+ "virtual:reload-on-update-in-background-script": ["./src/global.d.ts"],
25
+ "virtual:reload-on-update-in-view": ["./src/global.d.ts"]
26
+ }
27
+ },
28
+ "include": ["src", "utils", "vite.config.ts", "node_modules/@types"]
29
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from '@twind/core';
2
+ import presetTailwind from '@twind/preset-tailwind';
3
+ import presetAutoprefix from '@twind/preset-autoprefix';
4
+
5
+ export default defineConfig({
6
+ presets: [presetAutoprefix(), presetTailwind()],
7
+ });
@@ -0,0 +1,6 @@
1
+ export const checkShopl = (origin: string) => {
2
+ if (origin.includes('shoplworks') || origin.includes('localhost')) {
3
+ return true;
4
+ }
5
+ return false;
6
+ };
package/utils/log.ts ADDED
@@ -0,0 +1,52 @@
1
+ type ColorType = 'success' | 'info' | 'error' | 'warning' | keyof typeof COLORS;
2
+ const COLORS = {
3
+ Reset: '\x1b[0m',
4
+ Bright: '\x1b[1m',
5
+ Dim: '\x1b[2m',
6
+ Underscore: '\x1b[4m',
7
+ Blink: '\x1b[5m',
8
+ Reverse: '\x1b[7m',
9
+ Hidden: '\x1b[8m',
10
+ FgBlack: '\x1b[30m',
11
+ FgRed: '\x1b[31m',
12
+ FgGreen: '\x1b[32m',
13
+ FgYellow: '\x1b[33m',
14
+ FgBlue: '\x1b[34m',
15
+ FgMagenta: '\x1b[35m',
16
+ FgCyan: '\x1b[36m',
17
+ FgWhite: '\x1b[37m',
18
+ BgBlack: '\x1b[40m',
19
+ BgRed: '\x1b[41m',
20
+ BgGreen: '\x1b[42m',
21
+ BgYellow: '\x1b[43m',
22
+ BgBlue: '\x1b[44m',
23
+ BgMagenta: '\x1b[45m',
24
+ BgCyan: '\x1b[46m',
25
+ BgWhite: '\x1b[47m',
26
+ } as const;
27
+
28
+ type ValueOf<T> = T[keyof T];
29
+
30
+ export default function colorLog(message: string, type?: ColorType) {
31
+ let color: ValueOf<typeof COLORS>;
32
+
33
+ switch (type) {
34
+ case 'success':
35
+ color = COLORS.FgGreen;
36
+ break;
37
+ case 'info':
38
+ color = COLORS.FgBlue;
39
+ break;
40
+ case 'error':
41
+ color = COLORS.FgRed;
42
+ break;
43
+ case 'warning':
44
+ color = COLORS.FgYellow;
45
+ break;
46
+ default:
47
+ color = COLORS[type];
48
+ break;
49
+ }
50
+
51
+ console.info(color, message);
52
+ }
@@ -0,0 +1,35 @@
1
+ type Manifest = chrome.runtime.ManifestV3;
2
+
3
+ class ManifestParser {
4
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
5
+ private constructor() {}
6
+
7
+ static convertManifestToString(manifest: Manifest): string {
8
+ if (process.env.__FIREFOX__) {
9
+ manifest = this.convertToFirefoxCompatibleManifest(manifest);
10
+ }
11
+ return JSON.stringify(manifest, null, 2);
12
+ }
13
+
14
+ static convertToFirefoxCompatibleManifest(manifest: Manifest) {
15
+ const manifestCopy = {
16
+ ...manifest,
17
+ } as { [key: string]: unknown };
18
+
19
+ manifestCopy.background = {
20
+ scripts: [manifest.background?.service_worker],
21
+ type: 'module',
22
+ };
23
+ manifestCopy.options_ui = {
24
+ page: manifest.options_page,
25
+ browser_style: false,
26
+ };
27
+ manifestCopy.content_security_policy = {
28
+ extension_pages: "script-src 'self'; object-src 'self'",
29
+ };
30
+ delete manifestCopy.options_page;
31
+ return manifestCopy as Manifest;
32
+ }
33
+ }
34
+
35
+ export default ManifestParser;
@@ -0,0 +1,46 @@
1
+ import * as path from 'path';
2
+ import { readFileSync } from 'fs';
3
+ import type { PluginOption } from 'vite';
4
+
5
+ const isDev = process.env.__DEV__ === 'true';
6
+
7
+ const DUMMY_CODE = `export default function(){};`;
8
+
9
+ function getInjectionCode(fileName: string): string {
10
+ return readFileSync(path.resolve(__dirname, '..', 'reload', 'injections', fileName), { encoding: 'utf8' });
11
+ }
12
+
13
+ type Config = {
14
+ background?: boolean;
15
+ view?: boolean;
16
+ };
17
+ function getResolvedId(id: string) {
18
+ return '\0' + id;
19
+ }
20
+
21
+ export default function addHmr(config?: Config): PluginOption {
22
+ const { background = false, view = true } = config || {};
23
+ const idInBackgroundScript = 'virtual:reload-on-update-in-background-script';
24
+ const idInView = 'virtual:reload-on-update-in-view';
25
+
26
+ const scriptHmrCode = isDev ? getInjectionCode('script.js') : DUMMY_CODE;
27
+ const viewHmrCode = isDev ? getInjectionCode('view.js') : DUMMY_CODE;
28
+
29
+ return {
30
+ name: 'add-hmr',
31
+ resolveId(id) {
32
+ if (id === idInBackgroundScript || id === idInView) {
33
+ return getResolvedId(id);
34
+ }
35
+ },
36
+ load(id) {
37
+ if (id === getResolvedId(idInBackgroundScript)) {
38
+ return background ? scriptHmrCode : DUMMY_CODE;
39
+ }
40
+
41
+ if (id === getResolvedId(idInView)) {
42
+ return view ? viewHmrCode : DUMMY_CODE;
43
+ }
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,34 @@
1
+ import type { PluginOption } from 'vite';
2
+
3
+ export default function customDynamicImport(): PluginOption {
4
+ return {
5
+ name: 'custom-dynamic-import',
6
+ renderDynamicImport({ moduleId }) {
7
+ if (!moduleId.includes('node_modules')) {
8
+ // ↑ dont modify any import from node_modules
9
+ if (process.env.__FIREFOX__) {
10
+ return {
11
+ left: `
12
+ {
13
+ const dynamicImport = (path) => import(path);
14
+ dynamicImport(browser.runtime.getURL('./') +
15
+ `,
16
+ right: ".split('../').join(''))}",
17
+ };
18
+ }
19
+ return {
20
+ left: `
21
+ {
22
+ const dynamicImport = (path) => import(path);
23
+ dynamicImport(
24
+ `,
25
+ right: ')}',
26
+ };
27
+ }
28
+ return {
29
+ left: 'import(',
30
+ right: ')',
31
+ };
32
+ },
33
+ };
34
+ }
@@ -0,0 +1,49 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import colorLog from '../log';
4
+ import ManifestParser from '../manifest-parser';
5
+ import type { PluginOption } from 'vite';
6
+
7
+ // eslint-disable-next-line @typescript-eslint/unbound-method
8
+ const { resolve } = path;
9
+
10
+ const distDir = resolve(__dirname, '..', '..', 'dist');
11
+ const publicDir = resolve(__dirname, '..', '..', 'public');
12
+
13
+ export default function makeManifest(
14
+ manifest: chrome.runtime.ManifestV3,
15
+ config: { isDev: boolean; contentScriptCssKey?: string },
16
+ ): PluginOption {
17
+ function makeManifest(to: string) {
18
+ if (!fs.existsSync(to)) {
19
+ fs.mkdirSync(to);
20
+ }
21
+ const manifestPath = resolve(to, 'manifest.json');
22
+
23
+ // Naming change for cache invalidation
24
+ if (config.contentScriptCssKey) {
25
+ manifest.content_scripts.forEach((script) => {
26
+ script.css = script.css.map((css) => css.replace('<KEY>', config.contentScriptCssKey));
27
+ });
28
+ }
29
+
30
+ fs.writeFileSync(manifestPath, ManifestParser.convertManifestToString(manifest));
31
+
32
+ colorLog(`Manifest file copy complete: ${manifestPath}`, 'success');
33
+ }
34
+
35
+ return {
36
+ name: 'make-manifest',
37
+ buildStart() {
38
+ if (config.isDev) {
39
+ makeManifest(distDir);
40
+ }
41
+ },
42
+ buildEnd() {
43
+ if (config.isDev) {
44
+ return;
45
+ }
46
+ makeManifest(publicDir);
47
+ },
48
+ };
49
+ }