Skip to content

Commit

Permalink
Merge pull request #107640 from cockroachdb/blathers/backport-release…
Browse files Browse the repository at this point in the history
…-23.1-107493

release-23.1: ui,build: push cluster-ui assets into external folder during watch mode
  • Loading branch information
sjbarag authored Jul 27, 2023
2 parents f742245 + 2dbcac2 commit 2b689f8
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 8 deletions.
17 changes: 17 additions & 0 deletions pkg/cmd/dev/testdata/datadriven/ui
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ cp -r sandbox/pkg/ui/workspaces/cluster-ui/dist crdb-checkout/pkg/ui/workspaces/
pnpm --dir crdb-checkout/pkg/ui/workspaces/cluster-ui run build:watch
pnpm --dir crdb-checkout/pkg/ui/workspaces/db-console exec webpack-dev-server --config webpack.config.js --mode development --env.WEBPACK_SERVE --env.dist=ccl --env.target=http://localhost:8080 --port 12345

exec
dev ui watch --cluster-ui-dst /path/to/foo --cluster-ui-dst /path/to/bar
----
bazel info workspace --color=no
pnpm --dir crdb-checkout/pkg/ui install
bazel build //pkg/ui/workspaces/cluster-ui:cluster-ui-lib //pkg/ui/workspaces/db-console/ccl/src/js:crdb-protobuf-client-ccl-lib
bazel info bazel-bin --color=no
bazel info workspace --color=no
cp sandbox/pkg/ui/workspaces/db-console/src/js/protos.js crdb-checkout/pkg/ui/workspaces/db-console/src/js/protos.js
cp sandbox/pkg/ui/workspaces/db-console/ccl/src/js/protos.js crdb-checkout/pkg/ui/workspaces/db-console/ccl/src/js/protos.js
cp sandbox/pkg/ui/workspaces/db-console/src/js/protos.d.ts crdb-checkout/pkg/ui/workspaces/db-console/src/js/protos.d.ts
cp sandbox/pkg/ui/workspaces/db-console/ccl/src/js/protos.d.ts crdb-checkout/pkg/ui/workspaces/db-console/ccl/src/js/protos.d.ts
rm -rf crdb-checkout/pkg/ui/workspaces/cluster-ui/dist
cp -r sandbox/pkg/ui/workspaces/cluster-ui/dist crdb-checkout/pkg/ui/workspaces/cluster-ui/dist
pnpm --dir crdb-checkout/pkg/ui/workspaces/cluster-ui run build:watch --env.copy-to=/path/to/foo --env.copy-to=/path/to/bar
pnpm --dir crdb-checkout/pkg/ui/workspaces/db-console exec webpack-dev-server --config webpack.config.js --mode development --env.WEBPACK_SERVE --env.dist=ccl --env.target=http://localhost:8080 --port 3000

exec
dev ui lint
----
Expand Down
23 changes: 17 additions & 6 deletions pkg/cmd/dev/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ func makeUIWatchCmd(d *dev) *cobra.Command {
// secureFlag is the name of the boolean long (GNU-style) flag that makes
// webpack's dev server use HTTPS.
secureFlag = "secure"
// clusterUiDestinationsFlag is the name of the long (GNU-style) flag that
// tells webpack where to copy emitted files during watch mode.
clusterUiDestinationsFlag = "cluster-ui-dst"
)

