process_js.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. # Copyright 2024 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import json
  15. import os
  16. import subprocess
  17. from string import Template
  18. from waflib.Errors import WafError
  19. from waflib.TaskGen import before_method, feature
  20. from waflib import Context, Logs, Node, Task
  21. from sdk_helpers import find_sdk_component, get_node_from_abspath
  22. from sdk_helpers import process_package
  23. @feature('rockyjs')
  24. @before_method('process_sdk_resources')
  25. def process_rocky_js(task_gen):
  26. """
  27. Lint the JS source files using a Rocky-specific linter
  28. Keyword arguments:
  29. js -- a list of JS files to process for the build
  30. :param task_gen: the task generator instance
  31. :return: N/A
  32. """
  33. bld = task_gen.bld
  34. task_gen.mappings = {'': (lambda task_gen, node: None)}
  35. js_nodes = task_gen.to_nodes(task_gen.source)
  36. target = task_gen.to_nodes(task_gen.target)
  37. if not js_nodes:
  38. task_gen.bld.fatal("Project does not contain any source code.")
  39. js_nodes.append(find_sdk_component(bld, task_gen.env, 'include/rocky.js'))
  40. # This locates the available node_modules folders and performs a search for the rocky-lint
  41. # module. This code remains in this file un-abstracted because similar functionality is not yet
  42. # needed elsewhere.
  43. node_modules = []
  44. rocky_linter = None
  45. if bld.path.find_node('node_modules'):
  46. node_modules.append(bld.path.find_node('node_modules'))
  47. if bld.env.NODE_PATH:
  48. node_modules.append(bld.root.find_node(bld.env.NODE_PATH))
  49. for node_modules_node in node_modules:
  50. rocky_linter = node_modules_node.ant_glob('rocky-lint/**/rocky-lint.js')
  51. if rocky_linter:
  52. rocky_linter = rocky_linter[0]
  53. break
  54. rocky_definitions = find_sdk_component(bld, task_gen.env, 'tools/rocky-lint/rocky.d.ts')
  55. if rocky_linter and rocky_definitions:
  56. lintable_nodes = [node for node in js_nodes if node.is_child_of(bld.path)]
  57. lint_task = task_gen.create_task('lint_js', src=lintable_nodes)
  58. lint_task.linter = [task_gen.env.NODE,
  59. rocky_linter.path_from(bld.path),
  60. '-d',
  61. rocky_definitions.path_from(bld.path)]
  62. else:
  63. Logs.pprint('YELLOW', "Rocky JS linter not present - skipping lint task")
  64. # Create JS merge task for Rocky.js files
  65. merge_task = task_gen.create_task('merge_js', src=js_nodes, tgt=target)
  66. merge_task.js_entry_file = task_gen.js_entry_file
  67. merge_task.js_build_type = 'rocky'
  68. @feature('js')
  69. @before_method('make_pbl_bundle', 'make_lib_bundle')
  70. def process_js(task_gen):
  71. """
  72. Merge the JS source files into a single JS file if enableMultiJS is set to 'true', otherwise,
  73. skip JS processing
  74. Keyword arguments:
  75. js -- A list of JS files to process for the build
  76. :param task_gen: the task generator instance
  77. :return: N/A
  78. """
  79. # Skip JS handling if there are no JS files
  80. js_nodes = task_gen.to_nodes(getattr(task_gen, 'js', []))
  81. if not js_nodes:
  82. return
  83. # Create JS merge task if the project specifies "enableMultiJS: true"
  84. if task_gen.env.PROJECT_INFO.get('enableMultiJS', False):
  85. target_js = task_gen.bld.path.get_bld().make_node('pebble-js-app.js')
  86. target_js_map = target_js.change_ext('.js.map')
  87. task_gen.js = [target_js, target_js_map]
  88. merge_task = task_gen.create_task('merge_js', src=js_nodes, tgt=[target_js, target_js_map])
  89. merge_task.js_entry_file = task_gen.js_entry_file
  90. merge_task.js_build_type = 'pkjs'
  91. merge_task.js_source_map_config = {
  92. 'sourceMapFilename': target_js_map.name
  93. }
  94. return
  95. # Check for pebble-js-app.js if developer does not specify "enableMultiJS: true" in
  96. # the project
  97. if task_gen.env.BUILD_TYPE != 'lib':
  98. for node in js_nodes:
  99. if 'pebble-js-app.js' in node.abspath():
  100. break
  101. else:
  102. Logs.pprint("CYAN",
  103. "WARNING: enableMultiJS is not enabled for this project and "
  104. "pebble-js-app.js does not exist")
  105. # For apps without multiJS enabled and libs, copy JS files from src folder to build folder,
  106. # skipping any files already in the build folder
  107. js_nodes_to_copy = [js_node for js_node in js_nodes if not js_node.is_bld()]
  108. if not js_nodes_to_copy:
  109. task_gen.js = js_nodes
  110. return
  111. target_nodes = []
  112. for js in js_nodes_to_copy:
  113. if js.is_child_of(task_gen.bld.path.find_node('src')):
  114. js_path = js.path_from(task_gen.bld.path.find_node('src'))
  115. else:
  116. js_path = os.path.abspath(js.path_from(task_gen.bld.path))
  117. target_node = task_gen.bld.path.get_bld().make_node(js_path)
  118. target_node.parent.mkdir()
  119. target_nodes.append(target_node)
  120. task_gen.js = target_nodes + list(set(js_nodes) - set(js_nodes_to_copy))
  121. task_gen.create_task('copy_js', src=js_nodes_to_copy, tgt=target_nodes)
  122. class copy_js(Task.Task):
  123. """
  124. Task class for copying source JS files to a target location
  125. """
  126. def run(self):
  127. """
  128. This method executes when the JS copy task runs
  129. :return: N/A
  130. """
  131. bld = self.generator.bld
  132. if len(self.inputs) != len(self.outputs):
  133. bld.fatal("Number of input JS files ({}) does not match number of target JS files ({})".
  134. format(len(self.inputs), len(self.outputs)))
  135. for i in range(len(self.inputs)):
  136. bld.cmd_and_log('cp "{src}" "{tgt}"'.
  137. format(src=self.inputs[i].abspath(), tgt=self.outputs[i].abspath()),
  138. quiet=Context.BOTH)
  139. class merge_js(Task.Task):
  140. """
  141. Task class for merging all specified JS files into one `pebble-js-app.js` file
  142. """
  143. def run(self):
  144. """
  145. This method executes when the JS merge task runs
  146. :return: N/A
  147. """
  148. bld = self.generator.bld
  149. js_build_type = getattr(self, 'js_build_type')
  150. # Check for a valid JS entry point among JS files
  151. js_nodes = self.inputs
  152. entry_point = bld.path.find_resource(self.js_entry_file)
  153. if entry_point not in js_nodes:
  154. bld.fatal("\n\nJS entry file '{}' not found in JS source files '{}'. We expect to find "
  155. "a javascript file here that we will execute directly when your app launches."
  156. "\n\nIf you are an advanced user, you can supply the 'js_entry_file' "
  157. "parameter to 'pbl_bundle' in your wscript to change the default entry point."
  158. " Note that doing this will break CloudPebble compatibility.".
  159. format(self.js_entry_file, js_nodes))
  160. target_js = self.outputs[0]
  161. entry = [
  162. entry_point.abspath()
  163. ]
  164. if js_build_type == 'pkjs':
  165. # NOTE: The order is critical here.
  166. # _pkjs_shared_additions.js MUST be the first in the `entry` array!
  167. entry.insert(0, "_pkjs_shared_additions.js")
  168. if self.env.BUILD_TYPE == 'rocky':
  169. entry.insert(1, "_pkjs_message_wrapper.js")
  170. common_node = bld.root.find_node(self.generator.env.PEBBLE_SDK_COMMON)
  171. tools_webpack_node = common_node.find_node('tools').find_node('webpack')
  172. webpack_config_template_node = tools_webpack_node.find_node('webpack-config.js.pytemplate')
  173. with open(webpack_config_template_node.abspath()) as f:
  174. webpack_config_template_content = f.read()
  175. search_paths = [
  176. common_node.find_node('include').abspath(),
  177. tools_webpack_node.abspath(),
  178. bld.root.find_node(self.generator.env.NODE_PATH).abspath(),
  179. bld.path.get_bld().make_node('js').abspath()
  180. ]
  181. pebble_packages = [str(lib['name']) for lib in bld.env.LIB_JSON if 'pebble' in lib]
  182. aliases = {lib: "{}/dist/js".format(lib) for lib in pebble_packages}
  183. info_json_file = bld.path.find_node('package.json') or bld.path.find_node('appinfo.json')
  184. if info_json_file:
  185. aliases.update({'app_package.json': info_json_file.abspath()})
  186. config_file = (
  187. bld.path.get_bld().make_node("webpack/{}/webpack.config.js".format(js_build_type)))
  188. config_file.parent.mkdir()
  189. with open(config_file.abspath(), 'w') as f:
  190. m = {
  191. 'IS_SANDBOX': bool(self.env.SANDBOX),
  192. 'ENTRY_FILENAMES': entry,
  193. 'OUTPUT_PATH': target_js.parent.path_from(bld.path),
  194. 'OUTPUT_FILENAME': target_js.name,
  195. 'RESOLVE_ROOTS': search_paths,
  196. 'RESOLVE_ALIASES': aliases,
  197. 'SOURCE_MAP_CONFIG': getattr(self, 'js_source_map_config', None)
  198. }
  199. f.write(Template(webpack_config_template_content).substitute(
  200. {k: json.dumps(m[k], separators=(',\n',': ')) for k in m }))
  201. cmd = (
  202. "'{webpack}' --config {config} --display-modules".
  203. format(webpack=self.generator.env.WEBPACK, config=config_file.path_from(bld.path)))
  204. try:
  205. out = bld.cmd_and_log(cmd, quiet=Context.BOTH, output=Context.STDOUT)
  206. except WafError as e:
  207. bld.fatal("JS bundling failed\n{}\n{}".format(e.stdout, e.stderr))
  208. else:
  209. if self.env.VERBOSE > 0:
  210. Logs.pprint('WHITE', out)
  211. class lint_js(Task.Task):
  212. """
  213. Task class for linting JS source files with a specified linter script.
  214. """
  215. def run(self):
  216. """
  217. This method executes when the JS lint task runs
  218. :return: N/A
  219. """
  220. self.name = 'lint_js'
  221. js_nodes = self.inputs
  222. for js_node in js_nodes:
  223. cmd = self.linter + [js_node.path_from(self.generator.bld.path)]
  224. proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  225. out, err = proc.communicate()
  226. if err:
  227. Logs.pprint('CYAN', "\n========== Lint Results: {} ==========\n".format(js_node))
  228. Logs.pprint('WHITE', "{}\n{}\n".format(out, err))
  229. if proc.returncode != 0:
  230. self.generator.bld.fatal("Project failed linting.")