Merge pull request #587 from spotify/rugvip/cli-lib
packages/cli: rename helpers to lib and move build-cache and watch-deps to lib
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { run } from 'helpers/run';
|
||||
import { run } from 'lib/run';
|
||||
|
||||
export default async () => {
|
||||
const args = ['build'];
|
||||
|
||||
@@ -14,29 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { waitForExit } from 'helpers/run';
|
||||
import { watchDeps } from 'commands/watch-deps';
|
||||
import { createLogger } from 'commands/watch-deps/logger';
|
||||
import { run } from 'lib/run';
|
||||
import { createLogFunc } from 'lib/logging';
|
||||
import { watchDeps } from 'lib/watchDeps';
|
||||
|
||||
export default async () => {
|
||||
// Start dynamic watch and build of dependencies, then serve the app
|
||||
await watchDeps({ build: true });
|
||||
|
||||
const child = spawn('react-scripts', ['start'], {
|
||||
await run('react-scripts', ['start'], {
|
||||
env: {
|
||||
FORCE_COLOR: 'true',
|
||||
EXTEND_ESLINT: 'true',
|
||||
SKIP_PREFLIGHT_CHECK: 'true',
|
||||
...process.env,
|
||||
},
|
||||
stdio: ['inherit', 'pipe', 'inherit'],
|
||||
shell: true,
|
||||
// We need to avoid clearing the terminal, or the build feedback of dependencies will be lost
|
||||
stdoutLogFunc: createLogFunc(process.stdout),
|
||||
});
|
||||
|
||||
// We need to avoid clearing the terminal, or the build feedback of dependencies will be lost
|
||||
const log = createLogger();
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stdout!.on('data', log.out);
|
||||
await waitForExit(child);
|
||||
};
|
||||
|
||||
@@ -14,48 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { Command } from 'commander';
|
||||
import { run } from 'helpers/run';
|
||||
import { Cache } from './cache';
|
||||
import { parseOptions, Options } from './options';
|
||||
|
||||
function print(msg: string) {
|
||||
process.stdout.write(`[build-cache] ${msg}\n`);
|
||||
}
|
||||
|
||||
// Wrap a build function with a cache, which won't call the build function on cache hit
|
||||
export async function withCache(
|
||||
options: Options,
|
||||
buildFunc: () => Promise<void>,
|
||||
): Promise<void> {
|
||||
const key = await Cache.readInputKey(options.inputs);
|
||||
if (!key) {
|
||||
print('input directory is dirty, skipping cache');
|
||||
await fs.remove(options.output);
|
||||
await buildFunc();
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = await Cache.read(options);
|
||||
|
||||
const cacheResult = cache.query(key);
|
||||
if (cacheResult.hit) {
|
||||
if (cacheResult.copy) {
|
||||
print('external cache hit, copying archive to output folder');
|
||||
await cacheResult.copy(options.output);
|
||||
} else {
|
||||
print('cache hit, nothing to be done');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
print('cache miss, need to build');
|
||||
await fs.remove(options.output);
|
||||
await buildFunc();
|
||||
|
||||
await cacheResult.archive(options.output, options.maxCacheEntries);
|
||||
}
|
||||
import { run } from 'lib/run';
|
||||
import { withCache, parseOptions } from 'lib/buildCache';
|
||||
|
||||
/*
|
||||
* The build-cache command is used to make builds a no-op if there are no changes to the package.
|
||||
@@ -69,5 +30,3 @@ export default async (cmd: Command, args: string[]) => {
|
||||
await run(args[0], args.slice(1));
|
||||
});
|
||||
};
|
||||
|
||||
export * from './options';
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath, relative as relativePath } from 'path';
|
||||
import { getDefaultCacheOptions } from 'commands/build-cache/options';
|
||||
import { paths } from 'helpers/paths';
|
||||
import { paths } from 'lib/paths';
|
||||
import { getDefaultCacheOptions } from 'lib/buildCache';
|
||||
|
||||
export default async function clean() {
|
||||
const cacheOptions = getDefaultCacheOptions();
|
||||
|
||||
@@ -21,9 +21,9 @@ import inquirer, { Answers, Question } from 'inquirer';
|
||||
import { exec as execCb } from 'child_process';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import os from 'os';
|
||||
import { Task, templatingTask } from 'helpers/tasks';
|
||||
import { paths } from 'helpers/paths';
|
||||
import { version } from 'helpers/version';
|
||||
import { Task, templatingTask } from 'lib/tasks';
|
||||
import { paths } from 'lib/paths';
|
||||
import { version } from 'lib/version';
|
||||
const exec = promisify(execCb);
|
||||
|
||||
async function checkExists(rootDir: string, name: string) {
|
||||
|
||||
@@ -25,10 +25,10 @@ import {
|
||||
parseOwnerIds,
|
||||
addCodeownersEntry,
|
||||
getCodeownersFilePath,
|
||||
} from './lib/codeowners';
|
||||
import { paths } from 'helpers/paths';
|
||||
import { version } from 'helpers/version';
|
||||
import { Task, templatingTask } from 'helpers/tasks';
|
||||
} from 'lib/codeowners';
|
||||
import { paths } from 'lib/paths';
|
||||
import { version } from 'lib/version';
|
||||
import { Task, templatingTask } from 'lib/tasks';
|
||||
const exec = promisify(execCb);
|
||||
|
||||
async function checkExists(rootDir: string, id: string) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { run } from 'helpers/run';
|
||||
import { run } from 'lib/run';
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
const args = ['lint', '--max-warnings=0', '--format=codeframe'];
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
*/
|
||||
|
||||
import { rollup, watch, OutputOptions } from 'rollup';
|
||||
import conf from './rollup.config';
|
||||
import { Command } from 'commander';
|
||||
import { withCache, getDefaultCacheOptions } from 'commands/build-cache';
|
||||
import { paths } from 'helpers/paths';
|
||||
import chalk from 'chalk';
|
||||
import { withCache, getDefaultCacheOptions } from 'lib/buildCache';
|
||||
import { paths } from 'lib/paths';
|
||||
import conf from './rollup.config';
|
||||
|
||||
function logError(error: any) {
|
||||
console.log('');
|
||||
|
||||
@@ -22,7 +22,7 @@ import postcss from 'rollup-plugin-postcss';
|
||||
import imageFiles from 'rollup-plugin-image-files';
|
||||
import json from '@rollup/plugin-json';
|
||||
import { RollupWatchOptions } from 'rollup';
|
||||
import { paths } from 'helpers/paths';
|
||||
import { paths } from 'lib/paths';
|
||||
|
||||
export default {
|
||||
input: 'src/index.ts',
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { run } from 'helpers/run';
|
||||
import { run } from 'lib/run';
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
const args = ['test'];
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
import fse from 'fs-extra';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { paths } from '../../helpers/paths';
|
||||
import { paths } from 'lib/paths';
|
||||
import {
|
||||
addExportStatement,
|
||||
capitalize,
|
||||
createTemporaryPluginFolder,
|
||||
} from '../create-plugin/createPlugin';
|
||||
import { addCodeownersEntry } from '../create-plugin/lib/codeowners';
|
||||
import { addCodeownersEntry } from 'lib/codeowners';
|
||||
import {
|
||||
removeReferencesFromAppPackage,
|
||||
removeReferencesFromPluginsFile,
|
||||
|
||||
@@ -17,9 +17,9 @@ import fse from 'fs-extra';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import inquirer, { Answers, Question } from 'inquirer';
|
||||
import { getCodeownersFilePath } from '../create-plugin/lib/codeowners';
|
||||
import { paths } from 'helpers/paths';
|
||||
import { Task } from 'helpers/tasks';
|
||||
import { getCodeownersFilePath } from 'lib/codeowners';
|
||||
import { paths } from 'lib/paths';
|
||||
import { Task } from 'lib/tasks';
|
||||
// import os from 'os';
|
||||
|
||||
const BACKSTAGE = '@backstage';
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { paths } from 'helpers/paths';
|
||||
import { runCheck } from 'helpers/run';
|
||||
import { paths } from 'lib/paths';
|
||||
import { runCheck } from 'lib/run';
|
||||
|
||||
function includesAnyOf(hayStack: string[], ...needles: string[]) {
|
||||
for (const needle of needles) {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
import { createLogger } from './logger';
|
||||
|
||||
export function startChild(args: string[]) {
|
||||
const [command, ...commandArgs] = args;
|
||||
const child = spawn(command, commandArgs, {
|
||||
env: { FORCE_COLOR: 'true', ...process.env },
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
});
|
||||
|
||||
// We need to avoid clearing the terminal, or the build feedback of dependencies will be lost
|
||||
const log = createLogger();
|
||||
child.stdout!.on('data', (data: Buffer) => {
|
||||
log.out(data.toString('utf8'));
|
||||
});
|
||||
child.stderr!.on('data', data => {
|
||||
log.err(data.toString('utf8'));
|
||||
});
|
||||
return child;
|
||||
}
|
||||
@@ -14,71 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import { createLoggerFactory } from './logger';
|
||||
import { findAllDeps } from './packages';
|
||||
import { startWatcher, startPackageWatcher } from './watcher';
|
||||
import { startCompiler } from './compiler';
|
||||
import { startChild } from './child';
|
||||
import { waitForExit, run } from 'helpers/run';
|
||||
import { paths } from 'helpers/paths';
|
||||
import { Command } from 'commander';
|
||||
|
||||
const PACKAGE_BLACKLIST = [
|
||||
// We never want to watch for changes in the cli, but all packages will depend on it.
|
||||
'@backstage/cli',
|
||||
];
|
||||
|
||||
const WATCH_LOCATIONS = ['package.json', 'src', 'assets'];
|
||||
|
||||
export type Options = {
|
||||
build?: boolean;
|
||||
};
|
||||
|
||||
// Start watching for dependency changes.
|
||||
// The returned promise resolves when watchers have started for all current dependencies.
|
||||
export async function watchDeps(options: Options = {}) {
|
||||
const localPackagePath = paths.resolveTarget('package.json');
|
||||
|
||||
// Rotate through different prefix colors to make it easier to differenciate between different deps
|
||||
const logFactory = createLoggerFactory([
|
||||
chalk.yellow,
|
||||
chalk.blue,
|
||||
chalk.magenta,
|
||||
chalk.green,
|
||||
chalk.cyan,
|
||||
]);
|
||||
|
||||
// Find all direct and transitive local dependencies of the current package.
|
||||
const packageData = await fs.readFile(localPackagePath, 'utf8');
|
||||
const packageJson = JSON.parse(packageData);
|
||||
const packageName = packageJson.name;
|
||||
|
||||
const deps = await findAllDeps(packageName, PACKAGE_BLACKLIST);
|
||||
|
||||
if (options.build) {
|
||||
await run('lerna', [
|
||||
'run',
|
||||
'--scope',
|
||||
packageName,
|
||||
'--include-dependencies',
|
||||
'build',
|
||||
]);
|
||||
}
|
||||
|
||||
// We lazily watch all our deps, as in we don't start the actual watch compiler until a change is detected
|
||||
const watcher = await startWatcher(deps, WATCH_LOCATIONS, pkg => {
|
||||
startCompiler(pkg, logFactory(pkg.name)).promise.catch(error => {
|
||||
process.stderr.write(`${error}\n`);
|
||||
});
|
||||
});
|
||||
|
||||
await startPackageWatcher(localPackagePath, async () => {
|
||||
const newDeps = await findAllDeps(packageName, PACKAGE_BLACKLIST);
|
||||
await watcher.update(newDeps);
|
||||
});
|
||||
}
|
||||
import { run } from 'lib/run';
|
||||
import { createLogPipe } from 'lib/logging';
|
||||
import { watchDeps, Options } from 'lib/watchDeps';
|
||||
|
||||
/*
|
||||
* The watch-deps command is meant to improve iteration speed while working in a large monorepo
|
||||
@@ -99,6 +38,12 @@ export default async (cmd: Command, args: string[]) => {
|
||||
await watchDeps(options);
|
||||
|
||||
if (args?.length) {
|
||||
await waitForExit(startChild(args));
|
||||
// Use log pipe to avoid clearing the terminal
|
||||
const logPipe = createLogPipe();
|
||||
|
||||
await run(args[0], args.slice(1), {
|
||||
stdoutLogFunc: logPipe(process.stdout),
|
||||
stderrLogFunc: logPipe(process.stderr),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
import program from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { exitWithError } from 'helpers/errors';
|
||||
import { version } from 'helpers/version';
|
||||
import { exitWithError } from 'lib/errors';
|
||||
import { version } from 'lib/version';
|
||||
|
||||
const main = (argv: string[]) => {
|
||||
program.name('backstage-cli').version(version);
|
||||
|
||||
+3
-3
@@ -16,11 +16,11 @@
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath, relative as relativePath } from 'path';
|
||||
import { runPlain, runCheck } from 'helpers/run';
|
||||
import { runPlain, runCheck } from 'lib/run';
|
||||
import { Options } from './options';
|
||||
import { extractArchive, createArchive } from './archive';
|
||||
import { paths } from 'helpers/paths';
|
||||
import { version, isDev } from 'helpers/version';
|
||||
import { paths } from 'lib/paths';
|
||||
import { version, isDev } from 'lib/version';
|
||||
|
||||
const INFO_FILE = '.backstage-build-cache';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './withCache';
|
||||
export * from './options';
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { Command } from 'commander';
|
||||
import { paths } from 'helpers/paths';
|
||||
import { paths } from 'lib/paths';
|
||||
|
||||
const DEFAULT_CACHE_DIR = '<repoRoot>/node_modules/.cache/backstage-builds';
|
||||
const DEFAULT_MAX_ENTRIES = 10;
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { Cache } from './cache';
|
||||
import { Options } from './options';
|
||||
|
||||
function print(msg: string) {
|
||||
process.stdout.write(`[build-cache] ${msg}\n`);
|
||||
}
|
||||
|
||||
// Wrap a build function with a cache, which won't call the build function on cache hit
|
||||
export async function withCache(
|
||||
options: Options,
|
||||
buildFunc: () => Promise<void>,
|
||||
): Promise<void> {
|
||||
const key = await Cache.readInputKey(options.inputs);
|
||||
if (!key) {
|
||||
print('input directory is dirty, skipping cache');
|
||||
await fs.remove(options.output);
|
||||
await buildFunc();
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = await Cache.read(options);
|
||||
|
||||
const cacheResult = cache.query(key);
|
||||
if (cacheResult.hit) {
|
||||
if (cacheResult.copy) {
|
||||
print('external cache hit, copying archive to output folder');
|
||||
await cacheResult.copy(options.output);
|
||||
} else {
|
||||
print('cache hit, nothing to be done');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
print('cache miss, need to build');
|
||||
await fs.remove(options.output);
|
||||
await buildFunc();
|
||||
|
||||
await cacheResult.archive(options.output, options.maxCacheEntries);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './codeowners';
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type LogFunc = (data: Buffer | string) => void;
|
||||
export type LogPipe = (dst: NodeJS.WriteStream) => LogFunc;
|
||||
|
||||
export type LogOptions = {
|
||||
// If set, prefix each log message with this string
|
||||
prefix?: string;
|
||||
|
||||
// If true, clear terminal commands will be forwarded, otherwise they are removed
|
||||
forwardClearTerm?: boolean;
|
||||
};
|
||||
|
||||
// Creates a log pipe that binds to a destination stream and forwards logs with optional transforms.
|
||||
// Use returned logPipe e.g. as follows: child.stdout.on('data', logPipe(process.stdout))
|
||||
export function createLogPipe(options: LogOptions = {}): LogPipe {
|
||||
const { prefix = '', forwardClearTerm = false } = options;
|
||||
|
||||
return (dst: NodeJS.WriteStream) => (data: Buffer | string) => {
|
||||
let str = typeof data === 'string' ? data : data.toString('utf8');
|
||||
|
||||
if (!forwardClearTerm) {
|
||||
str = trimClearTerm(str);
|
||||
}
|
||||
if (prefix) {
|
||||
str = `${prefix}${str}`;
|
||||
}
|
||||
|
||||
dst.write(Buffer.from(str, 'utf8'));
|
||||
};
|
||||
}
|
||||
|
||||
// Wrapper around createLogPipe to avoid awkward immediate call of returned function
|
||||
export function createLogFunc(
|
||||
dst: NodeJS.WriteStream,
|
||||
options: LogOptions = {},
|
||||
): LogFunc {
|
||||
return createLogPipe(options)(dst);
|
||||
}
|
||||
|
||||
// Returns the string without terminal clear command if it was prefixed with one.
|
||||
export function trimClearTerm(msg: string): string {
|
||||
return msg.startsWith('\x1b\x63') ? msg.slice(2) : msg;
|
||||
}
|
||||
@@ -22,10 +22,15 @@ import {
|
||||
} from 'child_process';
|
||||
import { ExitCodeError } from './errors';
|
||||
import { promisify } from 'util';
|
||||
import { LogFunc } from './logging';
|
||||
const exec = promisify(execCb);
|
||||
|
||||
type SpawnOptionsPartialEnv = Omit<SpawnOptions, 'env'> & {
|
||||
env?: Partial<NodeJS.ProcessEnv>;
|
||||
// Pipe stdout to this log function
|
||||
stdoutLogFunc?: LogFunc;
|
||||
// Pipe stderr to this log function
|
||||
stderrLogFunc?: LogFunc;
|
||||
};
|
||||
|
||||
// Runs a child command, returning a promise that is only resolved if the child exits with code 0.
|
||||
@@ -34,19 +39,33 @@ export async function run(
|
||||
args: string[] = [],
|
||||
options: SpawnOptionsPartialEnv = {},
|
||||
) {
|
||||
const { stdoutLogFunc, stderrLogFunc } = options;
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
FORCE_COLOR: 'true',
|
||||
...(options.env ?? {}),
|
||||
};
|
||||
|
||||
const stdio = [
|
||||
'inherit',
|
||||
stdoutLogFunc ? 'pipe' : 'inherit',
|
||||
stderrLogFunc ? 'pipe' : 'inherit',
|
||||
] as ('inherit' | 'pipe')[];
|
||||
|
||||
const child = spawn(name, args, {
|
||||
stdio: 'inherit',
|
||||
stdio,
|
||||
shell: true,
|
||||
...options,
|
||||
env,
|
||||
});
|
||||
|
||||
if (stdoutLogFunc && child.stdout) {
|
||||
child.stdout.on('data', stdoutLogFunc);
|
||||
}
|
||||
if (stderrLogFunc && child.stderr) {
|
||||
child.stderr.on('data', stderrLogFunc);
|
||||
}
|
||||
|
||||
await waitForExit(child, name);
|
||||
}
|
||||
|
||||
+6
-9
@@ -15,11 +15,11 @@
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { Logger } from './logger';
|
||||
import { LogPipe } from 'lib/logging';
|
||||
import chalk from 'chalk';
|
||||
import { Package } from './packages';
|
||||
|
||||
export function startCompiler(pkg: Package, log: Logger) {
|
||||
export function startCompiler(pkg: Package, logPipe: LogPipe) {
|
||||
// First we figure out which yarn script is a available, falling back to "build --watch"
|
||||
const scriptName = ['build:watch', 'watch'].find(
|
||||
script => script in pkg.scripts,
|
||||
@@ -35,12 +35,9 @@ export function startCompiler(pkg: Package, log: Logger) {
|
||||
});
|
||||
|
||||
watch.stdin.end();
|
||||
watch.stdout!.on('data', (data: Buffer) => {
|
||||
log.out(data.toString('utf8'));
|
||||
});
|
||||
watch.stderr!.on('data', data => {
|
||||
log.err(data.toString('utf8'));
|
||||
});
|
||||
watch.stdout.on('data', logPipe(process.stdout));
|
||||
const logErr = logPipe(process.stderr);
|
||||
watch.stderr.on('data', logErr);
|
||||
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
watch.on('error', error => {
|
||||
@@ -50,7 +47,7 @@ export function startCompiler(pkg: Package, log: Logger) {
|
||||
watch.on('close', (code: number) => {
|
||||
if (code !== 0) {
|
||||
const msg = `Compiler exited with code ${code}`;
|
||||
log.err(chalk.red(msg));
|
||||
logErr(chalk.red(msg));
|
||||
reject(new Error(msg));
|
||||
} else {
|
||||
resolve();
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './watchDeps';
|
||||
+4
-25
@@ -14,33 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type Logger = {
|
||||
out(msg: string): void;
|
||||
err(msg: string): void;
|
||||
};
|
||||
import { createLogPipe } from 'lib/logging';
|
||||
|
||||
export type ColorFunc = (msg: string) => string;
|
||||
|
||||
// Logger utility that prefixes logs and removes terminal clear commands
|
||||
export function createLogger(prefix: string = ''): Logger {
|
||||
const write = (stream: NodeJS.WriteStream, msg: string) => {
|
||||
const noClearMsg = msg.startsWith('\x1b\x63') ? msg.slice(2) : msg;
|
||||
const prefixedMsg = noClearMsg.trimRight().replace(/^/gm, prefix);
|
||||
stream.write(`${prefixedMsg}\n`, 'utf8');
|
||||
};
|
||||
|
||||
return {
|
||||
out(msg: string) {
|
||||
write(process.stdout, msg);
|
||||
},
|
||||
err(msg: string) {
|
||||
write(process.stderr, msg);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// A factory for creating loggers that rotate between different coloring functions
|
||||
export function createLoggerFactory(colorFuncs: ColorFunc[]) {
|
||||
// A factory for creating log pipes that rotate between different coloring functions
|
||||
export function createLogPipeFactory(colorFuncs: ColorFunc[]) {
|
||||
let colorIndex = 0;
|
||||
|
||||
return (name: string) => {
|
||||
@@ -49,6 +28,6 @@ export function createLoggerFactory(colorFuncs: ColorFunc[]) {
|
||||
colorIndex = (colorIndex + 1) % colorFuncs.length;
|
||||
|
||||
const prefix = `${colorFunc(name)}: `;
|
||||
return createLogger(prefix);
|
||||
return createLogPipe({ prefix });
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { paths } from 'helpers/paths';
|
||||
import { paths } from 'lib/paths';
|
||||
|
||||
const LernaProject = require('@lerna/project');
|
||||
const PackageGraph = require('@lerna/package-graph');
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import { createLogPipeFactory } from './logger';
|
||||
import { findAllDeps } from './packages';
|
||||
import { startWatcher, startPackageWatcher } from './watcher';
|
||||
import { startCompiler } from './compiler';
|
||||
import { run } from 'lib/run';
|
||||
import { paths } from 'lib/paths';
|
||||
|
||||
const PACKAGE_BLACKLIST = [
|
||||
// We never want to watch for changes in the cli, but all packages will depend on it.
|
||||
'@backstage/cli',
|
||||
];
|
||||
|
||||
const WATCH_LOCATIONS = ['package.json', 'src', 'assets'];
|
||||
|
||||
export type Options = {
|
||||
build?: boolean;
|
||||
};
|
||||
|
||||
// Start watching for dependency changes.
|
||||
// The returned promise resolves when watchers have started for all current dependencies.
|
||||
export async function watchDeps(options: Options = {}) {
|
||||
const localPackagePath = paths.resolveTarget('package.json');
|
||||
|
||||
// Rotate through different prefix colors to make it easier to differenciate between different deps
|
||||
const createLogPipe = createLogPipeFactory([
|
||||
chalk.yellow,
|
||||
chalk.blue,
|
||||
chalk.magenta,
|
||||
chalk.green,
|
||||
chalk.cyan,
|
||||
]);
|
||||
|
||||
// Find all direct and transitive local dependencies of the current package.
|
||||
const packageData = await fs.readFile(localPackagePath, 'utf8');
|
||||
const packageJson = JSON.parse(packageData);
|
||||
const packageName = packageJson.name;
|
||||
|
||||
const deps = await findAllDeps(packageName, PACKAGE_BLACKLIST);
|
||||
|
||||
if (options.build) {
|
||||
await run('lerna', [
|
||||
'run',
|
||||
'--scope',
|
||||
packageName,
|
||||
'--include-dependencies',
|
||||
'build',
|
||||
]);
|
||||
}
|
||||
|
||||
// We lazily watch all our deps, as in we don't start the actual watch compiler until a change is detected
|
||||
const watcher = await startWatcher(deps, WATCH_LOCATIONS, pkg => {
|
||||
startCompiler(pkg, createLogPipe(pkg.name)).promise.catch(error => {
|
||||
process.stderr.write(`${error}\n`);
|
||||
});
|
||||
});
|
||||
|
||||
await startPackageWatcher(localPackagePath, async () => {
|
||||
const newDeps = await findAllDeps(packageName, PACKAGE_BLACKLIST);
|
||||
await watcher.update(newDeps);
|
||||
});
|
||||
}
|
||||
+3
-3
@@ -18,7 +18,7 @@ import { resolve as resolvePath } from 'path';
|
||||
import chalk from 'chalk';
|
||||
import chokidar from 'chokidar';
|
||||
import { Package } from './packages';
|
||||
import { createLogger } from './logger';
|
||||
import { createLogFunc } from 'lib/logging';
|
||||
|
||||
export type Watcher = {
|
||||
update(newPackages: Package[]): Promise<void>;
|
||||
@@ -36,7 +36,7 @@ export async function startWatcher(
|
||||
callback: (pkg: Package) => void,
|
||||
): Promise<Watcher> {
|
||||
const watchedPackageLocations = new Set<string>();
|
||||
const logger = createLogger();
|
||||
const log = createLogFunc(process.stdout);
|
||||
|
||||
const watchPackage = async (pkg: Package) => {
|
||||
let signalled = false;
|
||||
@@ -71,7 +71,7 @@ export async function startWatcher(
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.out(chalk.green(`Starting watch of new dependency ${pkg.name}`));
|
||||
log(chalk.green(`Starting watch of new dependency ${pkg.name}`));
|
||||
promises.push(watchPackage(pkg));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user