Coverage for src/kwai/frontend/vite.py: 96%

71 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2024-01-01 00:00 +0000

1"""Module for defining a class that handles files and assets created with vite.""" 

2 

3from abc import ABC, abstractmethod 

4from pathlib import Path 

5 

6from kwai.frontend.manifest import Chunk, Manifest 

7 

8 

9class Vite(ABC): 

10 """Interface for a Vite runtime.""" 

11 

12 @abstractmethod 

13 def init(self, *entries: str): 

14 """Initialize the Vite runtime for the given entries.""" 

15 raise NotImplementedError 

16 

17 @abstractmethod 

18 def get_scripts(self, base_url: str) -> list[str]: 

19 """Get the scripts for the given entries.""" 

20 raise NotImplementedError 

21 

22 @abstractmethod 

23 def get_css(self, base_url: str) -> list[str]: 

24 """Get the css files for the given entries.""" 

25 raise NotImplementedError 

26 

27 @abstractmethod 

28 def get_preloads(self, base_url: str) -> list[str]: 

29 """Get the preloads for the given entries.""" 

30 raise NotImplementedError 

31 

32 @abstractmethod 

33 def get_asset_path(self, asset_path: Path) -> Path | None: 

34 """Return the path for an asset. 

35 

36 None is returned when the file should not be processed. This is the case 

37 in the development environment, because Vite will serve the assets. 

38 

39 None should also be returned when the file does not exist in the dist folder. 

40 This way, the url path will be handled by Vue Router. 

41 

42 When the path starts with public, the path without 'public' is used to search 

43 the file in the dist folder. 

44 """ 

45 

46 

47class DevelopmentVite(Vite): 

48 """Vite implementation for development.""" 

49 

50 def __init__(self, server_url: str): 

51 """Initialize the development version of vite. 

52 

53 Args: 

54 server_url: The url for the vite server 

55 

56 !!! Note 

57 When vite is configured (see vite.config.ts) with a base then make sure that 

58 this base is also part of the server_url. For example: when base is 

59 '/apps/author' and the server is running on localhost with port 3001, then 

60 server_url should be: 'http://localhost:3001/apps/author'. 

61 """ 

62 self._server_url = server_url 

63 self._entries: list[str] = [] 

64 

65 def init(self, *entries: str): 

66 self._entries = entries 

67 

68 def get_scripts(self, base_url: str) -> list[str]: 

69 scripts = [f"{self._server_url}/@vite/client"] 

70 for entry in self._entries: 

71 scripts.append(f"{self._server_url}/{entry}") 

72 return scripts 

73 

74 def get_css(self, base_url: str) -> list[str]: 

75 return [] 

76 

77 def get_preloads(self, base_url: str) -> list[str]: 

78 return [] 

79 

80 def get_asset_path(self, asset_path: Path) -> Path | None: 

81 return None 

82 

83 

84class ProductionVite(Vite): 

85 """Vite implementation for production.""" 

86 

87 def __init__(self, manifest_filepath: Path, base_path: Path): 

88 """Initialize the production Vite runtime. 

89 

90 Args: 

91 manifest_filepath: Path to the manifest file. 

92 base_path: Path to the dist folder. 

93 

94 !!! Note 

95 base_path is the path where the dist folder of an application is installed 

96 For example '/website/frontend/apps/portal' must contain a dist folder. 

97 """ 

98 self._manifest_filepath: Path = manifest_filepath 

99 self._base_path: Path = base_path 

100 self._manifest: Manifest | None = None 

101 self._imported_chunks: dict[str, Chunk] = {} # chunks imported by the entries 

102 

103 def init(self, *entries: str): 

104 """Load the manifest file.""" 

105 self._manifest = Manifest.load_from_file(self._manifest_filepath) 

106 self._imported_chunks = self._find_imported_chunks(*entries) 

107 

108 def get_scripts(self, base_url: str) -> list[str]: 

109 scripts = [] 

110 

111 for chunk in self._imported_chunks.values(): 

112 if chunk.entry: 

113 scripts.append(base_url + chunk.file) 

114 

115 return scripts 

116 

117 def get_css(self, base_url: str) -> list[str]: 

118 styles = [] 

119 for chunk in self._imported_chunks.values(): 

120 for css in chunk.css: 

121 styles.append(base_url + css) 

122 return styles 

123 

124 def get_preloads(self, base_url: str) -> list[str]: 

125 preloads = [] 

126 

127 for chunk in self._imported_chunks.values(): 

128 if chunk.file.endswith(".js") and not chunk.entry: 

129 preloads.append(base_url + chunk.file) 

130 

131 return preloads 

132 

133 def _find_imported_chunks(self, *entries: str) -> dict[str, Chunk]: 

134 chunks = {} 

135 

136 for entry in entries: 

137 if not self._manifest.has_chunk(entry): 

138 raise ValueError(f"{entry} is not a chunk in {self._manifest_filepath}") 

139 

140 chunk = self._manifest.get_chunk(entry) 

141 if not chunk.entry: 

142 raise ValueError( 

143 f"{entry} is not an entry point in {self._manifest_filepath}" 

144 ) 

145 

146 chunks[entry] = chunk 

147 

148 imports = chunk.imports 

149 while imports: 

150 import_ = imports.pop() 

151 if import_ not in chunks: 

152 import_chunk = self._manifest.get_chunk(import_) 

153 chunks[import_] = import_chunk 

154 imports.extend(import_chunk.imports) 

155 

156 return chunks 

157 

158 def get_asset_path(self, path: Path) -> Path | None: 

159 dist_path = self._base_path / "dist" 

160 asset_file = dist_path / path 

161 if asset_file.is_relative_to(dist_path) and asset_file.is_file(): 

162 return asset_file 

163 

164 return None