feat(js): support esbuild and swc bundlers with the new ts solution config setup (#28409)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #

---------

Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
This commit is contained in:
Leosvel Pérez Espinosa 2024-10-14 09:30:43 +02:00 committed by GitHub
parent 74bdc583b9
commit db47dc30a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 240 additions and 219 deletions

View File

@ -24,6 +24,8 @@
"description": "The bundler to use. Choosing 'none' means this library is not buildable.", "description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"type": "string", "type": "string",
"enum": ["swc", "tsc", "rollup", "vite", "esbuild", "none"], "enum": ["swc", "tsc", "rollup", "vite", "esbuild", "none"],
"default": "tsc",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important" "x-priority": "important"
}, },
"linter": { "linter": {

View File

@ -48,6 +48,7 @@ describe('EsBuild Plugin', () => {
private: true, private: true,
type: 'commonjs', type: 'commonjs',
main: './index.cjs', main: './index.cjs',
typings: './index.d.ts',
dependencies: {}, dependencies: {},
}); });

View File

@ -1,22 +1,31 @@
import { joinPathFragments, logger, type ExecutorContext } from '@nx/devkit';
import { readTsConfig } from '@nx/js';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import * as esbuild from 'esbuild';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { import * as pc from 'picocolors';
import type {
EsBuildExecutorOptions, EsBuildExecutorOptions,
NormalizedEsBuildExecutorOptions, NormalizedEsBuildExecutorOptions,
} from '../schema'; } from '../schema';
import { ExecutorContext, joinPathFragments, logger } from '@nx/devkit';
import * as pc from 'picocolors';
import * as esbuild from 'esbuild';
import { readTsConfig } from '@nx/js';
export function normalizeOptions( export function normalizeOptions(
options: EsBuildExecutorOptions, options: EsBuildExecutorOptions,
context: ExecutorContext context: ExecutorContext
): NormalizedEsBuildExecutorOptions { ): NormalizedEsBuildExecutorOptions {
const isTsSolutionSetup = isUsingTsSolutionSetup();
if (isTsSolutionSetup && options.generatePackageJson) {
throw new Error(
`Setting 'generatePackageJson: true' is not allowed with the current TypeScript setup. Please update the 'package.json' file at the project root as needed and don't set the 'generatePackageJson' option.`
);
}
const tsConfig = readTsConfig(options.tsConfig); const tsConfig = readTsConfig(options.tsConfig);
// If we're not generating package.json file, then copy it as-is as an asset. // If we're not generating package.json file, then copy it as-is as an asset when not using ts solution setup.
const assets = options.generatePackageJson const assets =
options.generatePackageJson || isTsSolutionSetup
? options.assets ? options.assets
: [ : [
...options.assets, ...options.assets,
@ -33,7 +42,7 @@ export function normalizeOptions(
'bundle:false' 'bundle:false'
)} and ${pc.bold( )} and ${pc.bold(
'thirdParty:true' 'thirdParty:true'
)}. Your package.json depedencies might not be generated correctly so we added an update ${pc.bold( )}. Your package.json dependencies might not be generated correctly so we added an update ${pc.bold(
'thirdParty:false' 'thirdParty:false'
)}` )}`
) )
@ -42,8 +51,6 @@ export function normalizeOptions(
const thirdParty = !options.bundle ? false : options.thirdParty; const thirdParty = !options.bundle ? false : options.thirdParty;
const { root: projectRoot } =
context.projectsConfigurations.projects[context.projectName];
const declarationRootDir = options.declarationRootDir const declarationRootDir = options.declarationRootDir
? path.join(context.root, options.declarationRootDir) ? path.join(context.root, options.declarationRootDir)
: undefined; : undefined;

View File

@ -1,9 +1,10 @@
import { ExecutorContext, readJsonFile } from '@nx/devkit'; import { ExecutorContext, output, readJsonFile } from '@nx/devkit';
import { assetGlobsToFiles, FileInputOutput } from '../../utils/assets/assets';
import { sync as globSync } from 'fast-glob'; import { sync as globSync } from 'fast-glob';
import { rmSync } from 'node:fs'; import { rmSync } from 'node:fs';
import { dirname, join, relative, resolve, normalize } from 'path'; import { dirname, join, normalize, relative, resolve } from 'path';
import { copyAssets } from '../../utils/assets'; import { copyAssets } from '../../utils/assets';
import { assetGlobsToFiles, FileInputOutput } from '../../utils/assets/assets';
import type { DependentBuildableProjectNode } from '../../utils/buildable-libs-utils';
import { checkDependencies } from '../../utils/check-dependencies'; import { checkDependencies } from '../../utils/check-dependencies';
import { import {
getHelperDependency, getHelperDependency,
@ -13,16 +14,20 @@ import {
handleInliningBuild, handleInliningBuild,
isInlineGraphEmpty, isInlineGraphEmpty,
postProcessInlinedDependencies, postProcessInlinedDependencies,
type InlineProjectGraph,
} from '../../utils/inline'; } from '../../utils/inline';
import { copyPackageJson } from '../../utils/package-json'; import {
copyPackageJson,
type CopyPackageJsonResult,
} from '../../utils/package-json';
import { import {
NormalizedSwcExecutorOptions, NormalizedSwcExecutorOptions,
SwcCliOptions,
SwcExecutorOptions, SwcExecutorOptions,
} from '../../utils/schema'; } from '../../utils/schema';
import { compileSwc, compileSwcWatch } from '../../utils/swc/compile-swc'; import { compileSwc, compileSwcWatch } from '../../utils/swc/compile-swc';
import { getSwcrcPath } from '../../utils/swc/get-swcrc-path'; import { getSwcrcPath } from '../../utils/swc/get-swcrc-path';
import { generateTmpSwcrc } from '../../utils/swc/inline'; import { generateTmpSwcrc } from '../../utils/swc/inline';
import { isUsingTsSolutionSetup } from '../../utils/typescript/ts-solution-setup';
function normalizeOptions( function normalizeOptions(
options: SwcExecutorOptions, options: SwcExecutorOptions,
@ -30,6 +35,25 @@ function normalizeOptions(
sourceRoot: string, sourceRoot: string,
projectRoot: string projectRoot: string
): NormalizedSwcExecutorOptions { ): NormalizedSwcExecutorOptions {
const isTsSolutionSetup = isUsingTsSolutionSetup();
if (isTsSolutionSetup) {
if (options.generateLockfile) {
throw new Error(
`Setting 'generateLockfile: true' is not supported with the current TypeScript setup. Unset the 'generateLockfile' option and try again.`
);
}
if (options.generateExportsField) {
throw new Error(
`Setting 'generateExportsField: true' is not supported with the current TypeScript setup. Set 'exports' field in the 'package.json' file at the project root and unset the 'generateExportsField' option.`
);
}
if (options.additionalEntryPoints?.length) {
throw new Error(
`Setting 'additionalEntryPoints' is not supported with the current TypeScript setup. Set additional entry points in the 'package.json' file at the project root and unset the 'additionalEntryPoints' option.`
);
}
}
const outputPath = join(root, options.outputPath); const outputPath = join(root, options.outputPath);
if (options.skipTypeCheck == null) { if (options.skipTypeCheck == null) {
@ -87,6 +111,7 @@ function normalizeOptions(
tsConfig: join(root, options.tsConfig), tsConfig: join(root, options.tsConfig),
swcCliOptions, swcCliOptions,
tmpSwcrcPath, tmpSwcrcPath,
isTsSolutionSetup: isTsSolutionSetup,
} as NormalizedSwcExecutorOptions; } as NormalizedSwcExecutorOptions;
} }
@ -97,6 +122,10 @@ export async function* swcExecutor(
const { sourceRoot, root } = const { sourceRoot, root } =
context.projectsConfigurations.projects[context.projectName]; context.projectsConfigurations.projects[context.projectName];
const options = normalizeOptions(_options, context.root, sourceRoot, root); const options = normalizeOptions(_options, context.root, sourceRoot, root);
let swcHelperDependency: DependentBuildableProjectNode;
let inlineProjectGraph: InlineProjectGraph;
if (!options.isTsSolutionSetup) {
const { tmpTsConfig, dependencies } = checkDependencies( const { tmpTsConfig, dependencies } = checkDependencies(
context, context,
options.tsConfig options.tsConfig
@ -106,7 +135,7 @@ export async function* swcExecutor(
options.tsConfig = tmpTsConfig; options.tsConfig = tmpTsConfig;
} }
const swcHelperDependency = getHelperDependency( swcHelperDependency = getHelperDependency(
HelperDependency.swc, HelperDependency.swc,
options.swcCliOptions.swcrcPath, options.swcCliOptions.swcrcPath,
dependencies, dependencies,
@ -117,7 +146,7 @@ export async function* swcExecutor(
dependencies.push(swcHelperDependency); dependencies.push(swcHelperDependency);
} }
const inlineProjectGraph = handleInliningBuild( inlineProjectGraph = handleInliningBuild(
context, context,
options, options,
options.tsConfig options.tsConfig
@ -148,6 +177,7 @@ export async function* swcExecutor(
options.tmpSwcrcPath options.tmpSwcrcPath
); );
} }
}
function determineModuleFormatFromSwcrc( function determineModuleFormatFromSwcrc(
absolutePathToSwcrc: string absolutePathToSwcrc: string
@ -163,7 +193,9 @@ export async function* swcExecutor(
return yield* compileSwcWatch(context, options, async () => { return yield* compileSwcWatch(context, options, async () => {
const assetResult = await copyAssets(options, context); const assetResult = await copyAssets(options, context);
const packageJsonResult = await copyPackageJson( let packageJsonResult: CopyPackageJsonResult;
if (!options.isTsSolutionSetup) {
packageJsonResult = await copyPackageJson(
{ {
...options, ...options,
additionalEntryPoints: createEntryPoints(options, context), additionalEntryPoints: createEntryPoints(options, context),
@ -173,6 +205,7 @@ export async function* swcExecutor(
}, },
context context
); );
}
removeTmpSwcrc(options.swcCliOptions.swcrcPath); removeTmpSwcrc(options.swcCliOptions.swcrcPath);
disposeFn = () => { disposeFn = () => {
assetResult?.stop(); assetResult?.stop();
@ -182,6 +215,7 @@ export async function* swcExecutor(
} else { } else {
return yield compileSwc(context, options, async () => { return yield compileSwc(context, options, async () => {
await copyAssets(options, context); await copyAssets(options, context);
if (!options.isTsSolutionSetup) {
await copyPackageJson( await copyPackageJson(
{ {
...options, ...options,
@ -193,12 +227,13 @@ export async function* swcExecutor(
}, },
context context
); );
removeTmpSwcrc(options.swcCliOptions.swcrcPath);
postProcessInlinedDependencies( postProcessInlinedDependencies(
options.outputPath, options.outputPath,
options.originalProjectRoot, options.originalProjectRoot,
inlineProjectGraph inlineProjectGraph
); );
}
removeTmpSwcrc(options.swcCliOptions.swcrcPath);
}); });
} }
} }

View File

@ -1561,6 +1561,7 @@ describe('lib', () => {
"name": "@proj/my-lib", "name": "@proj/my-lib",
"nx": { "nx": {
"name": "my-lib", "name": "my-lib",
"sourceRoot": "my-lib/src",
}, },
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.1",

View File

@ -266,31 +266,6 @@ async function configureProject(
updateNxJson(tree, nxJson); updateNxJson(tree, nxJson);
} }
if (!options.useProjectJson) {
// we create a cleaner project configuration for the package.json file
const projectConfiguration: ProjectConfiguration = {
root: options.projectRoot,
};
if (options.name !== options.importPath) {
// if the name is different than the package.json name, we need to set
// the proper name in the configuration
projectConfiguration.name = options.name;
}
if (options.parsedTags?.length) {
projectConfiguration.tags = options.parsedTags;
}
if (options.publishable) {
await addProjectToNxReleaseConfig(tree, options, projectConfiguration);
}
updateProjectConfiguration(tree, options.name, projectConfiguration);
return;
}
const projectConfiguration: ProjectConfiguration = { const projectConfiguration: ProjectConfiguration = {
root: options.projectRoot, root: options.projectRoot,
sourceRoot: joinPathFragments(options.projectRoot, 'src'), sourceRoot: joinPathFragments(options.projectRoot, 'src'),
@ -300,11 +275,11 @@ async function configureProject(
}; };
if ( if (
options.bundler && options.config !== 'npm-scripts' &&
options.bundler !== 'none' && (options.bundler === 'swc' ||
options.config !== 'npm-scripts' options.bundler === 'esbuild' ||
(!options.isUsingTsSolutionConfig && options.bundler === 'tsc'))
) { ) {
if (options.bundler !== 'rollup') {
const outputPath = getOutputPath(options); const outputPath = getOutputPath(options);
const executor = getBuildExecutor(options.bundler); const executor = getBuildExecutor(options.bundler);
addBuildTargetDefaults(tree, executor); addBuildTargetDefaults(tree, executor);
@ -314,15 +289,12 @@ async function configureProject(
outputs: ['{options.outputPath}'], outputs: ['{options.outputPath}'],
options: { options: {
outputPath, outputPath,
main: main: `${options.projectRoot}/src/index` + (options.js ? '.js' : '.ts'),
`${options.projectRoot}/src/index` + (options.js ? '.js' : '.ts'),
tsConfig: `${options.projectRoot}/tsconfig.lib.json`, tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
assets: [],
}, },
}; };
if (options.bundler === 'esbuild') { if (options.bundler === 'esbuild') {
projectConfiguration.targets.build.options.generatePackageJson = true;
projectConfiguration.targets.build.options.format = ['cjs']; projectConfiguration.targets.build.options.format = ['cjs'];
} }
@ -330,6 +302,17 @@ async function configureProject(
projectConfiguration.targets.build.options.skipTypeCheck = true; projectConfiguration.targets.build.options.skipTypeCheck = true;
} }
if (options.isUsingTsSolutionConfig) {
if (options.bundler === 'esbuild') {
projectConfiguration.targets.build.options.declarationRootDir = `${options.projectRoot}/src`;
}
} else {
projectConfiguration.targets.build.options.assets = [];
if (options.bundler === 'esbuild') {
projectConfiguration.targets.build.options.generatePackageJson = true;
}
if (!options.minimal) { if (!options.minimal) {
projectConfiguration.targets.build.options.assets ??= []; projectConfiguration.targets.build.options.assets ??= [];
projectConfiguration.targets.build.options.assets.push( projectConfiguration.targets.build.options.assets.push(
@ -337,8 +320,10 @@ async function configureProject(
); );
} }
} }
}
if (options.publishable) { if (options.publishable) {
if (!options.isUsingTsSolutionConfig) {
const packageRoot = joinPathFragments( const packageRoot = joinPathFragments(
defaultOutputDirectory, defaultOutputDirectory,
'{projectRoot}' '{projectRoot}'
@ -361,12 +346,22 @@ async function configureProject(
}, },
}, },
}; };
}
await addProjectToNxReleaseConfig(tree, options, projectConfiguration); await addProjectToNxReleaseConfig(tree, options, projectConfiguration);
} }
}
if (options.config === 'workspace' || options.config === 'project') { if (!options.useProjectJson) {
// we want the package.json as clean as possible, with the bare minimum
if (!projectConfiguration.tags?.length) {
delete projectConfiguration.tags;
}
// automatically inferred as `library`
delete projectConfiguration.projectType;
// empty targets are cleaned up automatically by `updateProjectConfiguration`
updateProjectConfiguration(tree, options.name, projectConfiguration);
} else if (options.config === 'workspace' || options.config === 'project') {
addProjectConfiguration(tree, options.name, projectConfiguration); addProjectConfiguration(tree, options.name, projectConfiguration);
} else { } else {
addProjectConfiguration(tree, options.name, { addProjectConfiguration(tree, options.name, {
@ -716,30 +711,6 @@ async function normalizeOptions(
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(tree); const isUsingTsSolutionConfig = isUsingTsSolutionSetup(tree);
if (isUsingTsSolutionConfig) { if (isUsingTsSolutionConfig) {
if (options.bundler === 'esbuild' || options.bundler === 'swc') {
throw new Error(
`Cannot use the "${options.bundler}" bundler when using the @nx/js/typescript plugin.`
);
}
if (options.bundler === undefined && options.compiler === undefined) {
options.bundler = await promptWhenInteractive<{ bundler: Bundler }>(
{
type: 'select',
name: 'bundler',
message: `Which bundler would you like to use to build the library? Choose 'none' to skip build setup.`,
choices: [
{ name: 'tsc' },
{ name: 'rollup' },
{ name: 'vite' },
{ name: 'none' },
],
initial: 0,
},
{ bundler: 'tsc' }
).then(({ bundler }) => bundler);
}
options.linter ??= await promptWhenInteractive<{ options.linter ??= await promptWhenInteractive<{
linter: 'none' | 'eslint'; linter: 'none' | 'eslint';
}>( }>(
@ -766,50 +737,6 @@ async function normalizeOptions(
{ unitTestRunner: 'none' } { unitTestRunner: 'none' }
).then(({ unitTestRunner }) => unitTestRunner); ).then(({ unitTestRunner }) => unitTestRunner);
} else { } else {
if (options.bundler === undefined && options.compiler === undefined) {
options.bundler = await promptWhenInteractive<{ bundler: Bundler }>(
{
type: 'select',
name: 'bundler',
message: `Which bundler would you like to use to build the library? Choose 'none' to skip build setup.`,
choices: [
{ name: 'swc' },
{ name: 'tsc' },
{ name: 'rollup' },
{ name: 'vite' },
{ name: 'esbuild' },
{ name: 'none' },
],
initial: 1,
},
{ bundler: 'tsc' }
).then(({ bundler }) => bundler);
} else {
/**
* We are deprecating the compiler and the buildable options.
* However, we want to keep the existing behavior for now.
*
* So, if the user has not provided a bundler, we will use the compiler option, if any.
*
* If the user has not provided a bundler and no compiler, but has set buildable to true,
* we will use tsc, since that is the compiler the old generator used to default to, if buildable was true
* and no compiler was provided.
*
* If the user has not provided a bundler and no compiler, and has not set buildable to true, then
* set the bundler to tsc, to preserve old default behaviour (buildable: true by default).
*
* If it's publishable, we need to build the code before publishing it, so again
* we default to `tsc`. In the previous version of this, it would set `buildable` to true
* and that would default to `tsc`.
*
* In the past, the only way to get a non-buildable library was to set buildable to false.
* Now, the only way to get a non-buildble library is to set bundler to none.
* By default, with nothing provided, libraries are buildable with `@nx/js:tsc`.
*/
options.bundler ??= options.compiler;
}
options.linter ??= await promptWhenInteractive<{ options.linter ??= await promptWhenInteractive<{
linter: 'none' | 'eslint'; linter: 'none' | 'eslint';
}>( }>(
@ -843,6 +770,29 @@ async function normalizeOptions(
} }
} }
/**
* We are deprecating the compiler and the buildable options.
* However, we want to keep the existing behavior for now.
*
* So, if the user has not provided a bundler, we will use the compiler option, if any.
*
* If the user has not provided a bundler and no compiler, but has set buildable to true,
* we will use tsc, since that is the compiler the old generator used to default to, if buildable was true
* and no compiler was provided.
*
* If the user has not provided a bundler and no compiler, and has not set buildable to true, then
* set the bundler to tsc, to preserve old default behaviour (buildable: true by default).
*
* If it's publishable, we need to build the code before publishing it, so again
* we default to `tsc`. In the previous version of this, it would set `buildable` to true
* and that would default to `tsc`.
*
* In the past, the only way to get a non-buildable library was to set buildable to false.
* Now, the only way to get a non-buildble library is to set bundler to none.
* By default, with nothing provided, libraries are buildable with `@nx/js:tsc`.
*/
options.bundler ??= options.compiler ?? 'tsc';
// ensure programmatic runs have an expected default // ensure programmatic runs have an expected default
if (!options.config) { if (!options.config) {
options.config = 'project'; options.config = 'project';
@ -994,6 +944,11 @@ function getBuildExecutor(bundler: Bundler) {
} }
function getOutputPath(options: NormalizedLibraryGeneratorOptions) { function getOutputPath(options: NormalizedLibraryGeneratorOptions) {
if (options.isUsingTsSolutionConfig) {
// Executors expect paths relative to workspace root, so we prepend the project root
return joinPathFragments(options.projectRoot, 'dist');
}
const parts = [defaultOutputDirectory]; const parts = [defaultOutputDirectory];
if (options.projectRoot === '.') { if (options.projectRoot === '.') {
parts.push(options.name); parts.push(options.name);
@ -1170,8 +1125,12 @@ function determineEntryFields(
case 'swc': case 'swc':
return { return {
type: 'commonjs', type: 'commonjs',
main: './src/index.js', main: options.isUsingTsSolutionConfig
typings: './src/index.d.ts', ? './dist/src/index.js'
: './src/index.js',
typings: options.isUsingTsSolutionConfig
? './dist/src/index.d.ts'
: './src/index.d.ts',
}; };
case 'rollup': case 'rollup':
return { return {
@ -1202,8 +1161,12 @@ function determineEntryFields(
// For libraries intended for Node, use CJS. // For libraries intended for Node, use CJS.
return { return {
type: 'commonjs', type: 'commonjs',
main: './index.cjs', main: options.isUsingTsSolutionConfig
// typings is missing for esbuild currently ? './dist/index.cjs'
: './index.cjs',
typings: options.isUsingTsSolutionConfig
? './dist/index.d.ts'
: './index.d.ts',
}; };
default: { default: {
return { return {

View File

@ -24,6 +24,8 @@
"description": "The bundler to use. Choosing 'none' means this library is not buildable.", "description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"type": "string", "type": "string",
"enum": ["swc", "tsc", "rollup", "vite", "esbuild", "none"], "enum": ["swc", "tsc", "rollup", "vite", "esbuild", "none"],
"default": "tsc",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important" "x-priority": "important"
}, },
"linter": { "linter": {

View File

@ -53,6 +53,7 @@ export interface NormalizedSwcExecutorOptions
skipTypeCheck: boolean; skipTypeCheck: boolean;
swcCliOptions: SwcCliOptions; swcCliOptions: SwcCliOptions;
tmpSwcrcPath: string; tmpSwcrcPath: string;
isTsSolutionSetup: boolean;
sourceRoot?: string; sourceRoot?: string;
// TODO(v21): remove inline feature // TODO(v21): remove inline feature
inline?: boolean; inline?: boolean;

View File

@ -1,4 +1,11 @@
import { output, readJson, readNxJson, type Tree } from '@nx/devkit'; import {
output,
readJson,
readNxJson,
workspaceRoot,
type Tree,
} from '@nx/devkit';
import { FsTree } from 'nx/src/generators/tree';
import { isUsingPackageManagerWorkspaces } from '../package-manager-workspaces'; import { isUsingPackageManagerWorkspaces } from '../package-manager-workspaces';
export function isUsingTypeScriptPlugin(tree: Tree): boolean { export function isUsingTypeScriptPlugin(tree: Tree): boolean {
@ -13,7 +20,9 @@ export function isUsingTypeScriptPlugin(tree: Tree): boolean {
); );
} }
export function isUsingTsSolutionSetup(tree: Tree): boolean { export function isUsingTsSolutionSetup(tree?: Tree): boolean {
tree ??= new FsTree(workspaceRoot, false);
return ( return (
isUsingPackageManagerWorkspaces(tree) && isUsingPackageManagerWorkspaces(tree) &&
isWorkspaceSetupWithTsSolution(tree) isWorkspaceSetupWithTsSolution(tree)