pebble/sdk/waftools/process_js.py
2025-01-27 11:38:16 -08:00

266 lines
11 KiB
Python

# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import os
import subprocess
from string import Template
from waflib.Errors import WafError
from waflib.TaskGen import before_method, feature
from waflib import Context, Logs, Node, Task
from sdk_helpers import find_sdk_component, get_node_from_abspath
from sdk_helpers import process_package
@feature('rockyjs')
@before_method('process_sdk_resources')
def process_rocky_js(task_gen):
"""
Lint the JS source files using a Rocky-specific linter
Keyword arguments:
js -- a list of JS files to process for the build
:param task_gen: the task generator instance
:return: N/A
"""
bld = task_gen.bld
task_gen.mappings = {'': (lambda task_gen, node: None)}
js_nodes = task_gen.to_nodes(task_gen.source)
target = task_gen.to_nodes(task_gen.target)
if not js_nodes:
task_gen.bld.fatal("Project does not contain any source code.")
js_nodes.append(find_sdk_component(bld, task_gen.env, 'include/rocky.js'))
# This locates the available node_modules folders and performs a search for the rocky-lint
# module. This code remains in this file un-abstracted because similar functionality is not yet
# needed elsewhere.
node_modules = []
rocky_linter = None
if bld.path.find_node('node_modules'):
node_modules.append(bld.path.find_node('node_modules'))
if bld.env.NODE_PATH:
node_modules.append(bld.root.find_node(bld.env.NODE_PATH))
for node_modules_node in node_modules:
rocky_linter = node_modules_node.ant_glob('rocky-lint/**/rocky-lint.js')
if rocky_linter:
rocky_linter = rocky_linter[0]
break
rocky_definitions = find_sdk_component(bld, task_gen.env, 'tools/rocky-lint/rocky.d.ts')
if rocky_linter and rocky_definitions:
lintable_nodes = [node for node in js_nodes if node.is_child_of(bld.path)]
lint_task = task_gen.create_task('lint_js', src=lintable_nodes)
lint_task.linter = [task_gen.env.NODE,
rocky_linter.path_from(bld.path),
'-d',
rocky_definitions.path_from(bld.path)]
else:
Logs.pprint('YELLOW', "Rocky JS linter not present - skipping lint task")
# Create JS merge task for Rocky.js files
merge_task = task_gen.create_task('merge_js', src=js_nodes, tgt=target)
merge_task.js_entry_file = task_gen.js_entry_file
merge_task.js_build_type = 'rocky'
@feature('js')
@before_method('make_pbl_bundle', 'make_lib_bundle')
def process_js(task_gen):
"""
Merge the JS source files into a single JS file if enableMultiJS is set to 'true', otherwise,
skip JS processing
Keyword arguments:
js -- A list of JS files to process for the build
:param task_gen: the task generator instance
:return: N/A
"""
# Skip JS handling if there are no JS files
js_nodes = task_gen.to_nodes(getattr(task_gen, 'js', []))
if not js_nodes:
return
# Create JS merge task if the project specifies "enableMultiJS: true"
if task_gen.env.PROJECT_INFO.get('enableMultiJS', False):
target_js = task_gen.bld.path.get_bld().make_node('pebble-js-app.js')
target_js_map = target_js.change_ext('.js.map')
task_gen.js = [target_js, target_js_map]
merge_task = task_gen.create_task('merge_js', src=js_nodes, tgt=[target_js, target_js_map])
merge_task.js_entry_file = task_gen.js_entry_file
merge_task.js_build_type = 'pkjs'
merge_task.js_source_map_config = {
'sourceMapFilename': target_js_map.name
}
return
# Check for pebble-js-app.js if developer does not specify "enableMultiJS: true" in
# the project
if task_gen.env.BUILD_TYPE != 'lib':
for node in js_nodes:
if 'pebble-js-app.js' in node.abspath():
break
else:
Logs.pprint("CYAN",
"WARNING: enableMultiJS is not enabled for this project and "
"pebble-js-app.js does not exist")
# For apps without multiJS enabled and libs, copy JS files from src folder to build folder,
# skipping any files already in the build folder
js_nodes_to_copy = [js_node for js_node in js_nodes if not js_node.is_bld()]
if not js_nodes_to_copy:
task_gen.js = js_nodes
return
target_nodes = []
for js in js_nodes_to_copy:
if js.is_child_of(task_gen.bld.path.find_node('src')):
js_path = js.path_from(task_gen.bld.path.find_node('src'))
else:
js_path = os.path.abspath(js.path_from(task_gen.bld.path))
target_node = task_gen.bld.path.get_bld().make_node(js_path)
target_node.parent.mkdir()
target_nodes.append(target_node)
task_gen.js = target_nodes + list(set(js_nodes) - set(js_nodes_to_copy))
task_gen.create_task('copy_js', src=js_nodes_to_copy, tgt=target_nodes)
class copy_js(Task.Task):
"""
Task class for copying source JS files to a target location
"""
def run(self):
"""
This method executes when the JS copy task runs
:return: N/A
"""
bld = self.generator.bld
if len(self.inputs) != len(self.outputs):
bld.fatal("Number of input JS files ({}) does not match number of target JS files ({})".
format(len(self.inputs), len(self.outputs)))
for i in range(len(self.inputs)):
bld.cmd_and_log('cp "{src}" "{tgt}"'.
format(src=self.inputs[i].abspath(), tgt=self.outputs[i].abspath()),
quiet=Context.BOTH)
class merge_js(Task.Task):
"""
Task class for merging all specified JS files into one `pebble-js-app.js` file
"""
def run(self):
"""
This method executes when the JS merge task runs
:return: N/A
"""
bld = self.generator.bld
js_build_type = getattr(self, 'js_build_type')
# Check for a valid JS entry point among JS files
js_nodes = self.inputs
entry_point = bld.path.find_resource(self.js_entry_file)
if entry_point not in js_nodes:
bld.fatal("\n\nJS entry file '{}' not found in JS source files '{}'. We expect to find "
"a javascript file here that we will execute directly when your app launches."
"\n\nIf you are an advanced user, you can supply the 'js_entry_file' "
"parameter to 'pbl_bundle' in your wscript to change the default entry point."
" Note that doing this will break CloudPebble compatibility.".
format(self.js_entry_file, js_nodes))
target_js = self.outputs[0]
entry = [
entry_point.abspath()
]
if js_build_type == 'pkjs':
# NOTE: The order is critical here.
# _pkjs_shared_additions.js MUST be the first in the `entry` array!
entry.insert(0, "_pkjs_shared_additions.js")
if self.env.BUILD_TYPE == 'rocky':
entry.insert(1, "_pkjs_message_wrapper.js")
common_node = bld.root.find_node(self.generator.env.PEBBLE_SDK_COMMON)
tools_webpack_node = common_node.find_node('tools').find_node('webpack')
webpack_config_template_node = tools_webpack_node.find_node('webpack-config.js.pytemplate')
with open(webpack_config_template_node.abspath()) as f:
webpack_config_template_content = f.read()
search_paths = [
common_node.find_node('include').abspath(),
tools_webpack_node.abspath(),
bld.root.find_node(self.generator.env.NODE_PATH).abspath(),
bld.path.get_bld().make_node('js').abspath()
]
pebble_packages = [str(lib['name']) for lib in bld.env.LIB_JSON if 'pebble' in lib]
aliases = {lib: "{}/dist/js".format(lib) for lib in pebble_packages}
info_json_file = bld.path.find_node('package.json') or bld.path.find_node('appinfo.json')
if info_json_file:
aliases.update({'app_package.json': info_json_file.abspath()})
config_file = (
bld.path.get_bld().make_node("webpack/{}/webpack.config.js".format(js_build_type)))
config_file.parent.mkdir()
with open(config_file.abspath(), 'w') as f:
m = {
'IS_SANDBOX': bool(self.env.SANDBOX),
'ENTRY_FILENAMES': entry,
'OUTPUT_PATH': target_js.parent.path_from(bld.path),
'OUTPUT_FILENAME': target_js.name,
'RESOLVE_ROOTS': search_paths,
'RESOLVE_ALIASES': aliases,
'SOURCE_MAP_CONFIG': getattr(self, 'js_source_map_config', None)
}
f.write(Template(webpack_config_template_content).substitute(
{k: json.dumps(m[k], separators=(',\n',': ')) for k in m }))
cmd = (
"'{webpack}' --config {config} --display-modules".
format(webpack=self.generator.env.WEBPACK, config=config_file.path_from(bld.path)))
try:
out = bld.cmd_and_log(cmd, quiet=Context.BOTH, output=Context.STDOUT)
except WafError as e:
bld.fatal("JS bundling failed\n{}\n{}".format(e.stdout, e.stderr))
else:
if self.env.VERBOSE > 0:
Logs.pprint('WHITE', out)
class lint_js(Task.Task):
"""
Task class for linting JS source files with a specified linter script.
"""
def run(self):
"""
This method executes when the JS lint task runs
:return: N/A
"""
self.name = 'lint_js'
js_nodes = self.inputs
for js_node in js_nodes:
cmd = self.linter + [js_node.path_from(self.generator.bld.path)]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if err:
Logs.pprint('CYAN', "\n========== Lint Results: {} ==========\n".format(js_node))
Logs.pprint('WHITE', "{}\n{}\n".format(out, err))
if proc.returncode != 0:
self.generator.bld.fatal("Project failed linting.")