diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index c3fa6e4e23..f154c586fa 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -14,8 +14,6 @@ jobs: env: CI: true NODE_OPTIONS: --max-old-space-size=4096 - BACKSTAGE_CACHE_DIR: /.backstage-build-cache - BACKSTAGE_CACHE_MAX_ENTRIES: 2 steps: - uses: actions/checkout@v2 @@ -36,13 +34,6 @@ jobs: with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - - name: cache build cache - uses: actions/cache@v2 - with: - path: .backstage-build-cache - key: build-cache-${{ github.sha }} - restore-keys: | - build-cache- - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 52f2760469..3ef0c1fee3 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -15,8 +15,6 @@ jobs: env: CI: true NODE_OPTIONS: --max-old-space-size=4096 - BACKSTAGE_CACHE_DIR: /.backstage-build-cache - BACKSTAGE_CACHE_MAX_ENTRIES: 2 steps: - uses: actions/checkout@v2 @@ -35,13 +33,6 @@ jobs: with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - - name: cache build cache - uses: actions/cache@v2 - with: - path: .backstage-build-cache - key: build-cache-${{ github.sha }} - restore-keys: | - build-cache- - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: diff --git a/packages/cli/src/commands/build-cache/index.ts b/packages/cli/src/commands/build-cache/index.ts deleted file mode 100644 index 72199ec42e..0000000000 --- a/packages/cli/src/commands/build-cache/index.ts +++ /dev/null @@ -1,32 +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 { Command } from 'commander'; -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. - * It supports both local development where the output directory remains intact, as well as CI - * where the output directory is stored in a separate cache dir. - */ -export default async (cmd: Command, args: string[]) => { - const options = await parseOptions(cmd); - - await withCache(options, async () => { - await run(args[0], args.slice(1)); - }); -}; diff --git a/packages/cli/src/commands/clean/clean.ts b/packages/cli/src/commands/clean/clean.ts index 91531a53ed..2fa3e4a18f 100644 --- a/packages/cli/src/commands/clean/clean.ts +++ b/packages/cli/src/commands/clean/clean.ts @@ -15,20 +15,9 @@ */ import fs from 'fs-extra'; -import { resolve as resolvePath, relative as relativePath } from 'path'; import { paths } from '../../lib/paths'; -import { getDefaultCacheOptions } from '../../lib/buildCache'; export default async function clean() { - const cacheOptions = getDefaultCacheOptions(); - const packagePath = getPackagePath(cacheOptions.cacheDir); - await fs.remove(cacheOptions.output); - await fs.remove(packagePath); + await fs.remove(paths.resolveTarget('dist')); await fs.remove(paths.resolveTarget('coverage')); } - -function getPackagePath(cacheDir: string) { - const relativePackagePath = relativePath(paths.targetRoot, paths.targetDir); - const packagePath = resolvePath(cacheDir, relativePackagePath); - return packagePath; -} diff --git a/packages/cli/src/commands/watch-deps/index.ts b/packages/cli/src/commands/watch-deps/index.ts deleted file mode 100644 index 61f403883c..0000000000 --- a/packages/cli/src/commands/watch-deps/index.ts +++ /dev/null @@ -1,49 +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 { Command } from 'commander'; -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 - * with packages that are built independently, meaning packages depends on each other's build output. - * - * The command traverses all dependencies of the current package within the monorepo, and starts - * watching for updates in all those packages. If a change is detected, we stop listening for changes, - * and instead start up watch mode for that package. Starting watch mode means running the first - * available yarn script out of "build:watch", "watch", or "build" --watch. - */ -export default async (cmd: Command, args: string[]) => { - const options: Options = {}; - - if (cmd.build) { - options.build = true; - } - - await watchDeps(options); - - if (args?.length) { - // 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), - }); - } -}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a8b6250a20..e67a52c795 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -126,29 +126,6 @@ const main = (argv: string[]) => { .description('Restores the changes made by the prepack command') .action(lazyAction(() => import('./commands/pack'), 'post')); - program - .command('watch-deps') - .option('--build', 'Build all dependencies on startup') - .description('Watch all dependencies while running another command') - .action(lazyAction(() => import('./commands/watch-deps'), 'default')); - - program - .command('build-cache') - .description('Wrap build command with a cache') - .option( - '--input ', - 'List of input directories that invalidate the cache [.]', - (value, acc) => acc.concat(value), - [], - ) - .option('--output ', 'Output directory to cache', 'dist') - .option( - '--cache-dir ', - 'Cache dir', - '/node_modules/.cache/backstage-builds', - ) - .action(lazyAction(() => import('./commands/build-cache'), 'default')); - program .command('clean') .description('Delete cache directories') diff --git a/packages/cli/src/lib/buildCache/archive.ts b/packages/cli/src/lib/buildCache/archive.ts deleted file mode 100644 index b4a736240d..0000000000 --- a/packages/cli/src/lib/buildCache/archive.ts +++ /dev/null @@ -1,39 +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 fs from 'fs-extra'; -import tar from 'tar'; -import { dirname } from 'path'; - -// packages all files in inputDir into an archive at archivePath, deleting any existing archive -export async function createArchive( - archivePath: string, - inputDir: string, -): Promise { - await fs.remove(archivePath); - await fs.ensureDir(dirname(archivePath)); - await tar.create({ gzip: true, file: archivePath, cwd: inputDir }, ['.']); -} - -// extracts archive at archive path into outputDir, deleting any existing files at outputDir -export async function extractArchive( - archivePath: string, - outputDir: string, -): Promise { - await fs.remove(outputDir); - await fs.ensureDir(outputDir); - await tar.extract({ file: archivePath, cwd: outputDir }); -} diff --git a/packages/cli/src/lib/buildCache/cache.ts b/packages/cli/src/lib/buildCache/cache.ts deleted file mode 100644 index d49eedaba9..0000000000 --- a/packages/cli/src/lib/buildCache/cache.ts +++ /dev/null @@ -1,214 +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 fs from 'fs-extra'; -import { resolve as resolvePath, relative as relativePath } from 'path'; -import { runPlain, runCheck } from '../run'; -import { Options } from './options'; -import { extractArchive, createArchive } from './archive'; -import { paths } from '../paths'; -import { version, isDev } from '../version'; - -const INFO_FILE = '.backstage-build-cache'; - -// Result from a cache query -export type CacheQueryResult = { - // True if there was a cache hit - hit: boolean; - // If there is a cache hit and this method is defined, it needs to be called to restore the - // output contents before continuing. - copy?: (outputDir: string) => Promise; - // Call after a successful build to archive the output content. - // The content will be archived using the same key as was used to query the cache. - archive: (outputDir: string, maxEntries: number) => Promise; -}; - -// Key that determines whether cached output can be reused -export type CacheKey = string[]; - -type CacheEntry = { - // Key for the input of this cache entry - key: CacheKey; - // Path to the archive of this cache entry - path: string; -}; - -// Struct containing information about cache entries on the filesystem. -// Stored inside the INFO_FILE as JSON. -type CacheInfo = { - // Optional key entry for the contents of the current directory. Used to key the - // output present in the outputs folder, where the info file resides inside the output folder. - key?: CacheKey; - // Optional list of cache archives present in the same directory. Resides in the external - // cache location inside one info file for each package. - entries?: CacheEntry[]; -}; - -export class Cache { - // Read the current cache state form the filesystem. - static async read(options: Options) { - const relativePackagePath = relativePath(paths.targetRoot, paths.targetDir); - const location = resolvePath(options.cacheDir, relativePackagePath); - - const outputInfo = await readCacheInfo(options.output); - const localKey = outputInfo?.key; - - const { entries = [] } = (await readCacheInfo(location)) ?? {}; - return new Cache(location, entries, localKey); - } - - // Generates a key based on the contents of the input paths. - // Returns undefined if it's not possible to generate a stable key. - static async readInputKey( - inputPaths: string[], - ): Promise { - const allInputPaths = inputPaths.slice(); - - // If we're executing the cli inside the backstage repo, we add the cli src as cache key as well - if (isDev) { - allInputPaths.unshift(paths.ownDir); - } - - // Make sure we don't have any uncommitted changes to the input, in that case we skip caching. - const noChanges = await runCheck( - 'git', - 'diff', - '--quiet', - 'HEAD', - '--', - ...allInputPaths, - ); - if (!noChanges) { - return undefined; - } - - const trees = []; - for (const inputPath of allInputPaths) { - const output = await runPlain('git', 'ls-tree', 'HEAD', inputPath); - const [, , sha] = output.split(/\s+/, 3); - // If we can't get a tree sha it means we're outside of tracked files, so treat as dirty - if (!sha) { - return undefined; - } - trees.push(sha); - } - - if (isDev) { - return trees; - } - // If we're executing as a dependency, use the version as a key - return [version, ...trees]; - } - - constructor( - private readonly location: string, - private readonly entries: CacheEntry[] = [], - private readonly localKey?: CacheKey, - ) {} - - // Query for the presense of cached output for a given key - query(key: CacheKey): CacheQueryResult { - const { location } = this; - - const archive = async (outputDir: string, maxEntries: number) => { - await writeCacheInfo(outputDir, { key }); - - const timestamp = new Date().toISOString().replace(/-|:|\..*/g, ''); - const rand = Math.random().toString(36).slice(2, 6); - const archiveName = `cache-${timestamp}-${rand}.tgz`; - const archivePath = resolvePath(location, archiveName); - - // Read existing entries and prepend the new one - const { entries = [] } = (await readCacheInfo(location)) ?? {}; - - // Check if there's already aan entry for this key, in that case we just wanna bump it - const entryIndex = entries.findIndex((e) => compareKeys(e.key, key)); - if (entryIndex !== -1) { - const [existingEntry] = entries.splice(entryIndex, 1); - entries.unshift(existingEntry); - - await writeCacheInfo(location, { entries }); - return; - } - - // Create and add new archive to entries - await createArchive(archivePath, outputDir); - entries.unshift({ key, path: archiveName }); - - // Remove old cache entries - const removedEntries = entries.splice(maxEntries); - for (const entry of removedEntries) { - try { - await fs.remove(resolvePath(location, entry.path)); - } catch (error) { - process.stderr.write(`failed to remove old cache entry, ${error}\n`); - } - } - - await writeCacheInfo(location, { entries }); - }; - - if (compareKeys(this.localKey, key)) { - return { hit: true, archive }; - } - - const matchingEntry = this.entries.find((e) => compareKeys(e.key, key)); - if (!matchingEntry) { - return { hit: false, archive }; - } - - return { - hit: true, - archive, - copy: async (outputDir: string) => { - const archivePath = resolvePath(location, matchingEntry.path); - await extractArchive(archivePath, outputDir); - }, - }; - } -} - -// Compares to cache keys, returning true if they are both defined and equal -function compareKeys(a?: CacheKey, b?: CacheKey): boolean { - if (!a || !b) { - return false; - } - return a.join(',') === b.join(','); -} - -// Read and parse a cache info file in the given directory -async function readCacheInfo( - parentDir: string, -): Promise { - const infoFile = resolvePath(parentDir, INFO_FILE); - const exists = await fs.pathExists(infoFile); - if (!exists) { - return undefined; - } - - const infoData = await fs.readFile(infoFile); - const cacheInfo = JSON.parse(infoData.toString('utf8')) as CacheInfo; - return cacheInfo; -} - -// Write a cache info file to the given directory -async function writeCacheInfo( - parentDir: string, - cacheInfo: CacheInfo, -): Promise { - const infoData = Buffer.from(JSON.stringify(cacheInfo, null, 2), 'utf8'); - await fs.writeFile(resolvePath(parentDir, INFO_FILE), infoData); -} diff --git a/packages/cli/src/lib/buildCache/index.ts b/packages/cli/src/lib/buildCache/index.ts deleted file mode 100644 index 70bcbd36b8..0000000000 --- a/packages/cli/src/lib/buildCache/index.ts +++ /dev/null @@ -1,18 +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. - */ - -export * from './withCache'; -export * from './options'; diff --git a/packages/cli/src/lib/buildCache/options.ts b/packages/cli/src/lib/buildCache/options.ts deleted file mode 100644 index 9a6a379fce..0000000000 --- a/packages/cli/src/lib/buildCache/options.ts +++ /dev/null @@ -1,59 +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 { resolve as resolvePath } from 'path'; -import { Command } from 'commander'; -import { paths } from '../paths'; - -const DEFAULT_CACHE_DIR = '/node_modules/.cache/backstage-builds'; -const DEFAULT_MAX_ENTRIES = 10; - -export type Options = { - inputs: string[]; - output: string; - cacheDir: string; - maxCacheEntries: number; -}; - -function transformPath(path: string): string { - return resolvePath(path.replace(//g, paths.targetRoot)); -} - -export async function parseOptions(cmd: Command): Promise { - const inputs = cmd.input.map(transformPath) as string[]; - if (inputs.length === 0) { - inputs.push(transformPath('.')); - } - const output = transformPath(cmd.output); - const cacheDir = transformPath( - process.env.BACKSTAGE_CACHE_DIR || cmd.cacheDir, - ); - const maxCacheEntries = - Number(process.env.BACKSTAGE_CACHE_MAX_ENTRIES) || DEFAULT_MAX_ENTRIES; - return { inputs, output, cacheDir, maxCacheEntries }; -} - -export function getDefaultCacheOptions(): Options { - return { - inputs: [transformPath('.')], - output: transformPath('dist'), - cacheDir: transformPath( - process.env.BACKSTAGE_CACHE_DIR || DEFAULT_CACHE_DIR, - ), - maxCacheEntries: - Number(process.env.BACKSTAGE_CACHE_MAX_ENTRIES) || DEFAULT_MAX_ENTRIES, - }; -} diff --git a/packages/cli/src/lib/buildCache/withCache.ts b/packages/cli/src/lib/buildCache/withCache.ts deleted file mode 100644 index d21de94865..0000000000 --- a/packages/cli/src/lib/buildCache/withCache.ts +++ /dev/null @@ -1,56 +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 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, -): Promise { - 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); -} diff --git a/packages/cli/src/lib/watchDeps/compiler.ts b/packages/cli/src/lib/watchDeps/compiler.ts deleted file mode 100644 index 5ea8efc3e6..0000000000 --- a/packages/cli/src/lib/watchDeps/compiler.ts +++ /dev/null @@ -1,64 +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 { LogPipe } from '../logging'; -import chalk from 'chalk'; -import { Package } from './packages'; - -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, - ); - const args = scriptName ? [scriptName] : ['build', '--watch']; - - // Start the watch script inside the dependency - const watch = spawn('yarn', ['run', ...args], { - cwd: pkg.location, - env: { FORCE_COLOR: 'true', ...process.env }, - stdio: 'pipe', - shell: true, - }); - - watch.stdin.end(); - watch.stdout.on('data', logPipe(process.stdout)); - const logErr = logPipe(process.stderr); - watch.stderr.on('data', logErr); - - const promise = new Promise((resolve, reject) => { - watch.on('error', (error) => { - reject(error); - }); - - watch.on('close', (code: number) => { - if (code !== 0) { - const msg = `Compiler exited with code ${code}`; - logErr(chalk.red(msg)); - reject(new Error(msg)); - } else { - resolve(); - } - }); - }); - - return { - promise, - close() { - watch.kill('SIGINT'); - }, - }; -} diff --git a/packages/cli/src/lib/watchDeps/index.ts b/packages/cli/src/lib/watchDeps/index.ts deleted file mode 100644 index a6fc92ca91..0000000000 --- a/packages/cli/src/lib/watchDeps/index.ts +++ /dev/null @@ -1,17 +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. - */ - -export * from './watchDeps'; diff --git a/packages/cli/src/lib/watchDeps/logger.ts b/packages/cli/src/lib/watchDeps/logger.ts deleted file mode 100644 index 59eb400301..0000000000 --- a/packages/cli/src/lib/watchDeps/logger.ts +++ /dev/null @@ -1,33 +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 { createLogPipe } from '../logging'; - -export type ColorFunc = (msg: string) => string; - -// A factory for creating log pipes that rotate between different coloring functions -export function createLogPipeFactory(colorFuncs: ColorFunc[]) { - let colorIndex = 0; - - return (name: string) => { - const colorFunc = colorFuncs[colorIndex]; - - colorIndex = (colorIndex + 1) % colorFuncs.length; - - const prefix = `${colorFunc(name)}: `; - return createLogPipe({ prefix }); - }; -} diff --git a/packages/cli/src/lib/watchDeps/packages.ts b/packages/cli/src/lib/watchDeps/packages.ts deleted file mode 100644 index 7a45524b45..0000000000 --- a/packages/cli/src/lib/watchDeps/packages.ts +++ /dev/null @@ -1,62 +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 { paths } from '../paths'; - -const LernaProject = require('@lerna/project'); -const PackageGraph = require('@lerna/package-graph'); - -export type Package = { - name: string; - location: string; - scripts: { [name in string]: string }; -}; - -// Uses lerna to find all local deps of the root package, excluding itself or any package in the blacklist -export async function findAllDeps( - rootPackageName: string, - blacklist: string[], -): Promise { - const project = new LernaProject(paths.targetDir); - const packages = await project.getPackages(); - const graph = new PackageGraph(packages); - - const deps = new Map(); - const searchNames = [rootPackageName]; - - while (searchNames.length) { - const name = searchNames.pop()!; - - if (deps.has(name)) { - continue; - } - - const node = graph.get(name); - if (!node) { - throw new Error(`Package '${name}' not found`); - } - - searchNames.push(...node.localDependencies.keys()); - deps.set(name, node.pkg); - } - - deps.delete(rootPackageName); - for (const name of blacklist) { - deps.delete(name); - } - - return [...deps.values()]; -} diff --git a/packages/cli/src/lib/watchDeps/watchDeps.ts b/packages/cli/src/lib/watchDeps/watchDeps.ts deleted file mode 100644 index c986749d3b..0000000000 --- a/packages/cli/src/lib/watchDeps/watchDeps.ts +++ /dev/null @@ -1,79 +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 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 '../run'; -import { paths } from '../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); - }); -} diff --git a/packages/cli/src/lib/watchDeps/watcher.ts b/packages/cli/src/lib/watchDeps/watcher.ts deleted file mode 100644 index bcfe766b63..0000000000 --- a/packages/cli/src/lib/watchDeps/watcher.ts +++ /dev/null @@ -1,125 +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 { resolve as resolvePath } from 'path'; -import chalk from 'chalk'; -import chokidar from 'chokidar'; -import { Package } from './packages'; -import { createLogFunc } from '../logging'; - -export type Watcher = { - update(newPackages: Package[]): Promise; -}; - -/* - * Watch for changes inside a collection of packages. When a change is detected, stop - * watching and call the callback with the package the change occured in. - * - * The returned promise is resolved once all watchers are ready. - */ -export async function startWatcher( - packages: Package[], - paths: string[], - callback: (pkg: Package) => void, -): Promise { - const watchedPackageLocations = new Set(); - const log = createLogFunc(process.stdout); - - const watchPackage = async (pkg: Package) => { - let signalled = false; - watchedPackageLocations.add(pkg.location); - - const watchLocations = paths.map((path) => resolvePath(pkg.location, path)); - const watcher = chokidar - .watch(watchLocations, { - cwd: pkg.location, - ignoreInitial: true, - disableGlobbing: true, - }) - .on('all', () => { - if (!signalled) { - signalled = true; - callback(pkg); - } - watcher.close(); - }); - - return new Promise((resolve, reject) => { - watcher.on('ready', resolve); - watcher.on('error', reject); - }); - }; - - const update = async (newPackages: Package[]) => { - const promises = new Array>(); - - for (const pkg of newPackages) { - if (watchedPackageLocations.has(pkg.location)) { - continue; - } - - log(chalk.green(`Starting watch of new dependency ${pkg.name}`)); - promises.push(watchPackage(pkg)); - } - - await Promise.all(promises); - }; - - await Promise.all(packages.map(watchPackage)); - - return { update }; -} - -/** - * Watch a package.json for updates - */ -export function startPackageWatcher(packagePath: string, callback: () => void) { - let changed = false; - let working = false; - - const notifyDeps = async () => { - changed = true; - if (working) { - return; - } - working = true; - changed = false; - - try { - await callback(); - } finally { - working = false; - // Keep going if a change was emitted while working - if (changed) { - notifyDeps(); - } - } - }; - - const watcher = chokidar - .watch(packagePath, { - ignoreInitial: true, - disableGlobbing: true, - }) - .on('all', () => { - notifyDeps(); - }); - - return new Promise((resolve, reject) => { - watcher.on('ready', resolve); - watcher.on('error', reject); - }); -}