diff --git a/samcli/commands/_utils/experimental.py b/samcli/commands/_utils/experimental.py index 34b1f7e4d34..e74dc3fd973 100644 --- a/samcli/commands/_utils/experimental.py +++ b/samcli/commands/_utils/experimental.py @@ -57,6 +57,7 @@ class ExperimentalFlag: UvPackageManager = ExperimentalEntry( "experimentalUvPackageManager", EXPERIMENTAL_ENV_VAR_PREFIX + "UV_PACKAGE_MANAGER" ) + NodejsMonorepo = ExperimentalEntry("experimentalNodejsMonorepo", EXPERIMENTAL_ENV_VAR_PREFIX + "NODEJS_MONOREPO") def is_experimental_enabled(config_entry: ExperimentalEntry) -> bool: diff --git a/samcli/commands/_utils/options.py b/samcli/commands/_utils/options.py index 01de507b117..a8377ca7b19 100644 --- a/samcli/commands/_utils/options.py +++ b/samcli/commands/_utils/options.py @@ -1053,7 +1053,17 @@ def build_in_source_click_option(): required=False, is_flag=True, help="Opts in to build project in the source folder. The following workflows support " - f"building in source: {SUPPORTED_BUILD_IN_SOURCE_WORKFLOWS}", + f"building in source: {SUPPORTED_BUILD_IN_SOURCE_WORKFLOWS}\n" + "\n" + "For `sam build` on an npm workspaces monorepo, npm installs once at the workspace root " + "instead of once per function, and each function's artifacts are narrowed to the " + "dependencies it resolves. The install runs in the workspace root npm itself reports, " + "which is inside your project but above the function's CodeUri.\n" + "\n" + "This grouping is in beta: enable it with --beta-features or " + "SAM_CLI_BETA_NODEJS_MONOREPO=1. Without it, every function installs for itself, as in " + "earlier releases. It is also skipped for --use-container and --cached builds, so " + "`sam sync` does not use it.", cls=ClickMutex, incompatible_params=["use_container", "hook_name"], ) diff --git a/samcli/commands/build/core/command.py b/samcli/commands/build/core/command.py index 8405a8881ff..a8f09c97928 100644 --- a/samcli/commands/build/core/command.py +++ b/samcli/commands/build/core/command.py @@ -74,6 +74,18 @@ def format_examples(ctx: Context, formatter: CommandHelpTextFormatter): ), ] ) + with formatter.indented_section( + name="Build an npm workspaces monorepo, installing once at the workspace root (beta)", + extra_indents=1, + ): + formatter.write_text_rows( + [ + RowDefinition( + name=style(f"$ {ctx.command_path} --build-in-source --beta-features"), + extra_row_modifiers=[ShowcaseRowModifier()], + ), + ] + ) with formatter.indented_section(name="Build sam project and deploy", extra_indents=1): formatter.write_text_rows( [ diff --git a/samcli/lib/build/app_builder.py b/samcli/lib/build/app_builder.py index 6f4811100bd..ae53505aa62 100644 --- a/samcli/lib/build/app_builder.py +++ b/samcli/lib/build/app_builder.py @@ -18,13 +18,18 @@ from aws_lambda_builders.builder import LambdaBuilder from aws_lambda_builders.exceptions import LambdaBuilderError -from samcli.commands._utils.experimental import get_enabled_experimental_flags +from samcli.commands._utils.experimental import ( + ExperimentalFlag, + get_enabled_experimental_flags, + is_experimental_enabled, +) from samcli.lib.build.build_graph import BuildGraph, FunctionBuildDefinition, LayerBuildDefinition from samcli.lib.build.build_strategy import ( BuildStrategy, CachedOrIncrementalBuildStrategyWrapper, DefaultBuildStrategy, ParallelBuildStrategy, + WorkspaceAwareBuildStrategy, ) from samcli.lib.build.constants import BUILD_PROPERTIES, DEPRECATED_RUNTIMES from samcli.lib.build.exceptions import ( @@ -209,6 +214,8 @@ def __init__( self._mount_symlinks = mount_symlinks self._use_buildkit = use_buildkit self._image_build_client: Optional[ImageBuildClient] = None + # one SubprocessNpm per build session, so `npm prefix` answers are cached per directory + self._npm_subprocess: Optional[object] = None @property def _container_client(self) -> ContainerClient: @@ -271,6 +278,26 @@ def build(self) -> ApplicationBuildResult: bool(self._container_manager), ) + if ( + is_experimental_enabled(ExperimentalFlag.NodejsMonorepo) + and self._build_in_source + and not self._cached + and not self._container_manager + ): + # npm workspaces monorepo: install the shared dependency tree once per workspace root + # instead of once per function. Outside ParallelBuildStrategy on purpose - the install + # must complete before any grouped function build starts. --cached is excluded because + # it supplies dependencies from .aws-sam/deps and manages download_dependencies itself. + # Behind SAM_CLI_BETA_NODEJS_MONOREPO while the grouping rolls out: without it every + # function keeps its own install, which is the behaviour every release so far has had. + build_strategy = WorkspaceAwareBuildStrategy( + build_graph, + build_strategy, + self._base_dir, + self._resolve_npm_project_root, + self._prepare_workspace_shared_project, + ) + return ApplicationBuildResult(build_graph, build_strategy.build()) def _get_build_graph( @@ -701,6 +728,68 @@ def _build_layer( # Not including subfolder in return so that we copy subfolder, instead of copying artifacts inside it. return artifact_dir + def _resolve_npm_project_root(self, code_dir: str) -> Optional[str]: + """ + Ask npm (through aws-lambda-builders) which project root covers code_dir. + + Returns None when the answer is unavailable for any reason - npm missing, an + aws-lambda-builders too old to expose the resolver - which disables workspace grouping for + that function and keeps today's per-function install. + """ + try: + from aws_lambda_builders.workflows.nodejs_npm.npm import SubprocessNpm + from aws_lambda_builders.workflows.nodejs_npm.utils import OSUtils as NpmOSUtils + except ImportError: + return None + + if self._npm_subprocess is None: + self._npm_subprocess = SubprocessNpm(NpmOSUtils()) + resolver = getattr(self._npm_subprocess, "resolve_project_root", None) + if resolver is None: + return None + try: + return cast(Optional[str], resolver(code_dir)) + except OSError as ex: + LOG.debug("Could not resolve the npm project root of %s: %s", code_dir, ex) + return None + + def _prepare_workspace_shared_project(self, project_root: str, build_definition: "FunctionBuildDefinition") -> bool: + """ + Do once, at project_root, the work every workspace function under it shares. + + For the npm workflows that is installing the hoisted dependency tree. The build definition + supplies the workflow (npm or npm-esbuild) whose builder does it; both funnel into the same + npm project tree. Returns False on any failure so the caller leaves the grouped functions on + per-function installs. + """ + code_dir = str(pathlib.Path(self._base_dir, cast(str, build_definition.codeuri)).resolve()) + specified_workflow = build_definition.metadata.get("BuildMethod") if build_definition.metadata else None + try: + config = get_workflow_config( + build_definition.runtime, code_dir, self._base_dir, specified_workflow=specified_workflow + ) + builder = LambdaBuilder( + language=config.language, + dependency_manager=config.dependency_manager, + application_framework=config.application_framework, + ) + prepare = getattr(builder, "prepare_shared_project", None) + if prepare is None: + LOG.debug("Installed aws-lambda-builders has no shared-project hook; skipping grouping") + return False + # The same flags the per-function build() calls get: the builders hook refuses without + # experimentalNodejsMonorepo, because the per-function artifacts link is gated on it too + # and preparing the root without it would ship artifacts carrying no dependencies. + prepare(project_root, experimental_flags=get_enabled_experimental_flags()) + return True + except (UnsupportedRuntimeException, LambdaBuilderError) as ex: + LOG.warning( + "Preparing the shared project root %s failed (%s); falling back to per-function installs", + project_root, + ex, + ) + return False + def _build_function( # pylint: disable=R1710 self, function_name: str, diff --git a/samcli/lib/build/build_strategy.py b/samcli/lib/build/build_strategy.py index 4b88553b9e5..318185ea3cf 100644 --- a/samcli/lib/build/build_strategy.py +++ b/samcli/lib/build/build_strategy.py @@ -693,3 +693,116 @@ def _is_incremental_build_supported(self, runtime: Optional[str]) -> bool: return True return False + + +class WorkspaceAwareBuildStrategy(BuildStrategy): + """ + Wraps another build strategy to install nodejs dependencies once per npm workspace root. + + In an npm workspaces monorepo npm keeps ONE dependency tree, hoisted to the workspace root, and + every member's install reconciles that whole tree. Building N functions from one monorepo with + --build-in-source therefore reconciles the same tree N times. This strategy resolves each nodejs + function's npm project root before any function builds, installs each distinct root once through + aws-lambda-builders, and marks the grouped functions with download_dependencies=False - the same + lever IncrementalBuildStrategy uses - so their builds link the artifacts to the tree that one + install wrote. + + A function is grouped only when all of these hold, so anything else keeps today's per-function + install: + + - Zip package type, nodejs runtime, npm or esbuild build method + - npm resolves the function's project root OUTSIDE its own code directory, which is what + distinguishes a workspace member from a standalone package + - the shared install itself succeeded; a failed root leaves its functions untouched + + This strategy must wrap OUTSIDE ParallelBuildStrategy, whose _build_functions fires every + definition into one AsyncContext: running the install first is the barrier that keeps parallel + function builds from racing the shared tree. It is wired only for non-cached builds; --cached + supplies dependencies from .aws-sam/deps and manages download_dependencies itself. + """ + + def __init__( + self, + build_graph: BuildGraph, + delegate_build_strategy: BuildStrategy, + base_dir: str, + project_root_resolver: Callable[[str], Optional[str]], + prepare_shared_project_function: Callable[[str, FunctionBuildDefinition], bool], + ) -> None: + """ + Parameters + ---------- + project_root_resolver: + Given a function's resolved code directory, returns npm's project root for it, or None + when it cannot be resolved (npm missing, too-old aws-lambda-builders, non-npm project). + prepare_shared_project_function: + Runs the shared install for (project_root, one grouped build definition); returns True + on success. Failures must be handled inside and reported with False. + """ + super().__init__(build_graph) + self._delegate_build_strategy = delegate_build_strategy + self._base_dir = base_dir + self._project_root_resolver = project_root_resolver + self._prepare_shared_project_function = prepare_shared_project_function + + def build(self) -> Dict[str, str]: + with self._delegate_build_strategy: + return super().build() + + def _build_layers(self, build_graph: BuildGraph) -> Dict[str, str]: + return self._delegate_build_strategy._build_layers(build_graph) + + def _build_functions(self, build_graph: BuildGraph) -> Dict[str, str]: + self._prepare_shared_project_for_workspace_groups(build_graph) + return self._delegate_build_strategy._build_functions(build_graph) + + def build_single_function_definition(self, build_definition: FunctionBuildDefinition) -> Dict[str, str]: + return self._delegate_build_strategy.build_single_function_definition(build_definition) + + def build_single_layer_definition(self, layer_definition: LayerBuildDefinition) -> Dict[str, str]: + return self._delegate_build_strategy.build_single_layer_definition(layer_definition) + + def _prepare_shared_project_for_workspace_groups(self, build_graph: BuildGraph) -> None: + groups: Dict[str, List[FunctionBuildDefinition]] = {} + for build_definition in build_graph.get_function_build_definitions(): + project_root = self._workspace_root_for(build_definition) + if project_root: + groups.setdefault(project_root, []).append(build_definition) + + for project_root, build_definitions in groups.items(): + LOG.info( + "Installing dependencies once for workspace root %s, shared by (%s)", + project_root, + ", ".join(definition.get_resource_full_paths() for definition in build_definitions), + ) + if self._prepare_shared_project_function(project_root, build_definitions[0]): + for build_definition in build_definitions: + build_definition.download_dependencies = False + + def _workspace_root_for(self, build_definition: FunctionBuildDefinition) -> Optional[str]: + """ + The npm project root the function's install would write to, or None when the function is not + a workspace member covered by this strategy. + """ + if build_definition.packagetype != ZIP or not build_definition.codeuri: + return None + runtime = build_definition.runtime or "" + if not runtime.startswith("nodejs"): + return None + # only the npm workflows install through the npm project tree; BuildMethod naming the + # runtime is the template's way of spelling the default + build_method = (build_definition.metadata or {}).get("BuildMethod") + if build_method not in (None, "esbuild", runtime): + return None + + code_dir = str(pathlib.Path(self._base_dir, build_definition.codeuri).resolve()) + project_root = self._project_root_resolver(code_dir) + if not project_root: + return None + # npm answers with the directory itself for anything that is not a workspace member. normcase + # because npm's stdout and this path are spelled independently, and on Windows two spellings + # differing only in case - a drive letter included - name the same directory; reading those as + # different roots would group a project that is not a workspace at all. No-op off Windows. + if os.path.normcase(os.path.realpath(project_root)) == os.path.normcase(os.path.realpath(code_dir)): + return None + return project_root diff --git a/schema/samcli.json b/schema/samcli.json index fb48d4346fb..0288ffa3f36 100644 --- a/schema/samcli.json +++ b/schema/samcli.json @@ -310,7 +310,7 @@ "properties": { "parameters": { "title": "Parameters for the build command", - "description": "Available parameters for the build command:\n* terraform_project_root_path:\nUsed for passing the Terraform project root directory path. Current directory will be used as a default value, if this parameter is not provided.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* use_container:\nBuild functions within an AWS Lambda-like container.\n* use_buildkit:\nEnable buildkit for container image builds. Requires Docker with buildx plugin or Finch CLI.\n* build_in_source:\nOpts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* container_env_var:\nEnvironment variables to be passed into build containers\nResource format (FuncName.VarName=Value) or Global format (VarName=Value).\n\n Example: --container-env-var Func1.VAR1=value1 --container-env-var VAR2=value2\n* container_env_var_file:\nEnvironment variables json file (e.g., env_vars.json) to be passed to containers.\n* build_image:\nContainer image URIs for building functions/layers. You can specify for all functions/layers with just the image URI (--build-image public.ecr.aws/sam/build-nodejs18.x:latest). You can specify for each individual function with (--build-image FunctionLogicalID=public.ecr.aws/sam/build-nodejs18.x:latest). A combination of the two can be used. If a function does not have build image specified or an image URI for all functions, the default SAM CLI build images will be used.\n* exclude:\nName of the resource(s) to exclude from AWS SAM CLI build.\n* parallel:\nEnable parallel builds for AWS SAM template's functions and layers.\n* mount_with:\nSpecify mount mode for building functions/layers inside container. If it is mounted with write permissions, some files in source code directory may be changed/added by the build process. By default the source code directory is read only.\n* mount_symlinks:\nSpecify if symlinks at the top level of the code should be mounted inside the container. Activating this flag could allow access to locations outside of your workspace by using a symbolic link. By default symlinks are not mounted.\n* build_dir:\nDirectory to store build artifacts.Note: This directory will be first removed before starting a build.\n* cache_dir:\nDirectory to store cached artifacts. The default cache directory is .aws-sam/cache\n* base_dir:\nResolve relative paths to function's source code with respect to this directory. Use this if SAM template and source code are not in same enclosing folder. By default, relative paths are resolved with respect to the SAM template's location.\n* manifest:\nPath to a custom dependency manifest. Example: custom-package.json\n* cached:\nEnable cached builds.Reuse build artifacts that have not changed from previous builds. \n\nAWS SAM CLI evaluates if files in your project directory have changed. \n\nNote: AWS SAM CLI does not evaluate changes made to third party modules that the project depends on.Example: Python function includes a requirements.txt file with the following entry requests=1.x and the latest request module version changes from 1.1 to 1.2, AWS SAM CLI will not pull the latest version until a non-cached build is run.\n* template_file:\nAWS SAM template file.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* output:\nOutput the results from the command in a given output format. Supported formats: text (default), json.\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the build command:\n* terraform_project_root_path:\nUsed for passing the Terraform project root directory path. Current directory will be used as a default value, if this parameter is not provided.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* use_container:\nBuild functions within an AWS Lambda-like container.\n* use_buildkit:\nEnable buildkit for container image builds. Requires Docker with buildx plugin or Finch CLI.\n* build_in_source:\nOpts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']\n\nFor `sam build` on an npm workspaces monorepo, npm installs once at the workspace root instead of once per function, and each function's artifacts are narrowed to the dependencies it resolves. The install runs in the workspace root npm itself reports, which is inside your project but above the function's CodeUri.\n\nThis grouping is in beta: enable it with --beta-features or SAM_CLI_BETA_NODEJS_MONOREPO=1. Without it, every function installs for itself, as in earlier releases. It is also skipped for --use-container and --cached builds, so `sam sync` does not use it.\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* container_env_var:\nEnvironment variables to be passed into build containers\nResource format (FuncName.VarName=Value) or Global format (VarName=Value).\n\n Example: --container-env-var Func1.VAR1=value1 --container-env-var VAR2=value2\n* container_env_var_file:\nEnvironment variables json file (e.g., env_vars.json) to be passed to containers.\n* build_image:\nContainer image URIs for building functions/layers. You can specify for all functions/layers with just the image URI (--build-image public.ecr.aws/sam/build-nodejs18.x:latest). You can specify for each individual function with (--build-image FunctionLogicalID=public.ecr.aws/sam/build-nodejs18.x:latest). A combination of the two can be used. If a function does not have build image specified or an image URI for all functions, the default SAM CLI build images will be used.\n* exclude:\nName of the resource(s) to exclude from AWS SAM CLI build.\n* parallel:\nEnable parallel builds for AWS SAM template's functions and layers.\n* mount_with:\nSpecify mount mode for building functions/layers inside container. If it is mounted with write permissions, some files in source code directory may be changed/added by the build process. By default the source code directory is read only.\n* mount_symlinks:\nSpecify if symlinks at the top level of the code should be mounted inside the container. Activating this flag could allow access to locations outside of your workspace by using a symbolic link. By default symlinks are not mounted.\n* build_dir:\nDirectory to store build artifacts.Note: This directory will be first removed before starting a build.\n* cache_dir:\nDirectory to store cached artifacts. The default cache directory is .aws-sam/cache\n* base_dir:\nResolve relative paths to function's source code with respect to this directory. Use this if SAM template and source code are not in same enclosing folder. By default, relative paths are resolved with respect to the SAM template's location.\n* manifest:\nPath to a custom dependency manifest. Example: custom-package.json\n* cached:\nEnable cached builds.Reuse build artifacts that have not changed from previous builds. \n\nAWS SAM CLI evaluates if files in your project directory have changed. \n\nNote: AWS SAM CLI does not evaluate changes made to third party modules that the project depends on.Example: Python function includes a requirements.txt file with the following entry requests=1.x and the latest request module version changes from 1.1 to 1.2, AWS SAM CLI will not pull the latest version until a non-cached build is run.\n* template_file:\nAWS SAM template file.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* output:\nOutput the results from the command in a given output format. Supported formats: text (default), json.\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "terraform_project_root_path": { @@ -341,7 +341,7 @@ "build_in_source": { "title": "build_in_source", "type": "boolean", - "description": "Opts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']" + "description": "Opts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']\n\nFor `sam build` on an npm workspaces monorepo, npm installs once at the workspace root instead of once per function, and each function's artifacts are narrowed to the dependencies it resolves. The install runs in the workspace root npm itself reports, which is inside your project but above the function's CodeUri.\n\nThis grouping is in beta: enable it with --beta-features or SAM_CLI_BETA_NODEJS_MONOREPO=1. Without it, every function installs for itself, as in earlier releases. It is also skipped for --use-container and --cached builds, so `sam sync` does not use it." }, "language_extensions": { "title": "language_extensions", @@ -1838,7 +1838,7 @@ "properties": { "parameters": { "title": "Parameters for the sync command", - "description": "Available parameters for the sync command:\n* template_file:\nAWS SAM template file.\n* code:\nSync ONLY code resources. This includes Lambda Functions, API Gateway, and Step Functions.\n* watch:\nWatch local files and automatically sync with cloud.\n* resource_id:\nSync code for all the resources with the ID. To sync a resource within a nested stack, use the following pattern {ChildStack}/{logicalId}.\n* resource:\nSync code for all resources of the given resource type. Accepted values are ['AWS::Serverless::Function', 'AWS::Lambda::Function', 'AWS::Serverless::LayerVersion', 'AWS::Lambda::LayerVersion', 'AWS::Serverless::Api', 'AWS::ApiGateway::RestApi', 'AWS::Serverless::HttpApi', 'AWS::ApiGatewayV2::Api', 'AWS::Serverless::StateMachine', 'AWS::StepFunctions::StateMachine']\n* dependency_layer:\nSeparate dependencies of individual function into a Lambda layer for improved performance.\n* skip_deploy_sync:\nThis option will skip the initial infrastructure deployment if it is not required by comparing the local template with the template deployed in cloud.\n* express:\nUse CloudFormation Express mode to speed up infrastructure deployments by completing once resource configuration is applied, without waiting for full stabilization.\n* container_env_var_file:\nEnvironment variables json file (e.g., env_vars.json) to be passed to containers.\n* watch_exclude:\nExcludes a file or folder from being observed for file changes. Files and folders that are excluded will not trigger a sync workflow. This option can be provided multiple times.\n\nExamples:\n\nHelloWorldFunction=package-lock.json\n\nChildStackA/FunctionName=database.sqlite3\n* stack_name:\nName of the AWS CloudFormation stack.\n* base_dir:\nResolve relative paths to function's source code with respect to this directory. Use this if SAM template and source code are not in same enclosing folder. By default, relative paths are resolved with respect to the SAM template's location.\n* use_container:\nBuild functions within an AWS Lambda-like container.\n* build_in_source:\nOpts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']\n* build_image:\nContainer image URIs for building functions/layers. You can specify for all functions/layers with just the image URI (--build-image public.ecr.aws/sam/build-nodejs18.x:latest). You can specify for each individual function with (--build-image FunctionLogicalID=public.ecr.aws/sam/build-nodejs18.x:latest). A combination of the two can be used. If a function does not have build image specified or an image URI for all functions, the default SAM CLI build images will be used.\n* image_repository:\nAWS ECR repository URI where artifacts referenced in the template are uploaded.\n* image_repositories:\nMapping of Function Logical ID to AWS ECR Repository URI.\n\nExample: Function_Logical_ID=ECR_Repo_Uri\nThis option can be specified multiple times.\n* s3_bucket:\nAWS S3 bucket where artifacts referenced in the template are uploaded.\n* s3_prefix:\nPrefix name that is added to the artifact's name when it is uploaded to the AWS S3 bucket.\n* kms_key_id:\nThe ID of an AWS KMS key that is used to encrypt artifacts that are at rest in the AWS S3 bucket.\n* role_arn:\nARN of an IAM role that AWS Cloudformation assumes when executing a deployment change set.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* metadata:\nMap of metadata to attach to ALL the artifacts that are referenced in the template.\n* notification_arns:\nARNs of SNS topics that AWS Cloudformation associates with the stack.\n* tags:\nList of tags to associate with the stack.\n* capabilities:\nList of capabilities that one must specify before AWS Cloudformation can create certain stacks.\n\nAccepted Values: CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_RESOURCE_POLICY, CAPABILITY_AUTO_EXPAND.\n\nLearn more at: https://docs.aws.amazon.com/serverlessrepo/latest/devguide/acknowledging-application-capabilities.html\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the sync command:\n* template_file:\nAWS SAM template file.\n* code:\nSync ONLY code resources. This includes Lambda Functions, API Gateway, and Step Functions.\n* watch:\nWatch local files and automatically sync with cloud.\n* resource_id:\nSync code for all the resources with the ID. To sync a resource within a nested stack, use the following pattern {ChildStack}/{logicalId}.\n* resource:\nSync code for all resources of the given resource type. Accepted values are ['AWS::Serverless::Function', 'AWS::Lambda::Function', 'AWS::Serverless::LayerVersion', 'AWS::Lambda::LayerVersion', 'AWS::Serverless::Api', 'AWS::ApiGateway::RestApi', 'AWS::Serverless::HttpApi', 'AWS::ApiGatewayV2::Api', 'AWS::Serverless::StateMachine', 'AWS::StepFunctions::StateMachine']\n* dependency_layer:\nSeparate dependencies of individual function into a Lambda layer for improved performance.\n* skip_deploy_sync:\nThis option will skip the initial infrastructure deployment if it is not required by comparing the local template with the template deployed in cloud.\n* express:\nUse CloudFormation Express mode to speed up infrastructure deployments by completing once resource configuration is applied, without waiting for full stabilization.\n* container_env_var_file:\nEnvironment variables json file (e.g., env_vars.json) to be passed to containers.\n* watch_exclude:\nExcludes a file or folder from being observed for file changes. Files and folders that are excluded will not trigger a sync workflow. This option can be provided multiple times.\n\nExamples:\n\nHelloWorldFunction=package-lock.json\n\nChildStackA/FunctionName=database.sqlite3\n* stack_name:\nName of the AWS CloudFormation stack.\n* base_dir:\nResolve relative paths to function's source code with respect to this directory. Use this if SAM template and source code are not in same enclosing folder. By default, relative paths are resolved with respect to the SAM template's location.\n* use_container:\nBuild functions within an AWS Lambda-like container.\n* build_in_source:\nOpts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']\n\nFor `sam build` on an npm workspaces monorepo, npm installs once at the workspace root instead of once per function, and each function's artifacts are narrowed to the dependencies it resolves. The install runs in the workspace root npm itself reports, which is inside your project but above the function's CodeUri.\n\nThis grouping is in beta: enable it with --beta-features or SAM_CLI_BETA_NODEJS_MONOREPO=1. Without it, every function installs for itself, as in earlier releases. It is also skipped for --use-container and --cached builds, so `sam sync` does not use it.\n* build_image:\nContainer image URIs for building functions/layers. You can specify for all functions/layers with just the image URI (--build-image public.ecr.aws/sam/build-nodejs18.x:latest). You can specify for each individual function with (--build-image FunctionLogicalID=public.ecr.aws/sam/build-nodejs18.x:latest). A combination of the two can be used. If a function does not have build image specified or an image URI for all functions, the default SAM CLI build images will be used.\n* image_repository:\nAWS ECR repository URI where artifacts referenced in the template are uploaded.\n* image_repositories:\nMapping of Function Logical ID to AWS ECR Repository URI.\n\nExample: Function_Logical_ID=ECR_Repo_Uri\nThis option can be specified multiple times.\n* s3_bucket:\nAWS S3 bucket where artifacts referenced in the template are uploaded.\n* s3_prefix:\nPrefix name that is added to the artifact's name when it is uploaded to the AWS S3 bucket.\n* kms_key_id:\nThe ID of an AWS KMS key that is used to encrypt artifacts that are at rest in the AWS S3 bucket.\n* role_arn:\nARN of an IAM role that AWS Cloudformation assumes when executing a deployment change set.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* metadata:\nMap of metadata to attach to ALL the artifacts that are referenced in the template.\n* notification_arns:\nARNs of SNS topics that AWS Cloudformation associates with the stack.\n* tags:\nList of tags to associate with the stack.\n* capabilities:\nList of capabilities that one must specify before AWS Cloudformation can create certain stacks.\n\nAccepted Values: CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_RESOURCE_POLICY, CAPABILITY_AUTO_EXPAND.\n\nLearn more at: https://docs.aws.amazon.com/serverlessrepo/latest/devguide/acknowledging-application-capabilities.html\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "template_file": { @@ -1928,7 +1928,7 @@ "build_in_source": { "title": "build_in_source", "type": "boolean", - "description": "Opts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']" + "description": "Opts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']\n\nFor `sam build` on an npm workspaces monorepo, npm installs once at the workspace root instead of once per function, and each function's artifacts are narrowed to the dependencies it resolves. The install runs in the workspace root npm itself reports, which is inside your project but above the function's CodeUri.\n\nThis grouping is in beta: enable it with --beta-features or SAM_CLI_BETA_NODEJS_MONOREPO=1. Without it, every function installs for itself, as in earlier releases. It is also skipped for --use-container and --cached builds, so `sam sync` does not use it." }, "build_image": { "title": "build_image", diff --git a/tests/integration/buildcmd/test_build_in_source.py b/tests/integration/buildcmd/test_build_in_source.py index 8807e9feb28..ecce069efb2 100644 --- a/tests/integration/buildcmd/test_build_in_source.py +++ b/tests/integration/buildcmd/test_build_in_source.py @@ -1,4 +1,6 @@ +import json import os +import shutil from pathlib import Path import pytest import logging @@ -6,11 +8,15 @@ from samcli.lib.utils import osutils from tests.integration.buildcmd.build_integ_base import ( + BuildIntegBase, BuildIntegNodeBase, BuildIntegProvidedBase, BuildIntegEsbuildBase, ) -from tests.testing_utils import run_command +from tests.testing_utils import ( + IS_WINDOWS, + run_command, +) LOG = logging.getLogger(__name__) @@ -160,3 +166,289 @@ def test_builds_successfully_with_local_dependency(self): run_command(command_list, self.working_dir) self.validate_node_modules(True) + + +WORKSPACE_INSTALL_LOG_MESSAGE = "Installing dependencies once for workspace root" + + +class NpmCallLogMixin: + """ + Puts an npm shim first on PATH that appends every npm invocation to a log file before + delegating to the real npm, so tests can assert on how many installs a build ran. + """ + + def setup_npm_call_log(self): + real_npm = shutil.which("npm") + if not real_npm: + self.skipTest("npm is not available on PATH") + + self.npm_call_log = Path(self.scratch_dir, "npm-calls.log") + self._npm_shim_dir = Path(self.scratch_dir, "npm-shim") + self._npm_shim_dir.mkdir() + + if IS_WINDOWS: + shim = self._npm_shim_dir / "npm.cmd" + shim.write_text('@echo off\r\necho %* >> "%NPM_CALL_LOG%"\r\ncall "{}" %*\r\n'.format(real_npm)) + else: + shim = self._npm_shim_dir / "npm" + shim.write_text('#!/bin/sh\necho "$@" >> "$NPM_CALL_LOG"\nexec "{}" "$@"\n'.format(real_npm)) + shim.chmod(0o755) + + def npm_logging_env(self): + env = os.environ.copy() + env["PATH"] = str(self._npm_shim_dir) + os.pathsep + env["PATH"] + env["NPM_CALL_LOG"] = str(self.npm_call_log) + return env + + def npm_install_commands(self): + """Every logged npm invocation that resolves dependencies (install/update/ci).""" + if not self.npm_call_log.exists(): + return [] + lines = [line.strip() for line in self.npm_call_log.read_text().splitlines() if line.strip()] + return [line for line in lines if line.split()[0] in ("install", "update", "ci")] + + +class TestBuildCommand_BuildInSource_NodejsWorkspaces(NpmCallLogMixin, BuildIntegBase): + """ + An npm workspaces monorepo builds all member functions off ONE install at the workspace + root. The fixture has two functions with disjoint dependencies plus a shared workspace + package, and its lockfile pins versions below what the manifests' ranges allow (lodash + 4.17.20 for a ^4.17.20 range), so a lockfile-ignoring build is detectable. The shared + package pins lodash 4.17.15 exactly, giving a version conflict npm keeps as a nested copy. + """ + + template = None # the template lives inside the copied fixture + + def setUp(self): + super().setUp() + osutils.copytree(Path(self.test_data_path, "NodeWorkspaces"), self.working_dir) + self.template_path = str(Path(self.working_dir, "template.yaml")) + self.lockfile_path = Path(self.working_dir, "package-lock.json") + self.setup_npm_call_log() + + def artifact_modules(self, logical_id) -> Path: + return Path(self.default_build_dir, logical_id, "node_modules") + + def run_built_handler(self, logical_id) -> dict: + result = run_command( + ["node", "-e", 'require("./index.js").handler().then(r => console.log(r.body))'], + cwd=str(Path(self.default_build_dir, logical_id)), + ) + self.assertEqual(result.process.returncode, 0, result.stderr.decode("utf-8")) + body: dict = json.loads(result.stdout.decode("utf-8").strip()) + return body + + def build_workspace(self, parallel=False, beta_features=True): + # the grouping is behind SAM_CLI_BETA_NODEJS_MONOREPO, which --beta-features turns on + command_list = self.get_command_list( + build_in_source=True, parallel=parallel, debug=True, beta_features=beta_features + ) + result = run_command(command_list, cwd=self.working_dir, env=self.npm_logging_env()) + self.assertEqual(result.process.returncode, 0, result.stderr.decode("utf-8")) + return result + + def assert_artifacts_are_disjoint(self): + lodash_modules = self.artifact_modules("LodashFunction") + self.assertTrue((lodash_modules / "lodash").is_dir(), "LodashFunction is missing its own dependency") + self.assertTrue((lodash_modules / "@mono" / "shared").is_dir(), "LodashFunction is missing the shared package") + self.assertFalse((lodash_modules / "axios").exists(), "LodashFunction got its sibling's dependency") + + axios_modules = self.artifact_modules("AxiosFunction") + self.assertTrue((axios_modules / "axios").is_dir(), "AxiosFunction is missing its own dependency") + self.assertTrue((axios_modules / "@mono" / "shared").is_dir(), "AxiosFunction is missing the shared package") + self.assertFalse((axios_modules / "lodash").exists(), "AxiosFunction got its sibling's dependency") + + @pytest.mark.flaky(reruns=3) + def test_installs_once_with_disjoint_artifacts_and_untouched_lockfile(self): + lockfile_bytes_before = self.lockfile_path.read_bytes() + + result = self.build_workspace() + + self.assertIn(WORKSPACE_INSTALL_LOG_MESSAGE, result.stderr.decode("utf-8")) + + # one install for the whole workspace, not one per function + installs = self.npm_install_commands() + self.assertEqual(len(installs), 1, f"expected exactly one npm install, saw: {installs}") + # the root install must keep the root's own devDependencies (build tools like esbuild) + self.assertNotIn("--omit=dev", installs[0]) + + self.assert_artifacts_are_disjoint() + + # the lockfile was honoured: it pins 4.17.20 although the manifest range allows newer + lodash_manifest = self.artifact_modules("LodashFunction") / "lodash" / "package.json" + self.assertEqual(json.loads(lodash_manifest.read_text())["version"], "4.17.20") + + # and its bytes are untouched by the build + self.assertEqual(self.lockfile_path.read_bytes(), lockfile_bytes_before) + + @pytest.mark.flaky(reruns=3) + def test_handlers_resolve_from_artifact_dirs(self): + self.build_workspace() + + lodash_body = self.run_built_handler("LodashFunction") + self.assertEqual(lodash_body["ownLodashVersion"], "4.17.20") + # the shared package's conflicting lodash 4.17.15 stayed a nested copy: the function + # resolves its own version while the shared package resolves the pinned one + self.assertEqual(lodash_body["sharedLodashVersion"], "4.17.15") + + axios_body = self.run_built_handler("AxiosFunction") + self.assertEqual(axios_body["axiosVersion"], "1.6.0") + self.assertEqual(axios_body["sharedLodashVersion"], "4.17.15") + + @pytest.mark.flaky(reruns=3) + def test_parallel_build_installs_once_before_function_builds(self): + result = self.build_workspace(parallel=True) + + self.assertIn(WORKSPACE_INSTALL_LOG_MESSAGE, result.stderr.decode("utf-8")) + installs = self.npm_install_commands() + self.assertEqual(len(installs), 1, f"expected exactly one npm install, saw: {installs}") + + # a function build racing ahead of the install would link dependencies that do not + # exist yet, so complete artifacts prove the install was a barrier + self.assert_artifacts_are_disjoint() + + @pytest.mark.flaky(reruns=3) + def test_without_beta_features_the_monorepo_behaves_exactly_as_it_does_today(self): + # What the flag holds back, stated as behaviour rather than as a promise. Off, this fixture gets + # the shape every release so far has produced: one install per function, no grouping, and - the + # part worth being explicit about - a build that SUCCEEDS while emitting artifacts with no + # dependencies in them at all. That is aws/aws-lambda-builders#933, and the flag is what opts a + # user into its fix along with the grouping. Turning the flag on flips every assertion here, + # which the other tests in this class cover. + result = self.build_workspace(beta_features=False) + + self.assertNotIn(WORKSPACE_INSTALL_LOG_MESSAGE, result.stderr.decode("utf-8")) + installs = self.npm_install_commands() + self.assertEqual(len(installs), 2, f"expected one install per function, saw: {installs}") + + for logical_id in ("LodashFunction", "AxiosFunction"): + modules = self.artifact_modules(logical_id) + self.assertFalse( + modules.exists() and any(modules.iterdir()), + f"{logical_id} has dependencies without the flag; #933 would then already be fixed", + ) + + @pytest.mark.flaky(reruns=3) + def test_use_container_cannot_reach_the_workspace_grouping(self): + # workspace grouping only activates under --build-in-source, and the CLI refuses that + # flag together with --use-container, so a container build can never group: it keeps + # today's per-function installs by construction + command_list = self.get_command_list(build_in_source=True, use_container=True) + result = run_command(command_list, cwd=self.working_dir, env=self.npm_logging_env()) + + self.assertNotEqual(result.process.returncode, 0) + self.assertIn( + "must not provide both the --build-in-source and --use-container", + result.stderr.decode("utf-8"), + ) + self.assertNotIn(WORKSPACE_INSTALL_LOG_MESSAGE, result.stderr.decode("utf-8")) + self.assertEqual(self.npm_install_commands(), []) + + +class TestBuildCommand_BuildInSource_NodejsWorkspacesEsbuild(NpmCallLogMixin, BuildIntegBase): + """ + The esbuild workflow reaches the same grouping, because both nodejs workflows funnel their + installs through the same npm project tree. What differs is the artifact side: esbuild bundles + what each entry point imports instead of linking a node_modules, so these functions are built + with no dependency directory at all and the bundle has to have resolved through the tree npm + hoisted to the workspace root. + + This is the workflow aws/aws-sam-cli#6567 actually reports, and the fixture carries the reason + the shared install must not pass --omit=dev: `esbuild` is the ROOT's own devDependency, so an + install that pruned it would delete the bundler this build needs. + """ + + template = None # the template lives inside the copied fixture + + def setUp(self): + super().setUp() + osutils.copytree(Path(self.test_data_path, "NodeWorkspacesEsbuild"), self.working_dir) + self.template_path = str(Path(self.working_dir, "template.yaml")) + self.setup_npm_call_log() + + def artifact_dir(self, logical_id) -> Path: + return Path(self.default_build_dir, logical_id) + + def run_bundle(self, logical_id) -> dict: + result = run_command( + ["node", "-e", 'require("./index.js").handler().then(r => console.log(r.body))'], + cwd=str(self.artifact_dir(logical_id)), + ) + self.assertEqual(result.process.returncode, 0, result.stderr.decode("utf-8")) + body: dict = json.loads(result.stdout.decode("utf-8").strip()) + return body + + @pytest.mark.flaky(reruns=3) + def test_installs_once_then_bundles_each_function(self): + command_list = self.get_command_list(build_in_source=True, debug=True, beta_features=True) + result = run_command(command_list, cwd=self.working_dir, env=self.npm_logging_env()) + self.assertEqual(result.process.returncode, 0, result.stderr.decode("utf-8")) + + self.assertIn(WORKSPACE_INSTALL_LOG_MESSAGE, result.stderr.decode("utf-8")) + + # one install for the whole workspace, not one per function + installs = self.npm_install_commands() + self.assertEqual(len(installs), 1, f"expected exactly one npm install, saw: {installs}") + # --omit=dev here would remove the root's own esbuild and the bundling below could not run + self.assertNotIn("--omit=dev", installs[0]) + self.assertTrue( + Path(self.working_dir, "node_modules", "esbuild").is_dir(), + "the shared install pruned the root's own esbuild devDependency", + ) + + for logical_id in ("LodashFunction", "AxiosFunction"): + artifacts = self.artifact_dir(logical_id) + self.assertTrue((artifacts / "index.js").is_file(), f"{logical_id} produced no bundle") + # esbuild inlines what it needs, so unlike the plain npm workflow there is nothing to link + self.assertFalse( + (artifacts / "node_modules").exists(), + f"{logical_id} got a node_modules; the bundle should carry its dependencies", + ) + + # each bundle resolved through the hoisted root tree, including the shared package's + # conflicting lodash, which npm keeps as a nested copy + lodash_body = self.run_bundle("LodashFunction") + self.assertEqual(lodash_body["ownLodashVersion"], "4.17.20") + self.assertEqual(lodash_body["sharedLodashVersion"], "4.17.15") + + axios_body = self.run_bundle("AxiosFunction") + self.assertEqual(axios_body["axiosVersion"], "1.6.0") + self.assertEqual(axios_body["sharedLodashVersion"], "4.17.15") + + +class TestBuildCommand_BuildInSource_NodejsStandaloneNotGrouped(NpmCallLogMixin, BuildIntegNodeBase): + """A standalone (non-workspace) nodejs project keeps its per-function install unchanged.""" + + template = "template.yaml" + + def setUp(self): + super().setUp() + osutils.copytree(Path(self.test_data_path, "Esbuild"), self.working_dir) + self.setup_npm_call_log() + + @pytest.mark.flaky(reruns=3) + def test_standalone_project_keeps_per_function_install(self): + codeuri = Path(self.working_dir, "Node") + + overrides = self.get_override( + runtime="nodejs24.x", code_uri=str(codeuri), architecture="x86_64", handler="main.lambdaHandler" + ) + # beta features ON deliberately: what keeps this project on a per-function install has to be + # its own npm root, not the flag being off + command_list = self.get_command_list( + build_in_source=True, parameter_overrides=overrides, debug=True, beta_features=True + ) + result = run_command(command_list, cwd=self.working_dir, env=self.npm_logging_env()) + self.assertEqual(result.process.returncode, 0, result.stderr.decode("utf-8")) + + # no workspace grouping for a project whose npm root is its own directory + self.assertNotIn(WORKSPACE_INSTALL_LOG_MESSAGE, result.stderr.decode("utf-8")) + + # the same single per-function install as before the change: this fixture has no + # lockfile, so building in source resolves dependencies with `npm update --omit=dev` + installs = self.npm_install_commands() + self.assertEqual(len(installs), 1, f"expected exactly one npm install, saw: {installs}") + self.assertEqual(installs[0].split()[0], "update") + self.assertIn("--omit=dev", installs[0]) + + self.assertTrue(Path(codeuri, "node_modules").is_dir()) diff --git a/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-axios/index.js b/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-axios/index.js new file mode 100644 index 00000000000..dbf66c4a032 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-axios/index.js @@ -0,0 +1,10 @@ +const axios = require("axios"); +const shared = require("@mono/shared"); + +exports.handler = async () => ({ + statusCode: 200, + body: JSON.stringify({ + axiosVersion: axios.VERSION, + sharedLodashVersion: shared.sharedLodashVersion(), + }), +}); diff --git a/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-axios/package.json b/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-axios/package.json new file mode 100644 index 00000000000..5221d0dff40 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-axios/package.json @@ -0,0 +1,9 @@ +{ + "name": "@mono/fn-axios", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "@mono/shared": "^1.0.0", + "axios": "^1.6.0" + } +} diff --git a/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-lodash/index.js b/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-lodash/index.js new file mode 100644 index 00000000000..f9797ed00b9 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-lodash/index.js @@ -0,0 +1,10 @@ +const _ = require("lodash"); +const shared = require("@mono/shared"); + +exports.handler = async () => ({ + statusCode: 200, + body: JSON.stringify({ + ownLodashVersion: _.VERSION, + sharedLodashVersion: shared.sharedLodashVersion(), + }), +}); diff --git a/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-lodash/package.json b/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-lodash/package.json new file mode 100644 index 00000000000..a905566cf5f --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspaces/functions/fn-lodash/package.json @@ -0,0 +1,9 @@ +{ + "name": "@mono/fn-lodash", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "@mono/shared": "^1.0.0", + "lodash": "^4.17.20" + } +} diff --git a/tests/integration/testdata/buildcmd/NodeWorkspaces/package-lock.json b/tests/integration/testdata/buildcmd/NodeWorkspaces/package-lock.json new file mode 100644 index 00000000000..e0bacf75628 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspaces/package-lock.json @@ -0,0 +1,342 @@ +{ + "name": "node-workspaces-fixture", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-workspaces-fixture", + "version": "1.0.0", + "workspaces": [ + "packages/*", + "functions/*" + ] + }, + "functions/fn-axios": { + "name": "@mono/fn-axios", + "version": "1.0.0", + "dependencies": { + "@mono/shared": "^1.0.0", + "axios": "^1.6.0" + } + }, + "functions/fn-lodash": { + "name": "@mono/fn-lodash", + "version": "1.0.0", + "dependencies": { + "@mono/shared": "^1.0.0", + "lodash": "^4.17.20" + } + }, + "node_modules/@mono/fn-axios": { + "resolved": "functions/fn-axios", + "link": true + }, + "node_modules/@mono/fn-lodash": { + "resolved": "functions/fn-lodash", + "link": true + }, + "node_modules/@mono/shared": { + "resolved": "packages/shared", + "link": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "packages/shared": { + "name": "@mono/shared", + "version": "1.0.0", + "dependencies": { + "lodash": "4.17.15" + } + }, + "packages/shared/node_modules/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "license": "MIT" + } + } +} diff --git a/tests/integration/testdata/buildcmd/NodeWorkspaces/package.json b/tests/integration/testdata/buildcmd/NodeWorkspaces/package.json new file mode 100644 index 00000000000..31a157cfad7 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspaces/package.json @@ -0,0 +1,9 @@ +{ + "name": "node-workspaces-fixture", + "version": "1.0.0", + "private": true, + "workspaces": [ + "packages/*", + "functions/*" + ] +} diff --git a/tests/integration/testdata/buildcmd/NodeWorkspaces/packages/shared/index.js b/tests/integration/testdata/buildcmd/NodeWorkspaces/packages/shared/index.js new file mode 100644 index 00000000000..1f1ff18c58d --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspaces/packages/shared/index.js @@ -0,0 +1,3 @@ +const _ = require("lodash"); + +exports.sharedLodashVersion = () => _.VERSION; diff --git a/tests/integration/testdata/buildcmd/NodeWorkspaces/packages/shared/package.json b/tests/integration/testdata/buildcmd/NodeWorkspaces/packages/shared/package.json new file mode 100644 index 00000000000..ac5e3199244 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspaces/packages/shared/package.json @@ -0,0 +1,8 @@ +{ + "name": "@mono/shared", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "lodash": "4.17.15" + } +} diff --git a/tests/integration/testdata/buildcmd/NodeWorkspaces/template.yaml b/tests/integration/testdata/buildcmd/NodeWorkspaces/template.yaml new file mode 100644 index 00000000000..993d913129f --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspaces/template.yaml @@ -0,0 +1,18 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Resources: + LodashFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: functions/fn-lodash + Handler: index.handler + Runtime: nodejs22.x + Timeout: 600 + AxiosFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: functions/fn-axios + Handler: index.handler + Runtime: nodejs22.x + Timeout: 600 diff --git a/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-axios/index.js b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-axios/index.js new file mode 100644 index 00000000000..dbf66c4a032 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-axios/index.js @@ -0,0 +1,10 @@ +const axios = require("axios"); +const shared = require("@mono/shared"); + +exports.handler = async () => ({ + statusCode: 200, + body: JSON.stringify({ + axiosVersion: axios.VERSION, + sharedLodashVersion: shared.sharedLodashVersion(), + }), +}); diff --git a/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-axios/package.json b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-axios/package.json new file mode 100644 index 00000000000..bc7b26a19e0 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-axios/package.json @@ -0,0 +1,9 @@ +{ + "name": "@mono/fn-axios", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "@mono/shared": "^1.0.0", + "axios": "1.6.0" + } +} diff --git a/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-lodash/index.js b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-lodash/index.js new file mode 100644 index 00000000000..f9797ed00b9 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-lodash/index.js @@ -0,0 +1,10 @@ +const _ = require("lodash"); +const shared = require("@mono/shared"); + +exports.handler = async () => ({ + statusCode: 200, + body: JSON.stringify({ + ownLodashVersion: _.VERSION, + sharedLodashVersion: shared.sharedLodashVersion(), + }), +}); diff --git a/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-lodash/package.json b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-lodash/package.json new file mode 100644 index 00000000000..f00c0da92e6 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/functions/fn-lodash/package.json @@ -0,0 +1,9 @@ +{ + "name": "@mono/fn-lodash", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "@mono/shared": "^1.0.0", + "lodash": "4.17.20" + } +} diff --git a/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/package-lock.json b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/package-lock.json new file mode 100644 index 00000000000..46bb0fe2410 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/package-lock.json @@ -0,0 +1,739 @@ +{ + "name": "node-workspaces-esbuild-fixture", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-workspaces-esbuild-fixture", + "version": "1.0.0", + "workspaces": [ + "packages/*", + "functions/*" + ], + "devDependencies": { + "esbuild": "^0.14.14" + } + }, + "functions/fn-axios": { + "name": "@mono/fn-axios", + "version": "1.0.0", + "dependencies": { + "@mono/shared": "^1.0.0", + "axios": "1.6.0" + } + }, + "functions/fn-lodash": { + "name": "@mono/fn-lodash", + "version": "1.0.0", + "dependencies": { + "@mono/shared": "^1.0.0", + "lodash": "4.17.20" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@mono/fn-axios": { + "resolved": "functions/fn-axios", + "link": true + }, + "node_modules/@mono/fn-lodash": { + "resolved": "functions/fn-lodash", + "link": true + }, + "node_modules/@mono/shared": { + "resolved": "packages/shared", + "link": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "packages/shared": { + "name": "@mono/shared", + "version": "1.0.0", + "dependencies": { + "lodash": "4.17.15" + } + }, + "packages/shared/node_modules/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "license": "MIT" + } + } +} diff --git a/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/package.json b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/package.json new file mode 100644 index 00000000000..08165ed04ef --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/package.json @@ -0,0 +1,12 @@ +{ + "name": "node-workspaces-esbuild-fixture", + "version": "1.0.0", + "private": true, + "workspaces": [ + "packages/*", + "functions/*" + ], + "devDependencies": { + "esbuild": "^0.14.14" + } +} diff --git a/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/packages/shared/index.js b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/packages/shared/index.js new file mode 100644 index 00000000000..1f1ff18c58d --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/packages/shared/index.js @@ -0,0 +1,3 @@ +const _ = require("lodash"); + +exports.sharedLodashVersion = () => _.VERSION; diff --git a/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/packages/shared/package.json b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/packages/shared/package.json new file mode 100644 index 00000000000..ac5e3199244 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/packages/shared/package.json @@ -0,0 +1,8 @@ +{ + "name": "@mono/shared", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "lodash": "4.17.15" + } +} diff --git a/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/template.yaml b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/template.yaml new file mode 100644 index 00000000000..9f6ecfafce1 --- /dev/null +++ b/tests/integration/testdata/buildcmd/NodeWorkspacesEsbuild/template.yaml @@ -0,0 +1,30 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Resources: + LodashFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: functions/fn-lodash + Handler: index.handler + Runtime: nodejs22.x + Timeout: 600 + Metadata: + BuildMethod: esbuild + BuildProperties: + EntryPoints: + - index.js + Target: es2020 + AxiosFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: functions/fn-axios + Handler: index.handler + Runtime: nodejs22.x + Timeout: 600 + Metadata: + BuildMethod: esbuild + BuildProperties: + EntryPoints: + - index.js + Target: es2020 diff --git a/tests/unit/commands/_utils/test_experimental.py b/tests/unit/commands/_utils/test_experimental.py index 2e202dd95b5..770a7e18277 100644 --- a/tests/unit/commands/_utils/test_experimental.py +++ b/tests/unit/commands/_utils/test_experimental.py @@ -57,16 +57,16 @@ def test_set_experimental(self): self.gc_mock.return_value.set_value.assert_called_once_with(config_entry, False, is_flag=True, flush=False) def test_get_all_experimental(self): - self.assertEqual(len(get_all_experimental()), 5) + self.assertEqual(len(get_all_experimental()), 6) def test_get_all_experimental_statues(self): - self.assertEqual(len(get_all_experimental_statues()), 5) + self.assertEqual(len(get_all_experimental_statues()), 6) def test_get_all_experimental_env_vars(self): - self.assertEqual(len(get_all_experimental_env_vars()), 5) + self.assertEqual(len(get_all_experimental_env_vars()), 6) def test_get_enabled_experimental_flags(self): - self.assertEqual(len(get_enabled_experimental_flags()), 5) + self.assertEqual(len(get_enabled_experimental_flags()), 6) @patch("samcli.commands._utils.experimental.set_experimental") @patch("samcli.commands._utils.experimental.get_all_experimental") diff --git a/tests/unit/commands/buildcmd/core/test_command.py b/tests/unit/commands/buildcmd/core/test_command.py index 6e6020ab4ef..1758aea5896 100644 --- a/tests/unit/commands/buildcmd/core/test_command.py +++ b/tests/unit/commands/buildcmd/core/test_command.py @@ -49,6 +49,9 @@ def test_get_options_build_command_text(self, mock_get_params): "Build sam project and invoke default function locally": [ ("$ sam build && sam local invoke\x1b[0m", ""), ], + "Build an npm workspaces monorepo, installing once at the workspace root (beta)": [ + ("$ sam build --build-in-source --beta-features\x1b[0m", ""), + ], "Build sam project and deploy": [("$ sam build && sam deploy\x1b[0m", "")], "Required Options": [("--template-file", "")], "Template Options": [("--parameter-overrides", "")], diff --git a/tests/unit/lib/build_module/test_app_builder.py b/tests/unit/lib/build_module/test_app_builder.py index 4bfe506f03e..89d484d0357 100644 --- a/tests/unit/lib/build_module/test_app_builder.py +++ b/tests/unit/lib/build_module/test_app_builder.py @@ -12,6 +12,7 @@ import docker from parameterized import parameterized +from samcli.commands._utils.experimental import ExperimentalFlag from samcli.commands.local.cli_common.user_exceptions import InvalidFunctionPropertyType from samcli.lib.build.app_builder import ( ApplicationBuilder, @@ -454,6 +455,217 @@ def test_parallel_and_cached_run_should_pick_parallel_with_incremental( mock_parallel_build_strategy.build.assert_called_once() self.assertEqual(result, mock_parallel_build_strategy.build()) + @patch("samcli.lib.build.app_builder.get_validated_container_client") + @patch("samcli.lib.build.app_builder.DefaultBuildStrategy") + @patch("samcli.lib.build.app_builder.WorkspaceAwareBuildStrategy") + @patch("samcli.lib.build.app_builder.is_experimental_enabled", return_value=True) + def test_build_in_source_run_should_wrap_with_workspace_aware_strategy( + self, + _flag_enabled, + mock_workspace_aware_build_strategy_class, + mock_default_build_strategy_class, + mock_get_validated_client, + ): + mock_default_build_strategy = Mock() + mock_default_build_strategy_class.return_value = mock_default_build_strategy + mock_workspace_aware_build_strategy = Mock() + mock_workspace_aware_build_strategy_class.return_value = mock_workspace_aware_build_strategy + + mock_get_validated_client.return_value = Mock() + + builder = ApplicationBuilder( + MagicMock(), + "builddir", + "basedir", + "cachedir", + build_in_source=True, + stream_writer=StreamWriter(sys.stderr), + ) + builder._get_build_graph = Mock(return_value=Mock()) + + result = builder.build().artifacts + + # the wrapper takes the strategy it wraps, plus the two callbacks it delegates the npm work to + mock_workspace_aware_build_strategy_class.assert_called_once_with( + ANY, + mock_default_build_strategy, + "basedir", + builder._resolve_npm_project_root, + builder._prepare_workspace_shared_project, + ) + mock_workspace_aware_build_strategy.build.assert_called_once() + self.assertEqual(result, mock_workspace_aware_build_strategy.build()) + # the wrapped strategy is only reached through the wrapper + mock_default_build_strategy.build.assert_not_called() + + @patch("samcli.lib.build.app_builder.get_validated_container_client") + @patch("samcli.lib.build.app_builder.ParallelBuildStrategy") + @patch("samcli.lib.build.app_builder.WorkspaceAwareBuildStrategy") + @patch("samcli.lib.build.app_builder.is_experimental_enabled", return_value=True) + def test_build_in_source_with_parallel_should_wrap_outside_the_parallel_strategy( + self, + _flag_enabled, + mock_workspace_aware_build_strategy_class, + mock_parallel_build_strategy_class, + mock_get_validated_client, + ): + # the shared install has to finish before any grouped function build starts, so the wrapper + # must sit OUTSIDE ParallelBuildStrategy rather than inside it + mock_parallel_build_strategy = Mock() + mock_parallel_build_strategy_class.return_value = mock_parallel_build_strategy + mock_workspace_aware_build_strategy = Mock() + mock_workspace_aware_build_strategy_class.return_value = mock_workspace_aware_build_strategy + + mock_get_validated_client.return_value = Mock() + + builder = ApplicationBuilder( + MagicMock(), + "builddir", + "basedir", + "cachedir", + parallel=True, + build_in_source=True, + stream_writer=StreamWriter(sys.stderr), + ) + builder._get_build_graph = Mock(return_value=Mock()) + + result = builder.build().artifacts + + self.assertEqual( + mock_workspace_aware_build_strategy_class.call_args[0][1], + mock_parallel_build_strategy, + ) + self.assertEqual(result, mock_workspace_aware_build_strategy.build()) + + @patch("samcli.lib.build.app_builder.get_validated_container_client") + @patch("samcli.lib.build.app_builder.CachedOrIncrementalBuildStrategyWrapper") + @patch("samcli.lib.build.app_builder.WorkspaceAwareBuildStrategy") + @patch("samcli.lib.build.app_builder.is_experimental_enabled", return_value=True) + def test_cached_run_should_not_wrap_with_workspace_aware_strategy( + self, + _flag_enabled, + mock_workspace_aware_build_strategy_class, + mock_cached_and_incremental_build_strategy_class, + mock_get_validated_client, + ): + # --cached supplies dependencies from .aws-sam/deps and manages download_dependencies + # itself, so the shared install must not take that over + mock_cached_and_incremental_build_strategy = Mock() + mock_cached_and_incremental_build_strategy_class.return_value = mock_cached_and_incremental_build_strategy + + mock_get_validated_client.return_value = Mock() + + builder = ApplicationBuilder( + MagicMock(), + "builddir", + "basedir", + "cachedir", + cached=True, + build_in_source=True, + stream_writer=StreamWriter(sys.stderr), + ) + builder._get_build_graph = Mock(return_value=Mock()) + + result = builder.build().artifacts + + mock_workspace_aware_build_strategy_class.assert_not_called() + self.assertEqual(result, mock_cached_and_incremental_build_strategy.build()) + + @patch("samcli.lib.build.app_builder.get_validated_container_client") + @patch("samcli.lib.build.app_builder.DefaultBuildStrategy") + @patch("samcli.lib.build.app_builder.WorkspaceAwareBuildStrategy") + @patch("samcli.lib.build.app_builder.is_experimental_enabled", return_value=True) + def test_container_run_should_not_wrap_with_workspace_aware_strategy( + self, + _flag_enabled, + mock_workspace_aware_build_strategy_class, + mock_default_build_strategy_class, + mock_get_validated_client, + ): + # the install would run on the host while the build runs in the container + mock_default_build_strategy = Mock() + mock_default_build_strategy_class.return_value = mock_default_build_strategy + + mock_get_validated_client.return_value = Mock() + + builder = ApplicationBuilder( + MagicMock(), + "builddir", + "basedir", + "cachedir", + container_manager=Mock(), + build_in_source=True, + stream_writer=StreamWriter(sys.stderr), + ) + builder._get_build_graph = Mock(return_value=Mock()) + + result = builder.build().artifacts + + mock_workspace_aware_build_strategy_class.assert_not_called() + self.assertEqual(result, mock_default_build_strategy.build()) + + @patch("samcli.lib.build.app_builder.get_validated_container_client") + @patch("samcli.lib.build.app_builder.DefaultBuildStrategy") + @patch("samcli.lib.build.app_builder.WorkspaceAwareBuildStrategy") + @patch("samcli.lib.build.app_builder.is_experimental_enabled", return_value=True) + def test_default_run_should_not_wrap_with_workspace_aware_strategy( + self, + _flag_enabled, + mock_workspace_aware_build_strategy_class, + mock_default_build_strategy_class, + mock_get_validated_client, + ): + # without --build-in-source the dependencies are installed into the build directory, where + # there is no shared tree to hoist into + mock_default_build_strategy = Mock() + mock_default_build_strategy_class.return_value = mock_default_build_strategy + + mock_get_validated_client.return_value = Mock() + + builder = ApplicationBuilder( + MagicMock(), "builddir", "basedir", "cachedir", stream_writer=StreamWriter(sys.stderr) + ) + builder._get_build_graph = Mock(return_value=Mock()) + + result = builder.build().artifacts + + mock_workspace_aware_build_strategy_class.assert_not_called() + self.assertEqual(result, mock_default_build_strategy.build()) + + @patch("samcli.lib.build.app_builder.get_validated_container_client") + @patch("samcli.lib.build.app_builder.DefaultBuildStrategy") + @patch("samcli.lib.build.app_builder.WorkspaceAwareBuildStrategy") + @patch("samcli.lib.build.app_builder.is_experimental_enabled", return_value=False) + def test_workspace_grouping_is_off_without_the_experimental_flag( + self, + mock_is_experimental_enabled, + mock_workspace_aware_build_strategy_class, + mock_default_build_strategy_class, + mock_get_validated_client, + ): + # everything else about this build asks for the grouping; only the flag is missing, and that + # alone keeps every function on its own install - the behaviour of every release so far + mock_default_build_strategy = Mock() + mock_default_build_strategy_class.return_value = mock_default_build_strategy + + mock_get_validated_client.return_value = Mock() + + builder = ApplicationBuilder( + MagicMock(), + "builddir", + "basedir", + "cachedir", + build_in_source=True, + stream_writer=StreamWriter(sys.stderr), + ) + builder._get_build_graph = Mock(return_value=Mock()) + + result = builder.build().artifacts + + mock_is_experimental_enabled.assert_called_once_with(ExperimentalFlag.NodejsMonorepo) + mock_workspace_aware_build_strategy_class.assert_not_called() + self.assertEqual(result, mock_default_build_strategy.build()) + @patch("samcli.lib.build.build_graph.BuildGraph._write") @patch("samcli.lib.build.app_builder.get_validated_container_client") @patch("samcli.lib.build.build_graph.BuildGraph._read") @@ -596,6 +808,68 @@ def test_must_not_use_dep_layer_for_non_cached(self, mock_get_validated_client): ) +class TestApplicationBuilder_prepare_workspace_shared_project(TestCase): + """ + What this hands to aws-lambda-builders, which nothing else pins. The builders hook refuses + without experimentalNodejsMonorepo, and a caller that forgets the flag gets a caught + LambdaBuilderError and a silent fall back to per-function installs - correct artifacts by the + slow path, so no test downstream of here can see the mistake. + """ + + @patch("samcli.lib.build.app_builder.get_validated_container_client") + def setUp(self, _mock_client): + self.builder = ApplicationBuilder( + MagicMock(), "builddir", "basedir", "cachedir", stream_writer=StreamWriter(sys.stderr) + ) + self.definition = Mock(codeuri="functions/orders", runtime="nodejs22.x", metadata={}) + + @patch("samcli.lib.build.app_builder.get_enabled_experimental_flags", return_value=["experimentalNodejsMonorepo"]) + @patch("samcli.lib.build.app_builder.LambdaBuilder") + @patch("samcli.lib.build.app_builder.get_workflow_config") + def test_passes_the_enabled_experimental_flags_to_the_builders_hook(self, _config, mock_builder_class, _flags): + builder_instance = mock_builder_class.return_value + + self.assertTrue(self.builder._prepare_workspace_shared_project("/base/mono", self.definition)) + + builder_instance.prepare_shared_project.assert_called_once_with( + "/base/mono", experimental_flags=["experimentalNodejsMonorepo"] + ) + + @patch("samcli.lib.build.app_builder.get_enabled_experimental_flags", return_value=[]) + @patch("samcli.lib.build.app_builder.LambdaBuilder") + @patch("samcli.lib.build.app_builder.get_workflow_config") + def test_forwards_an_empty_flag_list_rather_than_omitting_the_argument(self, _config, mock_builder_class, _flags): + # the builders hook decides what an absent flag means, so the argument must always reach it + builder_instance = mock_builder_class.return_value + + self.builder._prepare_workspace_shared_project("/base/mono", self.definition) + + builder_instance.prepare_shared_project.assert_called_once_with("/base/mono", experimental_flags=[]) + + @patch("samcli.lib.build.app_builder.get_enabled_experimental_flags", return_value=[]) + @patch("samcli.lib.build.app_builder.LambdaBuilder") + @patch("samcli.lib.build.app_builder.get_workflow_config") + def test_declines_an_aws_lambda_builders_without_the_hook(self, _config, mock_builder_class, _flags): + # the pinned aws-lambda-builders predates the hook, so grouping must not engage + builder_instance = mock_builder_class.return_value + del builder_instance.prepare_shared_project + + self.assertFalse(self.builder._prepare_workspace_shared_project("/base/mono", self.definition)) + + @patch("samcli.lib.build.app_builder.get_enabled_experimental_flags", return_value=[]) + @patch("samcli.lib.build.app_builder.LambdaBuilder") + @patch("samcli.lib.build.app_builder.get_workflow_config") + def test_a_refused_or_failed_preparation_leaves_per_function_installs(self, _config, mock_builder_class, _flags): + mock_builder_class.return_value.prepare_shared_project.side_effect = LambdaBuilderError() + + self.assertFalse(self.builder._prepare_workspace_shared_project("/base/mono", self.definition)) + + @patch("samcli.lib.build.app_builder.get_enabled_experimental_flags", return_value=[]) + @patch("samcli.lib.build.app_builder.get_workflow_config", side_effect=UnsupportedRuntimeException()) + def test_an_unsupported_runtime_leaves_per_function_installs(self, _config, _flags): + self.assertFalse(self.builder._prepare_workspace_shared_project("/base/mono", self.definition)) + + class PathValidator: def __init__(self, path): self._path = path diff --git a/tests/unit/lib/build_module/test_build_strategy.py b/tests/unit/lib/build_module/test_build_strategy.py index 8004923c324..3f99f97e0fb 100644 --- a/tests/unit/lib/build_module/test_build_strategy.py +++ b/tests/unit/lib/build_module/test_build_strategy.py @@ -17,6 +17,7 @@ CachedBuildStrategy, CachedOrIncrementalBuildStrategyWrapper, IncrementalBuildStrategy, + WorkspaceAwareBuildStrategy, clean_redundant_folders, ) from samcli.lib.utils import osutils @@ -879,3 +880,121 @@ def test_wrapper_with_or_without_container(self, mocked_read, mocked_write, runt else: patched_cached_build_strategy.build_single_function_definition.assert_called_with(build_definition) patched_incremental_build_strategy.assert_not_called() + + +@patch("samcli.lib.build.build_graph.BuildGraph._write") +@patch("samcli.lib.build.build_graph.BuildGraph._read") +class WorkspaceAwareBuildStrategyTest(TestCase): + BASE_DIR = "/base" + + def setUp(self): + self.delegate = MagicMock() + self.resolver = Mock(return_value=None) + self.installer = Mock(return_value=True) + + def _strategy(self, build_graph): + return WorkspaceAwareBuildStrategy(build_graph, self.delegate, self.BASE_DIR, self.resolver, self.installer) + + def _graph(self, *definitions): + build_graph = BuildGraph("build_dir") + for index, definition in enumerate(definitions): + function = Mock() + function.inlinecode = None + function.full_path = f"function{index}" + build_graph.put_function_build_definition(definition, function) + return build_graph + + @staticmethod + def _definition(runtime="nodejs22.x", codeuri="endpoints/fn", packagetype=ZIP, metadata=None): + return FunctionBuildDefinition(runtime, codeuri, None, packagetype, X86_64, metadata or {}, "handler") + + def _code_dir(self, codeuri): + return str(Path(self.BASE_DIR, codeuri).resolve()) + + def test_functions_sharing_a_workspace_root_install_once_and_skip_their_own_installs(self, _read, _write): + definition1 = self._definition(codeuri="endpoints/fn1") + definition2 = self._definition(codeuri="endpoints/fn2", metadata={"BuildMethod": "esbuild"}) + build_graph = self._graph(definition1, definition2) + self.resolver.return_value = "/base/mono" + + strategy = self._strategy(build_graph) + result = strategy._build_functions(build_graph) + + self.installer.assert_called_once_with("/base/mono", definition1) + self.assertFalse(definition1.download_dependencies) + self.assertFalse(definition2.download_dependencies) + self.delegate._build_functions.assert_called_once_with(build_graph) + self.assertEqual(result, self.delegate._build_functions.return_value) + + def test_a_failed_shared_preparation_leaves_per_function_installs(self, _read, _write): + definition = self._definition() + build_graph = self._graph(definition) + self.resolver.return_value = "/base/mono" + self.installer.return_value = False + + self._strategy(build_graph)._build_functions(build_graph) + + self.assertTrue(definition.download_dependencies) + self.delegate._build_functions.assert_called_once_with(build_graph) + + def test_a_standalone_package_is_not_grouped(self, _read, _write): + # npm answers with the directory itself for anything that is not a workspace member + definition = self._definition(codeuri="standalone") + build_graph = self._graph(definition) + self.resolver.return_value = self._code_dir("standalone") + + self._strategy(build_graph)._build_functions(build_graph) + + self.installer.assert_not_called() + self.assertTrue(definition.download_dependencies) + + def test_out_of_scope_functions_never_resolve_a_root(self, _read, _write): + out_of_scope = [ + ("python3.12", ZIP, None), + ("nodejs22.x", IMAGE, None), + ("nodejs22.x", ZIP, {"BuildMethod": "makefile"}), + ] + for runtime, packagetype, metadata in out_of_scope: + with self.subTest(runtime=runtime, packagetype=packagetype, metadata=metadata): + definition = self._definition(runtime=runtime, packagetype=packagetype, metadata=metadata) + build_graph = self._graph(definition) + + self._strategy(build_graph)._build_functions(build_graph) + + self.resolver.assert_not_called() + self.installer.assert_not_called() + + def test_a_build_method_naming_the_runtime_is_in_scope(self, _read, _write): + definition = self._definition(metadata={"BuildMethod": "nodejs22.x"}) + build_graph = self._graph(definition) + self.resolver.return_value = "/base/mono" + + self._strategy(build_graph)._build_functions(build_graph) + + self.installer.assert_called_once() + self.assertFalse(definition.download_dependencies) + + def test_distinct_workspace_roots_install_separately(self, _read, _write): + definition1 = self._definition(codeuri="monoA/endpoints/fn1") + definition2 = self._definition(codeuri="monoB/endpoints/fn2") + build_graph = self._graph(definition1, definition2) + self.resolver.side_effect = ["/base/monoA", "/base/monoB"] + + self._strategy(build_graph)._build_functions(build_graph) + + self.installer.assert_has_calls([call("/base/monoA", definition1), call("/base/monoB", definition2)]) + self.assertFalse(definition1.download_dependencies) + self.assertFalse(definition2.download_dependencies) + + def test_build_enters_the_delegate_context_and_delegates_everything(self, _read, _write): + definition = self._definition(runtime="python3.12") + build_graph = self._graph(definition) + self.delegate._build_layers.return_value = {} + self.delegate._build_functions.return_value = {"function0": "artifacts"} + + result = self._strategy(build_graph).build() + + self.delegate.__enter__.assert_called_once() + self.delegate.__exit__.assert_called_once() + self.delegate._build_layers.assert_called_once_with(build_graph) + self.assertEqual(result, {"function0": "artifacts"})