From cd13276a2df1c33b5daabdd3a07f57e3128e471c Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Sun, 13 Feb 2022 13:22:20 -0700 Subject: [PATCH 1/5] Added dash.get_relative_path, dash.strip_relative_path, dash.get_asset_url --- dash/__init__.py | 5 ++ dash/_get_paths.py | 157 +++++++++++++++++++++++++++++++++++++ dash/_utils.py | 55 ------------- dash/dash.py | 26 +++--- tests/unit/test_configs.py | 2 +- 5 files changed, 173 insertions(+), 72 deletions(-) create mode 100644 dash/_get_paths.py diff --git a/dash/__init__.py b/dash/__init__.py index 85dd8b55b5..1753182a10 100644 --- a/dash/__init__.py +++ b/dash/__init__.py @@ -21,3 +21,8 @@ from .version import __version__ # noqa: F401,E402 from ._callback_context import callback_context # noqa: F401,E402 from ._callback import callback, clientside_callback # noqa: F401,E402 +from ._get_paths import ( + get_asset_url, + get_relative_path, + strip_relative_path, +) # noqa: F401,E402 diff --git a/dash/_get_paths.py b/dash/_get_paths.py new file mode 100644 index 0000000000..efd66a9817 --- /dev/null +++ b/dash/_get_paths.py @@ -0,0 +1,157 @@ +from ._utils import AttributeDict +from . import exceptions + +CONFIG = AttributeDict() + + +def get_asset_url(path): + return real_get_assets_url(CONFIG, path) + + +def real_get_assets_url(config, path): + if config.assets_external_path: + prefix = config.assets_external_path + else: + prefix = config.requests_pathname_prefix + return "/".join( + [ + # Only take the first part of the pathname + prefix.rstrip("/"), + config.assets_url_path.lstrip("/"), + path, + ] + ) + + +def get_relative_path(path): + """ + Return a path with `requests_pathname_prefix` prefixed before it. + Use this function when specifying local URL paths that will work + in environments regardless of what `requests_pathname_prefix` is. + In some deployment environments, like Dash Enterprise, + `requests_pathname_prefix` is set to the application name, + e.g. `my-dash-app`. + When working locally, `requests_pathname_prefix` might be unset and + so a relative URL like `/page-2` can just be `/page-2`. + However, when the app is deployed to a URL like `/my-dash-app`, then + `app.get_relative_path('/page-2')` will return `/my-dash-app/page-2`. + This can be used as an alternative to `get_asset_url` as well with + `app.get_relative_path('/assets/logo.png')` + + Use this function with `app.strip_relative_path` in callbacks that + deal with `dcc.Location` `pathname` routing. + That is, your usage may look like: + ``` + app.layout = html.Div([ + dcc.Location(id='url'), + html.Div(id='content') + ]) + @app.callback(Output('content', 'children'), [Input('url', 'pathname')]) + def display_content(path): + page_name = app.strip_relative_path(path) + if not page_name: # None or '' + return html.Div([ + dcc.Link(href=app.get_relative_path('/page-1')), + dcc.Link(href=app.get_relative_path('/page-2')), + ]) + elif page_name == 'page-1': + return chapters.page_1 + if page_name == "page-2": + return chapters.page_2 + ``` + """ + return real_get_relative_path(CONFIG, path) + + +def real_get_relative_path(config, path): + requests_pathname = config.requests_pathname_prefix + if requests_pathname == "/" and path == "": + return "/" + if requests_pathname != "/" and path == "": + return requests_pathname + if not path.startswith("/"): + raise exceptions.UnsupportedRelativePath( + """ + Paths that aren't prefixed with a leading / are not supported. + You supplied: {} + """.format( + path + ) + ) + return "/".join([requests_pathname.rstrip("/"), path.lstrip("/")]) + + +def strip_relative_path(path): + """ + Return a path with `requests_pathname_prefix` and leading and trailing + slashes stripped from it. Also, if None is passed in, None is returned. + Use this function with `get_relative_path` in callbacks that deal + with `dcc.Location` `pathname` routing. + That is, your usage may look like: + ``` + app.layout = html.Div([ + dcc.Location(id='url'), + html.Div(id='content') + ]) + @app.callback(Output('content', 'children'), [Input('url', 'pathname')]) + def display_content(path): + page_name = app.strip_relative_path(path) + if not page_name: # None or '' + return html.Div([ + dcc.Link(href=app.get_relative_path('/page-1')), + dcc.Link(href=app.get_relative_path('/page-2')), + ]) + elif page_name == 'page-1': + return chapters.page_1 + if page_name == "page-2": + return chapters.page_2 + ``` + Note that `chapters.page_1` will be served if the user visits `/page-1` + _or_ `/page-1/` since `strip_relative_path` removes the trailing slash. + + Also note that `strip_relative_path` is compatible with + `get_relative_path` in environments where `requests_pathname_prefix` set. + In some deployment environments, like Dash Enterprise, + `requests_pathname_prefix` is set to the application name, e.g. `my-dash-app`. + When working locally, `requests_pathname_prefix` might be unset and + so a relative URL like `/page-2` can just be `/page-2`. + However, when the app is deployed to a URL like `/my-dash-app`, then + `app.get_relative_path('/page-2')` will return `/my-dash-app/page-2` + + The `pathname` property of `dcc.Location` will return '`/my-dash-app/page-2`' + to the callback. + In this case, `app.strip_relative_path('/my-dash-app/page-2')` + will return `'page-2'` + + For nested URLs, slashes are still included: + `app.strip_relative_path('/page-1/sub-page-1/')` will return + `page-1/sub-page-1` + ``` + """ + return real_strip_relative_path(CONFIG, path) + + +def real_strip_relative_path(config, path): + requests_pathname = config.requests_pathname_prefix + if path is None: + return None + if ( + requests_pathname != "/" and not path.startswith(requests_pathname.rstrip("/")) + ) or (requests_pathname == "/" and not path.startswith("/")): + raise exceptions.UnsupportedRelativePath( + """ + Paths that aren't prefixed with requests_pathname_prefix are not supported. + You supplied: {} and requests_pathname_prefix was {} + """.format( + path, requests_pathname + ) + ) + if requests_pathname != "/" and path.startswith(requests_pathname.rstrip("/")): + path = path.replace( + # handle the case where the path might be `/my-dash-app` + # but the requests_pathname_prefix is `/my-dash-app/` + requests_pathname.rstrip("/"), + "", + 1, + ) + return path.strip("/") diff --git a/dash/_utils.py b/dash/_utils.py index 59490053a4..31de7e88cb 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -9,7 +9,6 @@ import io import json from functools import wraps -from . import exceptions logger = logging.getLogger() @@ -47,60 +46,6 @@ def generate_hash(): return str(uuid.uuid4().hex).strip("-") -def get_asset_path(requests_pathname, asset_path, asset_url_path): - - return "/".join( - [ - # Only take the first part of the pathname - requests_pathname.rstrip("/"), - asset_url_path, - asset_path, - ] - ) - - -def get_relative_path(requests_pathname, path): - if requests_pathname == "/" and path == "": - return "/" - if requests_pathname != "/" and path == "": - return requests_pathname - if not path.startswith("/"): - raise exceptions.UnsupportedRelativePath( - """ - Paths that aren't prefixed with a leading / are not supported. - You supplied: {} - """.format( - path - ) - ) - return "/".join([requests_pathname.rstrip("/"), path.lstrip("/")]) - - -def strip_relative_path(requests_pathname, path): - if path is None: - return None - if ( - requests_pathname != "/" and not path.startswith(requests_pathname.rstrip("/")) - ) or (requests_pathname == "/" and not path.startswith("/")): - raise exceptions.UnsupportedRelativePath( - """ - Paths that aren't prefixed with requests_pathname_prefix are not supported. - You supplied: {} and requests_pathname_prefix was {} - """.format( - path, requests_pathname - ) - ) - if requests_pathname != "/" and path.startswith(requests_pathname.rstrip("/")): - path = path.replace( - # handle the case where the path might be `/my-dash-app` - # but the requests_pathname_prefix is `/my-dash-app/` - requests_pathname.rstrip("/"), - "", - 1, - ) - return path.strip("/") - - # pylint: disable=no-member def patch_collections_abc(member): return getattr(collections.abc, member) diff --git a/dash/dash.py b/dash/dash.py index 3073d42503..c66dc67a9f 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -42,17 +42,15 @@ AttributeDict, format_tag, generate_hash, - get_asset_path, - get_relative_path, inputs_to_dict, inputs_to_vals, interpolate_str, patch_collections_abc, split_callback_id, - strip_relative_path, to_json, ) from . import _callback +from . import _get_paths from . import _dash_renderer from . import _validate from . import _watch @@ -366,6 +364,8 @@ def __init__( "via the Dash constructor" ) + _get_paths.CONFIG = self.config + # keep title as a class property for backwards compatibility self.title = title @@ -424,6 +424,9 @@ def __init__( self.logger.setLevel(logging.INFO) + def _get_config(self): + return self.config + def init_app(self, app=None, **kwargs): """Initialize the parts of Dash that require a flask app.""" @@ -1470,14 +1473,7 @@ def csp_hashes(self, hash_algorithm="sha256"): ] def get_asset_url(self, path): - if self.config.assets_external_path: - prefix = self.config.assets_external_path - else: - prefix = self.config.requests_pathname_prefix - - asset = get_asset_path(prefix, path, self.config.assets_url_path.lstrip("/")) - - return asset + return _get_paths.real_get_assets_url(self.config, path) def get_relative_path(self, path): """ @@ -1516,9 +1512,7 @@ def display_content(path): return chapters.page_2 ``` """ - asset = get_relative_path(self.config.requests_pathname_prefix, path) - - return asset + return _get_paths.real_get_relative_path(self.config, path) def strip_relative_path(self, path): """ @@ -1567,7 +1561,7 @@ def display_content(path): `page-1/sub-page-1` ``` """ - return strip_relative_path(self.config.requests_pathname_prefix, path) + return _get_paths.real_strip_relative_path(self.config, path) def _setup_dev_tools(self, **kwargs): debug = kwargs.get("debug", False) @@ -1743,7 +1737,7 @@ def enable_dev_tools( if hasattr(package, "path") and "dash/dash" in os.path.dirname( package.path ): - component_packages_dist[i : i + 1] = [ + component_packages_dist[i: i + 1] = [ os.path.join(os.path.dirname(package.path), x) for x in ["dcc", "html", "dash_table"] ] diff --git a/tests/unit/test_configs.py b/tests/unit/test_configs.py index d5a416e93c..a72c5ce2a6 100644 --- a/tests/unit/test_configs.py +++ b/tests/unit/test_configs.py @@ -13,7 +13,7 @@ get_combined_config, load_dash_env_vars, ) -from dash._utils import get_asset_path, get_relative_path, strip_relative_path +from dash import get_asset_path, get_relative_path, strip_relative_path @pytest.fixture From fa358844bd581837672c53ff063149bcf31a2e5f Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Sun, 13 Feb 2022 18:08:04 -0700 Subject: [PATCH 2/5] fixed tests --- dash/_get_paths.py | 14 ++++++-------- dash/dash.py | 4 ++-- tests/unit/test_configs.py | 19 +++++++++++++------ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/dash/_get_paths.py b/dash/_get_paths.py index efd66a9817..d5089222ab 100644 --- a/dash/_get_paths.py +++ b/dash/_get_paths.py @@ -5,10 +5,10 @@ def get_asset_url(path): - return real_get_assets_url(CONFIG, path) + return real_get_asset_url(CONFIG, path) -def real_get_assets_url(config, path): +def real_get_asset_url(config, path): if config.assets_external_path: prefix = config.assets_external_path else: @@ -60,11 +60,10 @@ def display_content(path): return chapters.page_2 ``` """ - return real_get_relative_path(CONFIG, path) + return real_get_relative_path(CONFIG.requests_pathname_prefix, path) -def real_get_relative_path(config, path): - requests_pathname = config.requests_pathname_prefix +def real_get_relative_path(requests_pathname, path): if requests_pathname == "/" and path == "": return "/" if requests_pathname != "/" and path == "": @@ -128,11 +127,10 @@ def display_content(path): `page-1/sub-page-1` ``` """ - return real_strip_relative_path(CONFIG, path) + return real_strip_relative_path(CONFIG.requests_pathname_prefix, path) -def real_strip_relative_path(config, path): - requests_pathname = config.requests_pathname_prefix +def real_strip_relative_path(requests_pathname, path): if path is None: return None if ( diff --git a/dash/dash.py b/dash/dash.py index c66dc67a9f..6d764985a5 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1473,7 +1473,7 @@ def csp_hashes(self, hash_algorithm="sha256"): ] def get_asset_url(self, path): - return _get_paths.real_get_assets_url(self.config, path) + return _get_paths.real_get_asset_url(self.config, path) def get_relative_path(self, path): """ @@ -1737,7 +1737,7 @@ def enable_dev_tools( if hasattr(package, "path") and "dash/dash" in os.path.dirname( package.path ): - component_packages_dist[i: i + 1] = [ + component_packages_dist[i : i + 1] = [ os.path.join(os.path.dirname(package.path), x) for x in ["dcc", "html", "dash_table"] ] diff --git a/tests/unit/test_configs.py b/tests/unit/test_configs.py index a72c5ce2a6..c1fc6b3694 100644 --- a/tests/unit/test_configs.py +++ b/tests/unit/test_configs.py @@ -13,7 +13,13 @@ get_combined_config, load_dash_env_vars, ) -from dash import get_asset_path, get_relative_path, strip_relative_path + +from dash._utils import AttributeDict +from dash._get_paths import ( + real_get_asset_url, + real_get_relative_path, + real_strip_relative_path, +) @pytest.fixture @@ -101,7 +107,8 @@ def test_pathname_prefix_environ_requests(empty_environ): ], ) def test_pathname_prefix_assets(empty_environ, req, expected): - path = get_asset_path(req, "reset.css", "assets") + config = AttributeDict(assets_external_path=req, assets_url_path="assets") + path = real_get_asset_url(config, "reset.css") assert path == expected @@ -209,7 +216,7 @@ def test_app_name_server(empty_environ, name, server, expected): ], ) def test_pathname_prefix_relative_url(prefix, partial_path, expected): - path = get_relative_path(prefix, partial_path) + path = real_get_relative_path(prefix, partial_path) assert path == expected @@ -219,7 +226,7 @@ def test_pathname_prefix_relative_url(prefix, partial_path, expected): ) def test_invalid_get_relative_path(prefix, partial_path): with pytest.raises(_exc.UnsupportedRelativePath): - get_relative_path(prefix, partial_path) + real_get_relative_path(prefix, partial_path) @pytest.mark.parametrize( @@ -247,7 +254,7 @@ def test_invalid_get_relative_path(prefix, partial_path): ], ) def test_strip_relative_path(prefix, partial_path, expected): - path = strip_relative_path(prefix, partial_path) + path = real_strip_relative_path(prefix, partial_path) assert path == expected @@ -261,7 +268,7 @@ def test_strip_relative_path(prefix, partial_path, expected): ) def test_invalid_strip_relative_path(prefix, partial_path): with pytest.raises(_exc.UnsupportedRelativePath): - strip_relative_path(prefix, partial_path) + real_strip_relative_path(prefix, partial_path) def test_port_env_fail_str(empty_environ): From 1fe99dc6677d9c598d9361d088fe3498afdfd61d Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Mon, 14 Feb 2022 06:52:31 -0700 Subject: [PATCH 3/5] added tests --- dash/__init__.py | 4 +-- dash/_get_paths.py | 12 +++---- dash/dash.py | 10 ++++-- tests/unit/test_configs.py | 66 ++++++++++++++++++++++++++++++++------ 4 files changed, 71 insertions(+), 21 deletions(-) diff --git a/dash/__init__.py b/dash/__init__.py index 1753182a10..7e59891b8b 100644 --- a/dash/__init__.py +++ b/dash/__init__.py @@ -21,8 +21,8 @@ from .version import __version__ # noqa: F401,E402 from ._callback_context import callback_context # noqa: F401,E402 from ._callback import callback, clientside_callback # noqa: F401,E402 -from ._get_paths import ( +from ._get_paths import ( # noqa: F401,E402 get_asset_url, get_relative_path, strip_relative_path, -) # noqa: F401,E402 +) diff --git a/dash/_get_paths.py b/dash/_get_paths.py index d5089222ab..0f031cbe45 100644 --- a/dash/_get_paths.py +++ b/dash/_get_paths.py @@ -5,10 +5,10 @@ def get_asset_url(path): - return real_get_asset_url(CONFIG, path) + return app_get_asset_url(CONFIG, path) -def real_get_asset_url(config, path): +def app_get_asset_url(config, path): if config.assets_external_path: prefix = config.assets_external_path else: @@ -60,10 +60,10 @@ def display_content(path): return chapters.page_2 ``` """ - return real_get_relative_path(CONFIG.requests_pathname_prefix, path) + return app_get_relative_path(CONFIG.requests_pathname_prefix, path) -def real_get_relative_path(requests_pathname, path): +def app_get_relative_path(requests_pathname, path): if requests_pathname == "/" and path == "": return "/" if requests_pathname != "/" and path == "": @@ -127,10 +127,10 @@ def display_content(path): `page-1/sub-page-1` ``` """ - return real_strip_relative_path(CONFIG.requests_pathname_prefix, path) + return app_strip_relative_path(CONFIG.requests_pathname_prefix, path) -def real_strip_relative_path(requests_pathname, path): +def app_strip_relative_path(requests_pathname, path): if path is None: return None if ( diff --git a/dash/dash.py b/dash/dash.py index 6d764985a5..f39ddd67a9 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1473,7 +1473,7 @@ def csp_hashes(self, hash_algorithm="sha256"): ] def get_asset_url(self, path): - return _get_paths.real_get_asset_url(self.config, path) + return _get_paths.app_get_asset_url(self.config, path) def get_relative_path(self, path): """ @@ -1512,7 +1512,9 @@ def display_content(path): return chapters.page_2 ``` """ - return _get_paths.real_get_relative_path(self.config, path) + return _get_paths.app_get_relative_path( + self.config.requests_pathname_prefix, path + ) def strip_relative_path(self, path): """ @@ -1561,7 +1563,9 @@ def display_content(path): `page-1/sub-page-1` ``` """ - return _get_paths.real_strip_relative_path(self.config, path) + return _get_paths.app_strip_relative_path( + self.config.requests_pathname_prefix, path + ) def _setup_dev_tools(self, **kwargs): debug = kwargs.get("debug", False) diff --git a/tests/unit/test_configs.py b/tests/unit/test_configs.py index c1fc6b3694..a7e307bdf7 100644 --- a/tests/unit/test_configs.py +++ b/tests/unit/test_configs.py @@ -16,9 +16,12 @@ from dash._utils import AttributeDict from dash._get_paths import ( - real_get_asset_url, - real_get_relative_path, - real_strip_relative_path, + app_get_asset_url, + app_get_relative_path, + app_strip_relative_path, + get_asset_url, + get_relative_path, + strip_relative_path, ) @@ -108,7 +111,7 @@ def test_pathname_prefix_environ_requests(empty_environ): ) def test_pathname_prefix_assets(empty_environ, req, expected): config = AttributeDict(assets_external_path=req, assets_url_path="assets") - path = real_get_asset_url(config, "reset.css") + path = app_get_asset_url(config, "reset.css") assert path == expected @@ -142,8 +145,51 @@ def test_asset_url( assets_url_path=assets_url_path, ) - path = app.get_asset_url("reset.css") - assert path == expected + app_path = app.get_asset_url("reset.css") + dash_path = get_asset_url("reset.css") + assert app_path == dash_path == expected + + +@pytest.mark.parametrize( + "requests_pathname_prefix, expected", + [ + (None, "/page2"), + ("/app/", "/app/page2"), + ], +) +def test_get_relative_path( + empty_environ, + requests_pathname_prefix, + expected, +): + app = Dash( + "Dash", + requests_pathname_prefix=requests_pathname_prefix, + ) + app_path = app.get_relative_path("/page2") + dash_path = get_relative_path("/page2") + assert app_path == dash_path == expected + + +@pytest.mark.parametrize( + "requests_pathname_prefix, expected", + [ + (None, "/app/page2"), + ("/app/", "/page2"), + ], +) +def test_strip_relative_path( + empty_environ, + requests_pathname_prefix, + expected, +): + app = Dash( + "Dash", + requests_pathname_prefix=requests_pathname_prefix, + ) + app_path = app.strip_relative_path("/app/page2") + dash_path = strip_relative_path("/app/page2") + assert app_path == dash_path == expected def test_get_combined_config_dev_tools_ui(empty_environ): @@ -216,7 +262,7 @@ def test_app_name_server(empty_environ, name, server, expected): ], ) def test_pathname_prefix_relative_url(prefix, partial_path, expected): - path = real_get_relative_path(prefix, partial_path) + path = app_get_relative_path(prefix, partial_path) assert path == expected @@ -226,7 +272,7 @@ def test_pathname_prefix_relative_url(prefix, partial_path, expected): ) def test_invalid_get_relative_path(prefix, partial_path): with pytest.raises(_exc.UnsupportedRelativePath): - real_get_relative_path(prefix, partial_path) + app_get_relative_path(prefix, partial_path) @pytest.mark.parametrize( @@ -254,7 +300,7 @@ def test_invalid_get_relative_path(prefix, partial_path): ], ) def test_strip_relative_path(prefix, partial_path, expected): - path = real_strip_relative_path(prefix, partial_path) + path = app_strip_relative_path(prefix, partial_path) assert path == expected @@ -268,7 +314,7 @@ def test_strip_relative_path(prefix, partial_path, expected): ) def test_invalid_strip_relative_path(prefix, partial_path): with pytest.raises(_exc.UnsupportedRelativePath): - real_strip_relative_path(prefix, partial_path) + app_strip_relative_path(prefix, partial_path) def test_port_env_fail_str(empty_environ): From 91cd0cdd1b3ddd328cdd43e55e832d0265ad07a1 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Mon, 14 Feb 2022 08:51:26 -0700 Subject: [PATCH 4/5] updated CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ dash/dash.py | 3 --- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3a419e9b2..7115ac8f29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Added +- [#1923](https://github.com/plotly/dash/pull/1923): +- `dash.get_relative_path` +- `dash.strip_relative_path` +- `dash.get_asset_url` +This is similar to `dash.callback` where you don't need the `app` object. It makes it possible to use these +functions in the `pages` folder of a multi-page app without running into the circular `app` imports issue. + +## [2.1.0] - 2022-01-22 + ### Changed - [#1876](https://github.com/plotly/dash/pull/1876) Delays finalizing `Dash.config` attributes not used in the constructor until `init_app()`. - [#1869](https://github.com/plotly/dash/pull/1869), [#1873](https://github.com/plotly/dash/pull/1873) Upgrade Plotly.js to v2.8.3. This includes: diff --git a/dash/dash.py b/dash/dash.py index f39ddd67a9..aee3d3b2c4 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -424,9 +424,6 @@ def __init__( self.logger.setLevel(logging.INFO) - def _get_config(self): - return self.config - def init_app(self, app=None, **kwargs): """Initialize the parts of Dash that require a flask app.""" From 85a1ab85f7eb9390c7dcb0f6182324409915e190 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Tue, 15 Feb 2022 09:25:24 -0700 Subject: [PATCH 5/5] removed --production --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4c46e25e2d..d371514bba 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -111,7 +111,7 @@ jobs: . venv/bin/activate set -eo pipefail pip install -e . --progress-bar off && pip list | grep dash - npm install --production && npm run initialize + npm install npm run initialize npm run build npm run lint - run: