nx/e2e/nx-run/src/run.test.ts
Jason Jean 396ffc4636
feat(core): enable project crystal by default (#21403)
Co-authored-by: Katerina Skroumpelou <sk.katherine@gmail.com>
Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
Co-authored-by: Colum Ferry <cferry09@gmail.com>
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
Co-authored-by: Emily Xiong <xiongemi@gmail.com>
Co-authored-by: Nicholas Cunningham <ndcunningham@gmail.com>
2024-02-02 03:40:59 -05:00

810 lines
25 KiB
TypeScript

import {
checkFilesExist,
cleanupProject,
fileExists,
isWindows,
newProject,
readJson,
removeFile,
runCLI,
runCLIAsync,
runCommand,
tmpProjPath,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { PackageJson } from 'nx/src/utils/package-json';
import * as path from 'path';
describe('Nx Running Tests', () => {
let proj: string;
beforeAll(
() => (proj = newProject({ packages: ['@nx/js', '@nx/web', '@nx/node'] }))
);
afterAll(() => cleanupProject());
// Ensures that nx.json is restored to its original state after each test
let existingNxJson;
beforeEach(() => {
existingNxJson = readJson('nx.json');
});
afterEach(() => {
updateFile('nx.json', JSON.stringify(existingNxJson, null, 2));
});
describe('running targets', () => {
describe('(forwarding params)', () => {
let proj = uniq('proj');
beforeAll(() => {
runCLI(`generate @nx/js:lib ${proj}`);
updateJson(`libs/${proj}/project.json`, (c) => {
c.targets['echo'] = {
command: 'echo ECHO:',
};
return c;
});
});
it.each([
'--watch false',
'--watch=false',
'--arr=a,b,c',
'--arr=a --arr=b --arr=c',
'a',
'--a.b=1',
'--a.b 1',
'-- a b c --a --a.b=1',
'--ignored -- a b c --a --a.b=1',
])('should forward %s properly', (args) => {
const output = runCLI(`echo ${proj} ${args}`);
expect(output).toContain(`ECHO: ${args.replace(/^.*-- /, '')}`);
});
});
it('should execute long running tasks', () => {
const myapp = uniq('myapp');
runCLI(`generate @nx/web:app ${myapp}`);
updateJson(`apps/${myapp}/project.json`, (c) => {
c.targets['counter'] = {
executor: '@nx/workspace:counter',
options: {
to: 2,
},
};
return c;
});
const success = runCLI(`counter ${myapp} --result=true`);
expect(success).toContain('0');
expect(success).toContain('1');
expect(() => runCLI(`counter ${myapp} --result=false`)).toThrowError();
});
it('should run npm scripts', async () => {
const mylib = uniq('mylib');
runCLI(`generate @nx/node:lib ${mylib}`);
// Used to restore targets to lib after test
const original = readJson(`libs/${mylib}/project.json`);
updateJson(`libs/${mylib}/project.json`, (j) => {
delete j.targets;
return j;
});
updateFile(
`libs/${mylib}/package.json`,
JSON.stringify(<PackageJson>{
name: 'mylib1',
version: '1.0.0',
scripts: { 'echo:dev': `echo ECHOED`, 'echo:fail': 'should not run' },
nx: {
includedScripts: ['echo:dev'],
},
})
);
const { stdout } = await runCLIAsync(
`echo:dev ${mylib} -- positional --a=123 --no-b`,
{
silent: true,
}
);
if (isWindows()) {
expect(stdout).toMatch(/ECHOED "positional" "--a=123" "--no-b"/);
} else {
expect(stdout).toMatch(/ECHOED positional --a=123 --no-b/);
}
expect(runCLI(`echo:fail ${mylib}`, { silenceError: true })).toContain(
`Cannot find configuration for task ${mylib}:echo:fail`
);
updateJson(`libs/${mylib}/project.json`, (c) => original);
}, 1000000);
describe('tokens support', () => {
let app: string;
beforeAll(async () => {
app = uniq('myapp');
runCLI(`generate @nx/web:app ${app}`);
});
it('should support using {projectRoot} in options blocks in project.json', async () => {
updateJson(`apps/${app}/project.json`, (c) => {
c.targets['echo'] = {
command: `node -e 'console.log("{projectRoot}")'`,
};
return c;
});
const output = runCLI(`echo ${app}`);
expect(output).toContain(`apps/${app}`);
});
it('should support using {projectName} in options blocks in project.json', () => {
updateJson(`apps/${app}/project.json`, (c) => {
c.targets['echo'] = {
command: `node -e 'console.log("{projectName}")'`,
};
return c;
});
const output = runCLI(`echo ${app}`);
expect(output).toContain(app);
});
it('should support using {projectRoot} in targetDefaults', async () => {
updateJson(`nx.json`, (json) => {
json.targetDefaults = {
echo: {
command: `node -e 'console.log("{projectRoot}")'`,
},
};
return json;
});
updateJson(`apps/${app}/project.json`, (c) => {
c.targets['echo'] = {};
return c;
});
const output = runCLI(`echo ${app}`);
expect(output).toContain(`apps/${app}`);
});
it('should support using {projectName} in targetDefaults', () => {
updateJson(`nx.json`, (json) => {
json.targetDefaults = {
echo: {
command: `node -e 'console.log("{projectName}")'`,
},
};
return json;
});
updateJson(`apps/${app}/project.json`, (c) => {
c.targets['echo'] = {};
return c;
});
const output = runCLI(`echo ${app}`);
expect(output).toContain(app);
});
});
});
describe('Nx Bail', () => {
it('should stop executing all tasks when one of the tasks fails', async () => {
const myapp1 = uniq('a');
const myapp2 = uniq('b');
runCLI(`generate @nx/web:app ${myapp1}`);
runCLI(`generate @nx/web:app ${myapp2}`);
updateJson(`apps/${myapp1}/project.json`, (c) => {
c.targets['error'] = {
command: 'echo boom1 && exit 1',
};
return c;
});
updateJson(`apps/${myapp2}/project.json`, (c) => {
c.targets['error'] = {
executor: 'nx:run-commands',
options: {
command: 'echo boom2 && exit 1',
},
};
return c;
});
let withoutBail = runCLI(`run-many --target=error --parallel=1`, {
silenceError: true,
})
.split('\n')
.map((r) => r.trim())
.filter((r) => r);
withoutBail = withoutBail.slice(withoutBail.indexOf('Failed tasks:'));
expect(withoutBail).toContain(`- ${myapp1}:error`);
expect(withoutBail).toContain(`- ${myapp2}:error`);
let withBail = runCLI(`run-many --target=error --parallel=1 --nx-bail`, {
silenceError: true,
})
.split('\n')
.map((r) => r.trim())
.filter((r) => r);
withBail = withBail.slice(withBail.indexOf('Failed tasks:'));
expect(withBail.length).toEqual(2);
if (withBail[1] === `- ${myapp1}:error`) {
expect(withBail).not.toContain(`- ${myapp2}:error`);
} else {
expect(withBail[1]).toEqual(`- ${myapp2}:error`);
expect(withBail).not.toContain(`- ${myapp1}:error`);
}
});
});
describe('run-one', () => {
it('should build a specific project', () => {
const myapp = uniq('app');
runCLI(`generate @nx/web:app ${myapp}`);
runCLI(`build ${myapp}`);
}, 10000);
it('should support project name positional arg non-consecutive to target', () => {
const myapp = uniq('app');
runCLI(`generate @nx/web:app ${myapp}`);
runCLI(`build --verbose ${myapp}`);
}, 10000);
it('should run targets from package json', () => {
const myapp = uniq('app');
const target = uniq('script');
const expectedOutput = uniq('myEchoedString');
const expectedEnvOutput = uniq('myEnvString');
runCLI(`generate @nx/web:app ${myapp}`);
updateFile(
`apps/${myapp}/package.json`,
JSON.stringify({
name: myapp,
scripts: {
[target]: `echo ${expectedOutput} $ENV_VAR`,
},
nx: {
targets: {
[target]: {
configurations: {
production: {},
},
},
},
},
})
);
updateFile(
`apps/${myapp}/.env.production`,
`ENV_VAR=${expectedEnvOutput}`
);
expect(runCLI(`${target} ${myapp}`)).toContain(expectedOutput);
expect(runCLI(`${target} ${myapp}`)).not.toContain(expectedEnvOutput);
expect(runCLI(`${target} ${myapp} --configuration production`)).toContain(
expectedEnvOutput
);
}, 10000);
it('should run targets inferred from plugin-specified project files', () => {
// Setup an app to extend
const myapp = uniq('app');
runCLI(`generate @nx/web:app ${myapp}`);
// Register an Nx plugin
const plugin = `module.exports = {
projectFilePatterns: ['inferred-project.nxproject'],
registerProjectTargets: () => ({
"echo": {
"executor": "nx:run-commands",
"options": {
"command": "echo inferred-target"
}
}
})
}`;
updateFile('tools/local-plugin/plugin.js', plugin);
updateFile('nx.json', (c) => {
const nxJson = JSON.parse(c);
nxJson.plugins = ['./tools/local-plugin/plugin.js'];
return JSON.stringify(nxJson, null, 2);
});
// Create a custom project file for the app
updateFile(`apps/${myapp}/inferred-project.nxproject`, 'contents');
expect(runCLI(`echo ${myapp}`)).toContain('inferred-target');
});
it('should build a specific project with the daemon disabled', () => {
const myapp = uniq('app');
runCLI(`generate @nx/web:app ${myapp}`);
const buildWithDaemon = runCLI(`build ${myapp}`, {
env: { NX_DAEMON: 'false' },
});
expect(buildWithDaemon).toContain('Successfully ran target build');
const buildAgain = runCLI(`build ${myapp}`, {
env: { NX_DAEMON: 'false' },
});
expect(buildAgain).toContain('[local cache]');
}, 10000);
it('should build the project when within the project root', () => {
const myapp = uniq('app');
runCLI(`generate @nx/web:app ${myapp}`);
// Should work within the project directory
expect(runCommand(`cd apps/${myapp}/src && npx nx build`)).toContain(
`nx run ${myapp}:build`
);
}, 10000);
describe('target defaults + executor specifications', () => {
it('should be able to run targets with unspecified executor given an appropriate targetDefaults entry', () => {
const target = uniq('target');
const lib = uniq('lib');
updateJson('nx.json', (nxJson) => {
nxJson.targetDefaults ??= {};
nxJson.targetDefaults[target] = {
executor: 'nx:run-commands',
options: {
command: `echo Hello from ${target}`,
},
};
return nxJson;
});
updateFile(
`libs/${lib}/project.json`,
JSON.stringify({
name: lib,
targets: {
[target]: {},
},
})
);
expect(runCLI(`${target} ${lib} --verbose`)).toContain(
`Hello from ${target}`
);
});
it('should be able to pull options from targetDefaults based on executor', () => {
const target = uniq('target');
const lib = uniq('lib');
updateJson('nx.json', (nxJson) => {
nxJson.targetDefaults ??= {};
nxJson.targetDefaults[`nx:run-commands`] = {
options: {
command: `echo Hello from ${target}`,
},
};
return nxJson;
});
updateFile(
`libs/${lib}/project.json`,
JSON.stringify({
name: lib,
targets: {
[target]: {
executor: 'nx:run-commands',
},
},
})
);
expect(runCLI(`${target} ${lib} --verbose`)).toContain(
`Hello from ${target}`
);
});
});
describe('target dependencies', () => {
let myapp;
let mylib1;
let mylib2;
beforeAll(() => {
myapp = uniq('myapp');
mylib1 = uniq('mylib1');
mylib2 = uniq('mylib1');
runCLI(`generate @nx/web:app ${myapp}`);
runCLI(`generate @nx/js:lib ${mylib1}`);
runCLI(`generate @nx/js:lib ${mylib2}`);
updateFile(
`apps/${myapp}/src/main.ts`,
`
import "@${proj}/${mylib1}";
import "@${proj}/${mylib2}";
`
);
});
it('should be able to include deps using dependsOn', async () => {
const originalWorkspace = readJson(`apps/${myapp}/project.json`);
updateJson(`apps/${myapp}/project.json`, (config) => {
config.targets.prep = {
executor: 'nx:run-commands',
options: {
command: 'echo PREP',
},
};
config.targets.build = {
dependsOn: ['prep', '^build'],
};
return config;
});
const output = runCLI(`build ${myapp}`);
expect(output).toContain(
`NX Running target build for project ${myapp} and 3 tasks it depends on`
);
expect(output).toContain(myapp);
expect(output).toContain(mylib1);
expect(output).toContain(mylib2);
expect(output).toContain('PREP');
updateJson(`apps/${myapp}/project.json`, () => originalWorkspace);
}, 10000);
it('should be able to include deps using target defaults defined at the root', async () => {
const nxJson = readJson('nx.json');
updateJson(`apps/${myapp}/project.json`, (config) => {
config.targets.prep = {
command: 'echo PREP > one.txt',
};
config.targets.outside = {
command: 'echo OUTSIDE',
};
return config;
});
nxJson.targetDefaults = {
prep: {
outputs: ['{workspaceRoot}/one.txt'],
cache: true,
},
outside: {
dependsOn: ['prep'],
cache: true,
},
};
updateFile('nx.json', JSON.stringify(nxJson));
const output = runCLI(`outside ${myapp}`);
expect(output).toContain(
`NX Running target outside for project ${myapp} and 1 task it depends on`
);
removeFile(`one.txt`);
runCLI(`outside ${myapp}`);
checkFilesExist(`one.txt`);
}, 10000);
});
});
describe('run-many', () => {
it('should build specific and all projects', () => {
// This is required to ensure the numbers used in the assertions make sense for this test
const proj = newProject();
const appA = uniq('appa-rand');
const libA = uniq('liba-rand');
const libB = uniq('libb-rand');
const libC = uniq('libc-rand');
const libD = uniq('libd-rand');
runCLI(`generate @nx/web:app ${appA}`);
runCLI(`generate @nx/js:lib ${libA} --bundler=tsc --defaults`);
runCLI(
`generate @nx/js:lib ${libB} --bundler=tsc --defaults --tags=ui-a`
);
runCLI(
`generate @nx/js:lib ${libC} --bundler=tsc --defaults --tags=ui-b,shared`
);
runCLI(`generate @nx/node:lib ${libD} --defaults --tags=api`);
// libA depends on libC
updateFile(
`libs/${libA}/src/lib/${libA}.spec.ts`,
`
import '@${proj}/${libC}';
describe('sample test', () => {
it('should test', () => {
expect(1).toEqual(1);
});
});
`
);
// testing run many starting'
const buildParallel = runCLI(
`run-many --target=build --projects="${libC},${libB}"`
);
expect(buildParallel).toContain(`Running target build for 2 projects:`);
expect(buildParallel).not.toContain(`- ${appA}`);
expect(buildParallel).not.toContain(`- ${libA}`);
expect(buildParallel).toContain(`- ${libB}`);
expect(buildParallel).toContain(`- ${libC}`);
expect(buildParallel).not.toContain(`- ${libD}`);
expect(buildParallel).toContain('Successfully ran target build');
// testing run many --all starting
const buildAllParallel = runCLI(`run-many --target=build`);
expect(buildAllParallel).toContain(
`Running target build for 4 projects:`
);
expect(buildAllParallel).toContain(`- ${appA}`);
expect(buildAllParallel).toContain(`- ${libA}`);
expect(buildAllParallel).toContain(`- ${libB}`);
expect(buildAllParallel).toContain(`- ${libC}`);
expect(buildAllParallel).not.toContain(`- ${libD}`);
expect(buildAllParallel).toContain('Successfully ran target build');
// testing run many by tags
const buildByTagParallel = runCLI(
`run-many --target=build --projects="tag:ui*"`
);
expect(buildByTagParallel).toContain(
`Running target build for 2 projects:`
);
expect(buildByTagParallel).not.toContain(`- ${appA}`);
expect(buildByTagParallel).not.toContain(`- ${libA}`);
expect(buildByTagParallel).toContain(`- ${libB}`);
expect(buildByTagParallel).toContain(`- ${libC}`);
expect(buildByTagParallel).not.toContain(`- ${libD}`);
expect(buildByTagParallel).toContain('Successfully ran target build');
// testing run many with exclude
const buildWithExcludeParallel = runCLI(
`run-many --target=build --exclude="${libD},tag:ui*"`
);
expect(buildWithExcludeParallel).toContain(
`Running target build for 2 projects and 1 task they depend on:`
);
expect(buildWithExcludeParallel).toContain(`- ${appA}`);
expect(buildWithExcludeParallel).toContain(`- ${libA}`);
expect(buildWithExcludeParallel).not.toContain(`- ${libB}`);
expect(buildWithExcludeParallel).toContain(`${libC}`); // should still include libC as dependency despite exclude
expect(buildWithExcludeParallel).not.toContain(`- ${libD}`);
expect(buildWithExcludeParallel).toContain(
'Successfully ran target build'
);
// testing run many when project depends on other projects
const buildWithDeps = runCLI(
`run-many --target=build --projects="${libA}"`
);
expect(buildWithDeps).toContain(
`Running target build for project ${libA} and 1 task it depends on:`
);
expect(buildWithDeps).not.toContain(`- ${appA}`);
expect(buildWithDeps).toContain(`- ${libA}`);
expect(buildWithDeps).toContain(`${libC}`); // build should include libC as dependency
expect(buildWithDeps).not.toContain(`- ${libB}`);
expect(buildWithDeps).not.toContain(`- ${libD}`);
expect(buildWithDeps).toContain('Successfully ran target build');
// testing run many --configuration
const buildConfig = runCLI(
`run-many --target=build --projects="${appA},${libA}" --prod`
);
expect(buildConfig).toContain(
`Running target build for 2 projects and 1 task they depend on:`
);
expect(buildConfig).toContain(`run ${appA}:build`);
expect(buildConfig).toContain(`run ${libA}:build`);
expect(buildConfig).toContain(`run ${libC}:build`);
expect(buildConfig).toContain('Successfully ran target build');
// testing run many with daemon disabled
const buildWithDaemon = runCLI(`run-many --target=build`, {
env: { NX_DAEMON: 'false' },
});
expect(buildWithDaemon).toContain(`Successfully ran target build`);
}, 1000000);
it('should run multiple targets', () => {
const myapp1 = uniq('myapp');
const myapp2 = uniq('myapp');
runCLI(`generate @nx/web:app ${myapp1}`);
runCLI(`generate @nx/web:app ${myapp2}`);
let outputs = runCLI(
// Options with lists can be specified using multiple args or with a delimiter (comma or space).
`run-many -t build -t test -p ${myapp1} ${myapp2}`
);
expect(outputs).toContain('Running targets build, test for 2 projects:');
outputs = runCLI(`run-many -t build test -p=${myapp1},${myapp2}`);
expect(outputs).toContain('Running targets build, test for 2 projects:');
});
});
describe('exec', () => {
let pkg: string;
let pkg2: string;
let pkgRoot: string;
let pkg2Root: string;
let originalRootPackageJson: PackageJson;
beforeAll(() => {
originalRootPackageJson = readJson<PackageJson>('package.json');
pkg = uniq('package');
pkg2 = uniq('package');
pkgRoot = tmpProjPath(path.join('libs', pkg));
pkg2Root = tmpProjPath(path.join('libs', pkg2));
runCLI(`generate @nx/js:lib ${pkg} --bundler=none --unitTestRunner=none`);
runCLI(
`generate @nx/js:lib ${pkg2} --bundler=none --unitTestRunner=none`
);
updateJson<PackageJson>('package.json', (v) => {
v.workspaces = ['libs/*'];
return v;
});
updateFile(
`libs/${pkg}/package.json`,
JSON.stringify(<PackageJson>{
name: pkg,
version: '0.0.1',
scripts: {
build: 'nx exec -- echo HELLO',
'build:option': 'nx exec -- echo HELLO WITH OPTION',
},
nx: {
targets: {
build: {
cache: true,
},
},
},
})
);
updateFile(
`libs/${pkg2}/package.json`,
JSON.stringify(<PackageJson>{
name: pkg2,
version: '0.0.1',
scripts: {
build: "nx exec -- echo '$NX_PROJECT_NAME'",
},
})
);
updateJson(`libs/${pkg2}/project.json`, (content) => {
content['implicitDependencies'] = [pkg];
return content;
});
});
afterAll(() => {
updateJson('package.json', () => originalRootPackageJson);
});
it('should work for npm scripts', () => {
const output = runCommand('npm run build', {
cwd: pkgRoot,
});
expect(output).toContain('HELLO');
expect(output).toContain(`nx run ${pkg}:build`);
});
it('should run adhoc tasks in topological order', () => {
let output = runCLI('exec -- echo HELLO');
expect(output).toContain('HELLO');
output = runCLI(`build ${pkg}`);
expect(output).toContain(pkg);
expect(output).not.toContain(pkg2);
output = runCommand('npm run build', {
cwd: pkgRoot,
});
expect(output).toContain(pkg);
expect(output).not.toContain(pkg2);
output = runCLI(`exec -- echo '$NX_PROJECT_NAME'`).replace(/\s+/g, ' ');
expect(output).toContain(pkg);
expect(output).toContain(pkg2);
output = runCLI("exec -- echo '$NX_PROJECT_ROOT_PATH'").replace(
/\s+/g,
' '
);
expect(output).toContain(`${path.join('libs', pkg)}`);
expect(output).toContain(`${path.join('libs', pkg2)}`);
output = runCLI(`exec --projects ${pkg} -- echo WORLD`);
expect(output).toContain('WORLD');
output = runCLI(`exec --projects ${pkg} -- echo '$NX_PROJECT_NAME'`);
expect(output).toContain(pkg);
expect(output).not.toContain(pkg2);
});
it('should work for npm scripts with delimiter', () => {
const output = runCommand('npm run build:option', { cwd: pkgRoot });
expect(output).toContain('HELLO WITH OPTION');
expect(output).toContain(`nx run ${pkg}:"build:option"`);
});
it('should pass overrides', () => {
const output = runCommand('npm run build WORLD', {
cwd: pkgRoot,
});
expect(output).toContain('HELLO WORLD');
});
describe('caching', () => {
it('should cache subsequent calls', () => {
runCommand('npm run build', {
cwd: pkgRoot,
});
const output = runCommand('npm run build', {
cwd: pkgRoot,
});
expect(output).toContain('Nx read the output from the cache');
});
// TODO(crystal, @Cammisuli): Investigate why this is failing
xit('should read outputs', () => {
const nodeCommands = [
"const fs = require('fs')",
"fs.mkdirSync('../../tmp/exec-outputs-test', {recursive: true})",
"fs.writeFileSync('../../tmp/exec-outputs-test/file.txt', 'Outputs')",
];
updateFile(
`libs/${pkg}/package.json`,
JSON.stringify(<PackageJson>{
name: pkg,
version: '0.0.1',
scripts: {
build: `nx exec -- node -e "${nodeCommands.join(';')}"`,
},
nx: {
targets: {
build: {
outputs: ['{workspaceRoot}/tmp/exec-outputs-test'],
},
},
},
})
);
runCommand('npm run build', {
cwd: pkgRoot,
});
expect(
fileExists(tmpProjPath('tmp/exec-outputs-test/file.txt'))
).toBeTruthy();
removeFile('tmp');
const output = runCommand('npm run build', {
cwd: pkgRoot,
});
expect(output).toContain('[local cache]');
expect(
fileExists(tmpProjPath('tmp/exec-outputs-test/file.txt'))
).toBeTruthy();
});
});
});
});