watchCmd := &cobra.Command{
Expand Down Expand Up @@ -334,23 +337,26 @@ Replaces 'make ui-watch'.`,
return err
}
port := fmt.Sprint(portNumber)
dbTarget, err := cmd.Flags().GetString(dbTargetFlag)
if err != nil {
log.Fatalf("unexpected error: %v", err)
return err
}
dbTarget := mustGetFlagString(cmd, dbTargetFlag)
_, err = url.Parse(dbTarget)
if err != nil {
log.Fatalf("invalid format for --%s argument: %v", dbTargetFlag, err)
return err
}
secure := mustGetFlagBool(cmd, secureFlag)
clusterUiDestinations := mustGetFlagStringArray(cmd, clusterUiDestinationsFlag)

// Start the cluster-ui watch task
// Start the cluster-ui watch tasks
nbExec := d.exec.AsNonBlocking()
argv := []string{
"--dir", dirs.clusterUI, "run", "build:watch",
}

// Add additional webpack args to copy cluster-ui output to external directories.
for _, dst := range clusterUiDestinations {
argv = append(argv, "--env.copy-to="+dst)
}

err = nbExec.CommandContextInheritingStdStreams(ctx, "pnpm", argv...)
if err != nil {
log.Fatalf("Unable to watch cluster-ui for changes: %v", err)
Expand Down Expand Up @@ -401,6 +407,11 @@ Replaces 'make ui-watch'.`,
watchCmd.Flags().String(dbTargetFlag, "http://localhost:8080", "url to proxy DB requests to")
watchCmd.Flags().Bool(secureFlag, false, "serve via HTTPS")
watchCmd.Flags().Bool(ossFlag, false, "build only the open-source parts of the UI")
watchCmd.Flags().StringArray(
clusterUiDestinationsFlag,
[]string{},
"directory to copy emitted cluster-ui files to. Can be set multiple times.",
)

return watchCmd
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/ui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion pkg/ui/workspaces/cluster-ui/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ load("@npm//pkg/ui/workspaces/cluster-ui:webpack-cli/package_json.bzl", webpack_
npm_link_all_packages(name = "node_modules")

WEBPACK_SRCS = glob(
["src/**"],
[
"src/**",
"build/webpack/**",
],
exclude = [
"src/**/*.stories.tsx",
"src/**/*.spec.tsx",
Expand Down
199 changes: 199 additions & 0 deletions pkg/ui/workspaces/cluster-ui/build/webpack/copyEmittedFilesPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

const fs = require("fs");
const fsp = require("fs/promises");
const os = require("os");
const path = require("path");
const semver = require("semver");
const { validate } = require("schema-utils");

const PLUGIN_NAME = `CopyEmittedFilesPlugin`;

const SCHEMA = {
type: "object",
additionalProperties: false,
properties: {
destinations: {
description: "Destination directories to copy emitted files to.",
type: "array",
items: {
type: "string",
},
// Disallow duplicates.
uniqueItems: true,
},
},
};

/**
* A webpack plugin that copies emitted files to additional directories.
* Its main purpose is to allow a watch-mode compilation to continuously "push"
* output files into external directories. This avoids the "multiple/mismatched
* copies of dependency $foo" behavior that often comes with symlink-based
* "pull" solutions like 'pnpm link'.
*/
class CopyEmittedFilesPlugin {
constructor(options) {
validate(SCHEMA, options, {
name: PLUGIN_NAME,
baseDataPath: "options",
});
this.options = options;
}

/**
* The implementation of the plugin. Runs one time once webpack starts up,
* setting up event listeners for various webpack events.
* @see https://webpack.js.org/contribute/writing-a-plugin/#creating-a-plugin
*/
apply(compiler) {
// If no destinations have been configured or if this isn't a watch-mode
// build, this plugin should do nothing.
// It's therefore safe to keep an instance of this plugin in the 'Plugins'
// array, as it'll have no impact on unconfigured builds.
if (this.options.destinations.length === 0 || !compiler.options.watch) {
return;
}

const logger = compiler.getInfrastructureLogger(PLUGIN_NAME);

// Extract the major and minor versions of this cluster-ui build.
const pkgVersion = getPkgVersion(compiler.context);

// Sanitize provided paths to ensure they point to a reasonable version of
// cluster-ui.
const destinations = this.options.destinations.map((dstOpt) => {
const dst = detildeify(dstOpt);

// The user provided paths to a specific cluster-ui version.
if (dst.includes("@cockroachlabs/cluster-ui-")) {
// Remove a possibly-trailing '/' literal.
const dstClean = dst[dst.length - 1] === "/"
? dst.slice(0, dst.length - 1)
: dst;

return dstClean;
}

// If the user provided a path to a project, look for a top-level
// node_modules/ within that directory
const dirents = fs.readdirSync(dst, { encoding: "utf-8", withFileTypes: true });
for (const dirent of dirents) {
if (dirent.name === "node_modules" && dirent.isDirectory()) {
return path.join(
dst,
`./node_modules/@cockroachlabs/cluster-ui-${pkgVersion.major}-${pkgVersion.minor}`,
);
}
}

const hasPnpmLock = dirents.some((dirent) => dirent.name === "pnpm-lock.yaml");
if (hasPnpmLock) {
logger.error(`Directory ${dst} doesn't have a node_modules directory, but does have a pnpm-lock.yaml.`);
logger.error(`Do you need to run 'pnpm install' there?`);
throw "missing node_modules";
}

logger.error(`Directory ${dst} doesn't have a node_modules directory, and does not appear to be`);
logger.error(`a JS package.`);
throw "unknown destination";
});

logger.info("Emitted files will be copied to:");
for (const dst of destinations) {
logger.info(" " + tildeify(dst));
}

const relOutputPath = path.relative(compiler.context, compiler.options.output.path);
// Clear destination areas and recreate output directory structure in each
// to ensure destinations all match the local output tree.
// @see https://v4.webpack.js.org/api/compiler-hooks/#afterenvironment
compiler.hooks.afterEnvironment.tap(PLUGIN_NAME, () => {
logger.warn("Deleting destinations in preparation for copied files:");
for (const dst of destinations) {
const prettyDst = tildeify(dst);
const stat = fs.statSync(dst);

if (stat.isDirectory()) {
logger.warn(` rm -r ${prettyDst}`);
} else {
logger.warn(` rm ${prettyDst}`);
}
fs.rmSync(dst, { recursive: stat.isDirectory() });

logger.debug(`mkdir -p ${path.join(dst, relOutputPath)}`);
fs.mkdirSync(path.join(dst, relOutputPath), { recursive: true });

logger.debug(`cp package.json ${path.join(dst, "package.json")}`);
fs.copyFileSync(
path.join(compiler.context, "package.json"),
path.join(dst, "package.json"),
);
}
});

// Copy files to each destination as they're emitted.
// @see https://v4.webpack.js.org/api/compiler-hooks/#assetemitted
compiler.hooks.assetEmitted.tapPromise(PLUGIN_NAME, (file) => {
return Promise.all(
destinations.map((dstBase) => {
const prettyDst = tildeify(dstBase);
const src = path.join(relOutputPath, file);
const dst = path.join(dstBase, relOutputPath, file);
logger.info(`cp ${src} ${path.join(prettyDst, relOutputPath)}`);
return fsp.copyFile(src, dst);
}),
);
});
}
}

/**
* Extracts the major and minor version number from the package at pkgRoot.
* @param pkgRoot {string} - the absolute path to the directory that holds the
* package's package.json
* @returns {object} - an object containing the major (`.major`) and minor
* (`.minor`) versions of the package
*/
function getPkgVersion(pkgRoot) {
const pkgJsonStr = fs.readFileSync(
path.join(pkgRoot, "package.json"),
"utf-8",
);
const pkgJson = JSON.parse(pkgJsonStr);
const version = semver.parse(pkgJson.version);
return {
major: version.major,
minor: version.minor,
};
}

/**
* Replaces the user's home directory with '~' in the provided path. The
* opposite of `detildeify`.
* @param {string} path - the path to replace a home directory in
* @returns {string} `path` but with the user's home directory swapped for '~'
*/
function tildeify(path) {
return path.replace(os.homedir(), "~");
}

/**
* Replaces '~' with the user's home directory in the provided path. The
* opposite of `tildeify`.
* @param {string} path - the path to replace a '~' in
* @returns {string} `path` but with '~' swapped for the user's home directory.
*/
function detildeify(path) {
return path.replace("~", os.homedir());
}

module.exports.CopyEmittedFilesPlugin = CopyEmittedFilesPlugin;
2 changes: 2 additions & 0 deletions pkg/ui/workspaces/cluster-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@
"reselect": "^4.0.0",
"sass": "^1.34.0",
"sass-loader": "^10.2.0",
"schema-utils": "3.3.0",
"semver": "7.5.3",
"sinon": "^9.0.2",
"source-map": "0.6.1",
"source-map-loader": "^0.2.4",
Expand Down
18 changes: 17 additions & 1 deletion pkg/ui/workspaces/cluster-ui/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const MomentLocalesPlugin = require("moment-locales-webpack-plugin");
const MomentTimezoneDataPlugin = require("moment-timezone-data-webpack-plugin")
const { ESBuildMinifyPlugin } = require("esbuild-loader");

const { CopyEmittedFilesPlugin } = require("./build/webpack/copyEmittedFilesPlugin");

const currentYear = new Date().getFullYear();

// tslint:disable:object-literal-sort-keys
Expand Down Expand Up @@ -180,7 +182,21 @@ module.exports = (env, argv) => {
// We have to tell the plugin where to store the pruned file
// otherwise webpack can't find it.
cacheDir: path.resolve(__dirname, "timezones"),
})
}),

// When requested with --env.copy-to=foo, copy all emitted files to
// arbitrary destination(s). Note that multiple destinations are supported
// but providing --env.copy-to multiple times at the command-line.
// This plugin does nothing in one-shot (i.e. non-watch) builds, or when
// no destinations are provided.
new CopyEmittedFilesPlugin({
destinations: (function() {
const copyTo = env["copy-to"] || [];
return typeof copyTo === "string"
? [copyTo]
: copyTo;
})(),
}),
],

// When importing a module whose path matches one of the following, just
Expand Down

0 comments on commit 2b689f8

Please sign in to comment.