protected String buildAndCopyWebApp()

in installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java [1691:1822]


    protected String buildAndCopyWebApp() {
        Path webDir = workingDir.resolve(Path.of("client", "web"));
        if (!Files.isDirectory(webDir)) {
            outputMessage("Error, can't find client/web directory at " + webDir.toAbsolutePath().toString());
            System.exit(2);
        }

        /*
        REACT_APP_SIGNOUT_URI saas-boost::${ENVIRONMENT}-${AWS_REGION}:webUrl
        REACT_APP_CALLBACK_URI saas-boost::${ENVIRONMENT}-${AWS_REGION}:webUrl
        REACT_APP_COGNITO_USERPOOL saas-boost::${ENVIRONMENT}-${AWS_REGION}:userPoolId
        REACT_APP_CLIENT_ID saas-boost::${ENVIRONMENT}-${AWS_REGION}:userPoolClientId
        REACT_APP_COGNITO_USERPOOL_BASE_URI saas-boost::${ENVIRONMENT}-${AWS_REGION}:cognitoBaseUri
        REACT_APP_API_URI saas-boost::${ENVIRONMENT}-${AWS_REGION}:publicApiUrl
        WEB_BUCKET saas-boost::${ENVIRONMENT}-${AWS_REGION}:webBucket
        */
        String nextToken = null;
        Map<String, String> exportsMap = new HashMap<>();
        try {
            do {
                ListExportsResponse response = cfn.listExports(ListExportsRequest.builder()
                        .nextToken(nextToken)
                        .build());
                nextToken = response.nextToken();
                for (Export export : response.exports()) {
                    if (export.name().startsWith("saas-boost::" + envName)) {
                        exportsMap.put(export.name(), export.value());
                    }
                }
            } while (nextToken != null);
        } catch (SdkServiceException cfnError) {
            LOGGER.error("cloudformation:ListExports error", cfnError);
            LOGGER.error(getFullStackTrace(cfnError));
            throw cfnError;
        }

        final String prefix = "saas-boost::" + envName + "-" + AWS_REGION.id() + ":";

        // We need to know the CloudFront distribution URL to continue
        final String webUrl = exportsMap.get(prefix + "webUrl");
        if (isEmpty(webUrl)) {
            outputMessage("Unexpected errors, CloudFormation export " + prefix + "webUrl not found");
            LOGGER.info("Available exports part of stack output" + String.join(", ", exportsMap.keySet()));
            System.exit(2);
        }

        // We need to know the S3 bucket to host the web files in to continue
        final String webBucket = exportsMap.get(prefix + "webBucket");
        if (isEmpty(webBucket)) {
            outputMessage("Unexpected errors, CloudFormation export " + prefix + "webBucket not found");
            LOGGER.info("Available exports part of stack output" + String.join(", ", exportsMap.keySet()));
            System.exit(2);
        }

        // Execute yarn build to generate the React app
        outputMessage("Start build of AWS SaaS Boost React web application with yarn...");
        ProcessBuilder pb;
        Process process = null;
        try {
            if (isWindows()) {
                pb = new ProcessBuilder("cmd", "/c", "yarn", "build");
            } else {
                pb = new ProcessBuilder("yarn", "build");
            }

            Map<String, String> env = pb.environment();
            pb.directory(webDir.toFile());
            env.put("REACT_APP_SIGNOUT_URI", webUrl);
            env.put("REACT_APP_CALLBACK_URI", webUrl);
            env.put("REACT_APP_COGNITO_USERPOOL", exportsMap.get(prefix + "userPoolId"));
            env.put("REACT_APP_CLIENT_ID", exportsMap.get(prefix + "userPoolClientId"));
            env.put("REACT_APP_COGNITO_USERPOOL_BASE_URI", exportsMap.get(prefix + "cognitoBaseUri"));
            env.put("REACT_APP_API_URI", exportsMap.get(prefix + "publicApiUrl"));
            env.put("REACT_APP_AWS_ACCOUNT", accountId);
            env.put("REACT_APP_ENVIRONMENT", envName);
            env.put("REACT_APP_AWS_REGION", AWS_REGION.id());

            process = pb.start();
            printResults(process);
            process.waitFor();
            int exitValue = process.exitValue();
            if (exitValue != 0) {
                throw new RuntimeException("Error building web application. Verify version of Node is correct.");
            }
            outputMessage("Completed build of AWS SaaS Boost React web application.");
        } catch (IOException | InterruptedException e) {
            LOGGER.error(getFullStackTrace(e));
        } finally {
            if (process != null) {
                process.destroy();
            }
        }

        // Sync files to the web bucket
        outputMessage("Synchronizing AWS SaaS Boost web application files to s3 web bucket");
        cleanUpS3(webBucket, "");
        Map<String, String> metadata = Stream
                .of(new AbstractMap.SimpleEntry<>("Cache-Control", "no-store"))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        Path yarnBuildDir = webDir.resolve(Path.of("build"));
        List<Path> filesToUpload;
        try (Stream<Path> stream = Files.walk(yarnBuildDir)) {
            filesToUpload = stream.filter(Files::isRegularFile).collect(Collectors.toList());
            outputMessage("Uploading " + filesToUpload.size() + " files to S3");
            for (Path fileToUpload : filesToUpload) {
                // Remove the parent client/web/build path from the S3 key
                String key = fileToUpload.subpath(yarnBuildDir.getNameCount(), fileToUpload.getNameCount()).toString();
                try {
                    // TODO this really should be a delete and copy like aws s3 sync --delete
                    LOGGER.info("Uploading to S3 " + fileToUpload.toString() + " -> " + key);
                    s3.putObject(PutObjectRequest.builder()
                            .bucket(webBucket)
                            // java.nio.file.Path will use OS dependent file separators, so when we run the installer on
                            // Windows, the S3 key will have back slashes instead of forward slashes. The CloudFormation
                            // definitions of Lambda functions will always use forward slashes for the S3Key property.
                            .key(key.replace('\\', '/'))
                            .metadata(metadata)
                            .build(), RequestBody.fromFile(fileToUpload)
                    );
                } catch (SdkServiceException s3Error) {
                    LOGGER.error("s3:PutObject error", s3Error);
                    LOGGER.error(getFullStackTrace(s3Error));
                    throw s3Error;
                }
            }
        } catch (IOException ioe) {
            LOGGER.error("Error walking web app build directory", ioe);
            LOGGER.error(getFullStackTrace(ioe));
            throw new RuntimeException(ioe);
        }
        return webUrl;
    }