Compare commits

...

12 Commits

Author SHA1 Message Date
Leosvel Pérez Espinosa 0429728f6d fix(angular): prevent creating stylesheet worker multiple times in ng-packagr executors (#22491) 2024-03-25 11:29:40 -04:00
Mike Pham 814a12db15 fix(core): exponential backoff retry on cache put fail (#21926)
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
(cherry picked from commit 16af95fb3c)
2024-03-25 10:39:28 -04:00
Emily Xiong 24a9754848 fix(core): fix no plugins found for nx init without packge.json (#22434)
(cherry picked from commit b7b70da6f8)
2024-03-25 10:39:16 -04:00
Leosvel Pérez Espinosa eceb35b42e fix(misc): handle cwd correctly when generating artifacts with as-provided (#22411)
(cherry picked from commit c20e00cab8)
2024-03-25 10:38:38 -04:00
Jason Jean 43403ac8a6 fix(gradle): fix gradle plugin path (#22405)
(cherry picked from commit 64b23966cc)
2024-03-25 10:38:05 -04:00
Emily Xiong e2bd49f89e feat(gradle): add gradle init generator (#22245)
(cherry picked from commit 6d83dd7ff0)
2024-03-25 10:38:04 -04:00
Miroslav Jonaš beee11ee4d fix(linter): convert parser options to flat config even is parser is missing (#22388)
(cherry picked from commit 85ba3f9ae0)
2024-03-25 10:37:01 -04:00
Jack Hsu 002ab582e9 fix(bundling): prevent sensitive keys from being bundled (#22413)
(cherry picked from commit b7ffb257a2)
2024-03-25 10:37:00 -04:00
Nicholas Cunningham 66ae83e569 fix(core): Should work if extends is a string
(cherry picked from commit a00f6438b9)
2024-03-25 10:36:59 -04:00
Emily Xiong 5beb8d2127 fix(gradle): fix missing tasks (#22400)
(cherry picked from commit 998e99a5f7)
2024-03-25 10:36:59 -04:00
Joel Pelaez Jorge d54c080189 fix(core): override Path env variable on Windows platform (#22382)
(cherry picked from commit 384d8744e0)
2024-03-25 10:36:58 -04:00
Nicholas Cunningham 7d7841a71a fix(webpack): Stylus loader path (#22373)
(cherry picked from commit 55f31cf07b)
2024-03-25 10:36:57 -04:00
34 changed files with 767 additions and 143 deletions
+28
View File
@@ -230,4 +230,32 @@ describe('EsBuild Plugin', () => {
const output = runCLI(`build ${myPkg}`);
expect(output).toContain('custom config loaded');
}, 120_000);
it('should bundle in non-sensitive NX_ environment variables', () => {
const myPkg = uniq('my-pkg');
runCLI(`generate @nx/js:lib ${myPkg} --bundler=esbuild`, {});
updateFile(
`libs/${myPkg}/src/index.ts`,
`
console.log(process.env['NX_CLOUD_ENCRYPTION_KEY']);
console.log(process.env['NX_CLOUD_ACCESS_TOKEN']);
console.log(process.env['NX_PUBLIC_TEST']);
`
);
runCLI(`build ${myPkg} --platform=browser`, {
env: {
NX_CLOUD_ENCRYPTION_KEY: 'secret',
NX_CLOUD_ACCESS_TOKEN: 'secret',
NX_PUBLIC_TEST: 'foobar',
},
});
const output = runCommand(`node dist/libs/${myPkg}/index.cjs`, {
failOnError: true,
});
expect(output).not.toMatch(/secret/);
expect(output).toMatch(/foobar/);
});
});
+66 -57
View File
@@ -8,80 +8,89 @@ import {
runCommand,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { execSync } from 'child_process';
describe('Gradle', () => {
let gradleProjectName = uniq('my-gradle-project');
describe.each([{ type: 'kotlin' }, { type: 'groovy' }])(
'$type',
({ type }: { type: 'kotlin' | 'groovy' }) => {
let gradleProjectName = uniq('my-gradle-project');
beforeAll(() => {
newProject();
createGradleProject(gradleProjectName, type);
});
afterAll(() => cleanupProject());
beforeAll(() => {
newProject();
createGradleProject(gradleProjectName);
});
afterAll(() => cleanupProject());
it('should build', () => {
const projects = runCLI(`show projects`);
expect(projects).toContain('app');
expect(projects).toContain('list');
expect(projects).toContain('utilities');
expect(projects).toContain(gradleProjectName);
it('should build', () => {
const projects = runCLI(`show projects`);
expect(projects).toContain('app');
expect(projects).toContain('list');
expect(projects).toContain('utilities');
expect(projects).toContain(gradleProjectName);
const buildOutput = runCLI('build app', { verbose: true });
// app depends on list and utilities
expect(buildOutput).toContain('nx run list:build');
expect(buildOutput).toContain('nx run utilities:build');
const buildOutput = runCLI('build app', { verbose: true });
// app depends on list and utilities
expect(buildOutput).toContain('nx run list:build');
expect(buildOutput).toContain('nx run utilities:build');
checkFilesExist(
`app/build/libs/app.jar`,
`list/build/libs/list.jar`,
`utilities/build/libs/utilities.jar`
);
});
checkFilesExist(
`app/build/libs/app.jar`,
`list/build/libs/list.jar`,
`utilities/build/libs/utilities.jar`
);
});
it('should track dependencies for new app', () => {
if (type === 'groovy') {
createFile(
`app2/build.gradle`,
`plugins {
id 'gradleProject.groovy-application-conventions'
}
it('should track dependencies for new app', () => {
createFile(
'app2/build.gradle.kts',
`
plugins {
id("gradleProject.kotlin-application-conventions")
dependencies {
implementation project(':app')
}`
);
} else {
createFile(
`app2/build.gradle.kts`,
`plugins {
id("gradleProject.kotlin-library-conventions")
}
dependencies {
implementation(project(":app"))
}`
);
}
updateFile(
`settings.gradle${type === 'kotlin' ? '.kts' : ''}`,
(content) => {
content += `\r\ninclude("app2")`;
return content;
}
);
const buildOutput = runCLI('build app2', { verbose: true });
// app2 depends on app
expect(buildOutput).toContain('nx run app:build');
});
}
dependencies {
implementation(project(":app"))
}
`
);
updateFile(`settings.gradle.kts`, (content) => {
content += `\r\ninclude("app2")`;
return content;
});
const buildOutput = runCLI('build app2', { verbose: true });
// app2 depends on app
expect(buildOutput).toContain('nx run app:build');
});
);
});
function createGradleProject(projectName: string) {
function createGradleProject(
projectName: string,
type: 'kotlin' | 'groovy' = 'kotlin'
) {
e2eConsoleLogger(`Using java version: ${execSync('java --version')}`);
e2eConsoleLogger(`Using gradle version: ${execSync('gradle --version')}`);
e2eConsoleLogger(execSync(`gradle help --task :init`).toString());
e2eConsoleLogger(
runCommand(
`gradle init --type kotlin-application --dsl kotlin --project-name ${projectName} --package gradleProject --no-incubating --split-project`
`gradle init --type ${type}-application --dsl ${type} --project-name ${projectName} --package gradleProject --no-incubating --split-project`
)
);
updateJson('nx.json', (nxJson) => {
nxJson.plugins = ['@nx/gradle'];
return nxJson;
});
createFile(
'build.gradle.kts',
`allprojects {
apply {
plugin("project-report")
}
}`
);
runCLI(`add @nx/gradle`);
}
+30
View File
@@ -2,11 +2,14 @@ import {
checkFilesExist,
cleanupProject,
killPorts,
listFiles,
newProject,
readFile,
runCLI,
runCommandUntil,
tmpProjPath,
uniq,
updateFile,
} from '@nx/e2e/utils';
import { writeFileSync } from 'fs';
import { createFileSync } from 'fs-extra';
@@ -106,5 +109,32 @@ describe('Storybook generators and executors for monorepos', () => {
runCLI(`run ${reactStorybookApp}:build-storybook --verbose`);
checkFilesExist(`${reactStorybookApp}/storybook-static/index.html`);
}, 300_000);
it('should not bundle in sensitive NX_ environment variables', () => {
updateFile(
`${reactStorybookApp}/.storybook/main.ts`,
(content) => `
${content}
console.log(process.env);
`
);
runCLI(`run ${reactStorybookApp}:build-storybook --verbose`, {
env: {
NX_CLOUD_ENCRYPTION_KEY: 'MY SECRET',
NX_CLOUD_ACCESS_TOKEN: 'MY SECRET',
},
});
// Check all output chunks for bundled environment variables
const outDir = `${reactStorybookApp}/storybook-static`;
const files = listFiles(outDir);
for (const file of files) {
if (!file.endsWith('.js')) continue;
const content = readFile(`${outDir}/${file}`);
expect(content).not.toMatch(/NX_CLOUD_ENCRYPTION_KEY/);
expect(content).not.toMatch(/NX_CLOUD_ACCESS_TOKEN/);
expect(content).not.toMatch(/MY SECRET/);
}
}, 300_000);
});
});
+30 -1
View File
@@ -1,7 +1,8 @@
import {
checkFilesExist,
cleanupProject,
listFiles,
newProject,
readFile,
rmDist,
runCLI,
runCommand,
@@ -159,4 +160,32 @@ describe('Webpack Plugin', () => {
let output = runCommand(`node dist/${appName}/main.js`);
expect(output).toMatch(/Hello/);
}, 500_000);
it('should bundle in non-sensitive NX_ environment variables', () => {
const appName = uniq('app');
runCLI(`generate @nx/web:app ${appName} --bundler webpack`);
updateFile(
`apps/${appName}/src/main.ts`,
`
console.log(process.env['NX_CLOUD_ENCRYPTION_KEY']);
console.log(process.env['NX_CLOUD_ACCESS_TOKEN']);
console.log(process.env['NX_PUBLIC_TEST']);
`
);
runCLI(`build ${appName}`, {
env: {
NX_CLOUD_ENCRYPTION_KEY: 'secret',
NX_CLOUD_ACCESS_TOKEN: 'secret',
NX_PUBLIC_TEST: 'foobar',
},
});
const mainFile = listFiles(`dist/apps/${appName}`).filter((f) =>
f.startsWith('main.')
);
const content = readFile(`dist/apps/${appName}/${mainFile}`);
expect(content).not.toMatch(/secret/);
expect(content).toMatch(/foobar/);
});
});
@@ -1,9 +1,20 @@
import { FactoryProvider } from 'injection-js';
import type { FactoryProvider } from 'injection-js';
import { STYLESHEET_PROCESSOR_TOKEN } from 'ng-packagr/lib/styles/stylesheet-processor.di';
import { StylesheetProcessor } from './stylesheet-processor';
import { getInstalledPackageVersionInfo } from '../angular-version-utils';
import {
AsyncStylesheetProcessor,
StylesheetProcessor,
} from './stylesheet-processor';
export const STYLESHEET_PROCESSOR: FactoryProvider = {
provide: STYLESHEET_PROCESSOR_TOKEN,
useFactory: () => StylesheetProcessor,
useFactory: () => {
const { version: ngPackagrVersion } =
getInstalledPackageVersionInfo('ng-packagr');
return ngPackagrVersion !== '17.2.0'
? StylesheetProcessor
: AsyncStylesheetProcessor;
},
deps: [],
};
@@ -53,6 +53,110 @@ export class StylesheetProcessor {
];
}
async process({
filePath,
content,
}: {
filePath: string;
content: string;
}): Promise<string> {
this.createRenderWorker();
return this.renderWorker.run({ content, filePath });
}
/** Destory workers in pool. */
destroy(): void {
void this.renderWorker?.destroy();
}
private createRenderWorker(): Promise<void> {
if (this.renderWorker) {
return;
}
const styleIncludePaths = [...this.includePaths];
let prevDir = null;
let currentDir = this.basePath;
while (currentDir !== prevDir) {
const p = join(currentDir, 'node_modules');
if (existsSync(p)) {
styleIncludePaths.push(p);
}
prevDir = currentDir;
currentDir = dirname(prevDir);
}
const browserslistData = browserslist(undefined, { path: this.basePath });
const { version: ngPackagrVersion } =
getInstalledPackageVersionInfo('ng-packagr');
let postcssConfiguration: PostcssConfiguration | undefined;
if (gte(ngPackagrVersion, '17.3.0')) {
const {
loadPostcssConfiguration,
} = require('ng-packagr/lib/styles/postcss-configuration');
postcssConfiguration = loadPostcssConfiguration(this.projectBasePath);
}
this.renderWorker = new Piscina({
filename: require.resolve(
'ng-packagr/lib/styles/stylesheet-processor-worker'
),
maxThreads,
env: {
...process.env,
FORCE_COLOR: '' + colors.enabled,
},
workerData: {
postcssConfiguration,
tailwindConfigPath: getTailwindConfigPath(
this.projectBasePath,
workspaceRoot
),
projectBasePath: this.projectBasePath,
browserslistData,
targets: transformSupportedBrowsersToTargets(browserslistData),
cacheDirectory: this.cacheDirectory,
cssUrl: this.cssUrl,
styleIncludePaths,
},
});
}
}
/**
* This class is used when ng-packagr version is 17.2.0. The async `loadPostcssConfiguration` function
* introduced in ng-packagr 17.2.0 causes a memory leak due to multiple workers being created. We must
* keep this class to support any workspace that might be using ng-packagr 17.2.0 where that function
* need to be awaited.
*/
export class AsyncStylesheetProcessor {
private renderWorker: typeof Piscina | undefined;
constructor(
private readonly projectBasePath: string,
private readonly basePath: string,
private readonly cssUrl?: CssUrl,
private readonly includePaths?: string[],
private readonly cacheDirectory?: string | false
) {
// By default, browserslist defaults are too inclusive
// https://github.com/browserslist/browserslist/blob/83764ea81ffaa39111c204b02c371afa44a4ff07/index.js#L516-L522
// We change the default query to browsers that Angular support.
// https://angular.io/guide/browser-support
(browserslist.defaults as string[]) = [
'last 2 Chrome versions',
'last 1 Firefox version',
'last 2 Edge major versions',
'last 2 Safari major versions',
'last 2 iOS major versions',
'Firefox ESR',
];
}
async process({
filePath,
content,
@@ -94,7 +198,7 @@ export class StylesheetProcessor {
const { version: ngPackagrVersion } =
getInstalledPackageVersionInfo('ng-packagr');
let postcssConfiguration: PostcssConfiguration | undefined;
if (gte(ngPackagrVersion, '17.2.0')) {
if (ngPackagrVersion === '17.2.0') {
const { loadPostcssConfiguration } = await import(
'ng-packagr/lib/styles/postcss-configuration'
);
@@ -166,6 +166,33 @@ describe('determineArtifactNameAndDirectoryOptions', () => {
restoreCwd();
});
it('should not duplicate the cwd when the provided directory starts with the cwd and format is "as-provided"', async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
projectType: 'application',
});
setCwd('apps/app1');
const result = await determineArtifactNameAndDirectoryOptions(tree, {
name: 'myComponent',
directory: 'apps/app1',
nameAndDirectoryFormat: 'as-provided',
artifactType: 'component',
callingGenerator: '@my-org/my-plugin:component',
});
expect(result).toStrictEqual({
artifactName: 'myComponent',
directory: 'apps/app1',
fileName: 'myComponent',
filePath: 'apps/app1/myComponent.ts',
project: 'app1',
nameAndDirectoryFormat: 'as-provided',
});
restoreCwd();
});
it('should return the options as provided when directory is provided', async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
@@ -237,9 +237,20 @@ function getAsProvidedOptions(
): NameAndDirectoryOptions {
const relativeCwd = getRelativeCwd();
const asProvidedDirectory = options.directory
? joinPathFragments(relativeCwd, options.directory)
: relativeCwd;
let asProvidedDirectory: string;
if (options.directory) {
// append the directory to the current working directory if it doesn't start with it
if (
options.directory === relativeCwd ||
options.directory.startsWith(`${relativeCwd}/`)
) {
asProvidedDirectory = options.directory;
} else {
asProvidedDirectory = joinPathFragments(relativeCwd, options.directory);
}
} else {
asProvidedDirectory = relativeCwd;
}
const asProvidedProject = findProjectFromPath(tree, asProvidedDirectory);
const asProvidedFileName =
@@ -1,8 +1,15 @@
// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`).
// TODO(v19): Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18.
const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY'];
export function getClientEnvironment(): Record<string, string> {
const NX_APP = /^NX_/i;
return Object.keys(process.env)
.filter((key) => NX_APP.test(key) || key === 'NODE_ENV')
.filter(
(key) =>
!excludedKeys.includes(key) && (NX_APP.test(key) || key === 'NODE_ENV')
)
.reduce((env, key) => {
env[`process.env.${key}`] = JSON.stringify(process.env[key]);
return env;
@@ -496,4 +496,70 @@ describe('convert-to-flat-config generator', () => {
expect(tree.exists('eslint.config.js')).toBeTruthy();
expect(tree.exists('libs/test-lib/eslint.config.js')).toBeTruthy();
});
it('should handle parser options even if parser is extended', async () => {
addProjectConfiguration(tree, 'dx-assets-ui', {
root: 'apps/dx-assets-ui',
targets: {},
});
await lintProjectGenerator(tree, {
skipFormat: false,
linter: Linter.EsLint,
project: 'dx-assets-ui',
setParserOptionsProject: false,
});
updateJson(tree, 'apps/dx-assets-ui/.eslintrc.json', () => {
return {
extends: ['../../.eslintrc.json'],
ignorePatterns: ['!**/*', '__fixtures__/**/*'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
parserOptions: {
project: ['apps/dx-assets-ui/tsconfig.*?.json'],
},
rules: {},
},
{
files: ['*.ts', '*.tsx'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
rules: {},
},
],
};
});
await convertToFlatConfigGenerator(tree, options);
expect(tree.exists('apps/dx-assets-ui/eslint.config.js')).toBeTruthy();
expect(tree.exists('eslint.config.js')).toBeTruthy();
expect(tree.read('apps/dx-assets-ui/eslint.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"const baseConfig = require('../../eslint.config.js');
module.exports = [
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {},
languageSettings: {
parserOptions: { project: ['apps/dx-assets-ui/tsconfig.*?.json'] },
},
},
{
files: ['**/*.ts', '**/*.tsx'],
rules: {},
},
{
files: ['**/*.js', '**/*.jsx'],
rules: {},
},
{ ignores: ['__fixtures__/**/*'] },
];
"
`);
});
});
@@ -159,6 +159,12 @@ function migrateEslintFile(projectEslintPath: string, tree: Tree) {
}
// add extends
json.extends = json.extends || [];
// ensure extends is an array
if (typeof json.extends === 'string') {
json.extends = [json.extends];
}
const pathToRootConfig = `${offsetFromRoot(
dirname(projectEslintPath)
)}${baseFile}`;
@@ -760,9 +760,13 @@ export function generateFlatOverride(
!override.plugins &&
!override.parser
) {
if (override.parserOptions) {
const { parserOptions, ...rest } = override;
return generateAst({ ...rest, languageSettings: { parserOptions } });
}
return generateAst(override);
}
const { files, excludedFiles, rules, ...rest } = override;
const { files, excludedFiles, rules, parserOptions, ...rest } = override;
const objectLiteralElements: ts.ObjectLiteralElementLike[] = [
ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')),
@@ -770,6 +774,11 @@ export function generateFlatOverride(
addTSObjectProperty(objectLiteralElements, 'files', files);
addTSObjectProperty(objectLiteralElements, 'excludedFiles', excludedFiles);
addTSObjectProperty(objectLiteralElements, 'rules', rules);
if (parserOptions) {
addTSObjectProperty(objectLiteralElements, 'languageSettings', {
parserOptions,
});
}
return ts.factory.createSpreadElement(
ts.factory.createCallExpression(
+11
View File
@@ -0,0 +1,11 @@
{
"name": "Nx Gradle",
"version": "0.1",
"generators": {
"init": {
"factory": "./src/generators/init/init#initGenerator",
"schema": "./src/generators/init/schema.json",
"description": "Initializes a Gradle project in the current workspace"
}
}
}
+1
View File
@@ -1 +1,2 @@
export * from './plugin';
export { initGenerator } from './src/generators/init/init';
+8
View File
@@ -22,6 +22,14 @@
"url": "https://github.com/nrwl/nx/issues"
},
"homepage": "https://nx.dev",
"generators": "./generators.json",
"exports": {
".": "./index.js",
"./package.json": "./package.json",
"./migrations.json": "./migrations.json",
"./generators.json": "./generators.json",
"./plugin": "./plugin.js"
},
"nx-migrate": {
"migrations": "./migrations.json"
},
@@ -0,0 +1,73 @@
import { readNxJson, Tree, updateNxJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { initGenerator } from './init';
describe('@nx/gradle:init', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
tree.write('settings.gradle', '');
});
it('should add the plugin', async () => {
await initGenerator(tree, {
skipFormat: true,
skipPackageJson: false,
});
const nxJson = readNxJson(tree);
expect(nxJson.plugins).toMatchInlineSnapshot(`
[
{
"options": {
"buildTargetName": "build",
"classesTargetName": "classes",
"testTargetName": "test",
},
"plugin": "@nx/gradle",
},
]
`);
});
it('should not overwrite existing plugins', async () => {
updateNxJson(tree, {
plugins: ['foo'],
});
await initGenerator(tree, {
skipFormat: true,
skipPackageJson: false,
});
const nxJson = readNxJson(tree);
expect(nxJson.plugins).toMatchInlineSnapshot(`
[
"foo",
{
"options": {
"buildTargetName": "build",
"classesTargetName": "classes",
"testTargetName": "test",
},
"plugin": "@nx/gradle",
},
]
`);
});
it('should not add plugin if already in array', async () => {
updateNxJson(tree, {
plugins: ['@nx/gradle'],
});
await initGenerator(tree, {
skipFormat: true,
skipPackageJson: false,
});
const nxJson = readNxJson(tree);
expect(nxJson.plugins).toMatchInlineSnapshot(`
[
"@nx/gradle",
]
`);
});
});
+102
View File
@@ -0,0 +1,102 @@
import {
addDependenciesToPackageJson,
formatFiles,
GeneratorCallback,
logger,
readNxJson,
runTasksInSerial,
Tree,
updateNxJson,
} from '@nx/devkit';
import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts';
import { createNodes } from '../../plugin/nodes';
import { nxVersion } from '../../utils/versions';
import { InitGeneratorSchema } from './schema';
import { hasGradlePlugin } from '../../utils/has-gradle-plugin';
export async function initGenerator(tree: Tree, options: InitGeneratorSchema) {
const tasks: GeneratorCallback[] = [];
if (!options.skipPackageJson && tree.exists('package.json')) {
tasks.push(
addDependenciesToPackageJson(
tree,
{},
{
'@nx/gradle': nxVersion,
},
undefined,
options.keepExistingVersions
)
);
}
addPlugin(tree);
addProjectReportToBuildGradle(tree);
if (options.updatePackageScripts && tree.exists('package.json')) {
await updatePackageScripts(tree, createNodes);
}
if (!options.skipFormat) {
await formatFiles(tree);
}
return runTasksInSerial(...tasks);
}
function addPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
if (!hasGradlePlugin(tree)) {
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/gradle',
options: {
testTargetName: 'test',
classesTargetName: 'classes',
buildTargetName: 'build',
},
});
updateNxJson(tree, nxJson);
}
}
/**
* This function adds the project-report plugin to the build.gradle or build.gradle.kts file
*/
function addProjectReportToBuildGradle(tree: Tree) {
let buildGradleFile: string;
if (tree.exists('settings.gradle.kts')) {
buildGradleFile = 'build.gradle.kts';
} else if (tree.exists('settings.gradle')) {
buildGradleFile = 'build.gradle';
} else {
throw new Error(
'Could not find settings.gradle or settings.gradle.kts file in your gradle workspace.'
);
}
let buildGradleContent = '';
if (tree.exists(buildGradleFile)) {
buildGradleContent = tree.read(buildGradleFile).toString();
}
if (buildGradleContent.includes('allprojects')) {
if (!buildGradleContent.includes('"project-report')) {
logger.warn(`Please add the project-report plugin to your ${buildGradleFile}:
allprojects {
apply {
plugin("project-report")
}
}`);
}
} else {
buildGradleContent += `\n\rallprojects {
apply {
plugin("project-report")
}
}`;
tree.write(buildGradleFile, buildGradleContent);
}
}
export default initGenerator;
+6
View File
@@ -0,0 +1,6 @@
export interface InitGeneratorSchema {
skipFormat?: boolean;
skipPackageJson?: boolean;
keepExistingVersions?: boolean;
updatePackageScripts?: boolean;
}
@@ -0,0 +1,34 @@
{
"$schema": "https://json-schema.org/schema",
"$id": "NxGradleInitSchema",
"title": "Gradle Init Generator",
"description": "Initializes a Gradle project in the current workspace.",
"type": "object",
"properties": {
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`.",
"x-priority": "internal"
},
"keepExistingVersions": {
"type": "boolean",
"x-priority": "internal",
"description": "Keep existing dependencies versions",
"default": false
},
"updatePackageScripts": {
"type": "boolean",
"x-priority": "internal",
"description": "Update `package.json` scripts with inferred targets",
"default": false
}
},
"required": []
}
+15 -10
View File
@@ -39,12 +39,14 @@ export const createDependencies: CreateDependencies = async (
if (projectName && depsFile) {
dependencies = dependencies.concat(
processGradleDependencies(
depsFile,
gradleProjectToProjectName,
projectName,
gradleFile,
context
Array.from(
processGradleDependencies(
depsFile,
gradleProjectToProjectName,
projectName,
gradleFile,
context
)
)
);
}
@@ -85,12 +87,15 @@ function processGradleDependencies(
sourceProjectName: string,
gradleFile: string,
context: CreateDependenciesContext
) {
const dependencies: RawProjectGraphDependency[] = [];
): Set<RawProjectGraphDependency> {
const dependencies: Set<RawProjectGraphDependency> = new Set();
const lines = readFileSync(depsFile).toString().split('\n');
let inDeps = false;
for (const line of lines) {
if (line.startsWith('implementationDependenciesMetadata')) {
if (
line.startsWith('implementationDependenciesMetadata') ||
line.startsWith('compileClasspath')
) {
inDeps = true;
continue;
}
@@ -116,7 +121,7 @@ function processGradleDependencies(
sourceFile: gradleFile,
};
validateDependency(dependency, context);
dependencies.push(dependency);
dependencies.add(dependency);
}
}
}
+6 -7
View File
@@ -81,7 +81,6 @@ export const createNodes: CreateNodes<GradlePluginOptions> = [
try {
const {
tasksMap,
gradleProjectToTasksTypeMap,
gradleFileToOutputDirsMap,
gradleFileToGradleProjectMap,
@@ -96,16 +95,16 @@ export const createNodes: CreateNodes<GradlePluginOptions> = [
return;
}
const availableTaskNames = tasksMap.get(gradleFilePath) as string[];
const tasksTypeMap = gradleProjectToTasksTypeMap.get(
gradleProject
) as Map<string, string>;
const tasks: GradleTask[] = availableTaskNames.map((taskName) => {
return {
type: tasksTypeMap.get(taskName) ?? 'Unknown',
let tasks: GradleTask[] = [];
for (let [taskName, taskType] of tasksTypeMap.entries()) {
tasks.push({
type: taskType,
name: taskName,
};
});
});
}
const outputDirs = gradleFileToOutputDirsMap.get(gradleFilePath) as Map<
string,
+1 -14
View File
@@ -1,8 +1,7 @@
import { readFileSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { join, relative } from 'node:path';
import { workspaceRoot } from '@nx/devkit';
import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context';
import { execGradle } from './exec-gradle';
@@ -11,7 +10,6 @@ interface GradleReport {
buildFileToDepsMap: Map<string, string>;
gradleFileToOutputDirsMap: Map<string, Map<string, string>>;
gradleProjectToTasksTypeMap: Map<string, Map<string, string>>;
tasksMap: Map<string, string[]>;
gradleProjectToProjectName: Map<string, string>;
}
@@ -54,10 +52,6 @@ function processProjectReports(projectReportLines: string[]): GradleReport {
*/
const gradleProjectToGradleFileMap = new Map<string, string>();
const dependenciesMap = new Map<string, string>();
/**
* Map of Gradle Build File to available tasks
*/
const tasksMap = new Map<string, string[]>();
/**
* Map of Gradle Build File to tasks type map
*/
@@ -95,7 +89,6 @@ function processProjectReports(projectReportLines: string[]): GradleReport {
let projectName: string,
absBuildFilePath: string,
absBuildDirPath: string;
const tasks: string[] = [];
const outputDirMap = new Map<string, string>();
for (const line of propertyReportLines) {
if (line.startsWith('name: ')) {
@@ -107,10 +100,6 @@ function processProjectReports(projectReportLines: string[]): GradleReport {
if (line.startsWith('buildDir: ')) {
absBuildDirPath = line.substring('buildDir: '.length);
}
if (line.includes(': task ')) {
const taskSegments = line.split(': task ');
tasks.push(taskSegments[0]);
}
if (line.includes('Dir: ')) {
const [dirName, dirPath] = line.split(': ');
const taskName = dirName.replace('Dir', '');
@@ -141,7 +130,6 @@ function processProjectReports(projectReportLines: string[]): GradleReport {
gradleFileToGradleProjectMap.set(buildFile, gradleProject);
gradleProjectToGradleFileMap.set(gradleProject, buildFile);
gradleProjectToProjectName.set(gradleProject, projectName);
tasksMap.set(buildFile, tasks);
}
if (line.endsWith('taskReport')) {
const gradleProject = line.substring(
@@ -179,7 +167,6 @@ function processProjectReports(projectReportLines: string[]): GradleReport {
buildFileToDepsMap,
gradleFileToOutputDirsMap,
gradleProjectToTasksTypeMap,
tasksMap,
gradleProjectToProjectName,
};
}
@@ -0,0 +1,8 @@
import { readNxJson, Tree } from '@nx/devkit';
export function hasGradlePlugin(tree: Tree): boolean {
const nxJson = readNxJson(tree);
return !!nxJson.plugins?.some((p) =>
typeof p === 'string' ? p === '@nx/gradle' : p.plugin === '@nx/gradle'
);
}
+1
View File
@@ -0,0 +1 @@
export const nxVersion = require('../../package.json').version;
+5 -1
View File
@@ -360,9 +360,13 @@ export function getNextConfig(
};
}
// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`).
// TODO(v19): BREAKING: Only support NEXT_PUBLIC_ env vars and ignore NX_ vars since this is a standard Next.js feature.
const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY'];
function getNxEnvironmentVariables() {
return Object.keys(process.env)
.filter((env) => /^NX_/i.test(env))
.filter((env) => !excludedKeys.includes(env) && /^NX_/i.test(env))
.reduce((env, key) => {
env[key] = process.env[key];
return env;
+1 -1
View File
@@ -63,7 +63,7 @@ async function installPackage(pkgName: string, version: string): Promise<void> {
writeJsonFile('nx.json', nxJson);
try {
await runNxAsync('');
await runNxAsync('--help', { silent: true });
} catch (e) {
// revert adding the plugin to nx.json
nxJson.installation.plugins[pkgName] = undefined;
+50 -33
View File
@@ -49,6 +49,13 @@ export async function initHandler(options: InitArgs): Promise<void> {
);
}
generateDotNxSetup(version);
const { plugins } = await detectPlugins();
plugins.forEach((plugin) => {
execSync(`./nx add ${plugin}`, {
stdio: 'inherit',
});
});
// invokes the wrapper, thus invoking the initial installation process
runNxSync('--version', { stdio: 'ignore' });
return;
@@ -65,9 +72,9 @@ export async function initHandler(options: InitArgs): Promise<void> {
output.log({ title: '🧐 Checking dependencies' });
const detectPluginsResponse = await detectPlugins();
const { plugins, updatePackageScripts } = await detectPlugins();
if (!detectPluginsResponse?.plugins.length) {
if (!plugins.length) {
// If no plugins are detected/chosen, guide users to setup
// their targetDefaults correctly so their package scripts will work.
const packageJson: PackageJson = readJsonFile('package.json');
@@ -89,19 +96,17 @@ export async function initHandler(options: InitArgs): Promise<void> {
createNxJsonFile(repoRoot, [], [], {});
updateGitIgnore(repoRoot);
addDepsToPackageJson(repoRoot, detectPluginsResponse.plugins);
addDepsToPackageJson(repoRoot, plugins);
output.log({ title: '📦 Installing Nx' });
runInstall(repoRoot, pmc);
output.log({ title: '🔨 Configuring plugins' });
for (const plugin of detectPluginsResponse.plugins) {
for (const plugin of plugins) {
execSync(
`${pmc.exec} nx g ${plugin}:init --keepExistingVersions ${
detectPluginsResponse.updatePackageScripts
? '--updatePackageScripts'
: ''
updatePackageScripts ? '--updatePackageScripts' : ''
} --no-interactive`,
{
stdio: [0, 1, 2],
@@ -110,7 +115,7 @@ export async function initHandler(options: InitArgs): Promise<void> {
);
}
if (!detectPluginsResponse.updatePackageScripts) {
if (!updatePackageScripts) {
const rootPackageJsonPath = join(repoRoot, 'package.json');
const json = readJsonFile<PackageJson>(rootPackageJsonPath);
json.nx = { includedScripts: [] };
@@ -160,9 +165,10 @@ const npmPackageToPluginMap: Record<string, string> = {
'@remix-run/dev': '@nx/remix',
};
async function detectPlugins(): Promise<
undefined | { plugins: string[]; updatePackageScripts: boolean }
> {
async function detectPlugins(): Promise<{
plugins: string[];
updatePackageScripts: boolean;
}> {
let files = ['package.json'].concat(
globWithWorkspaceContext(process.cwd(), ['**/*/package.json'])
);
@@ -190,10 +196,18 @@ async function detectPlugins(): Promise<
}
}
}
if (existsSync('gradlew') || existsSync('gradlew.bat')) {
detectedPlugins.add('@nx/gradle');
}
const plugins = Array.from(detectedPlugins);
if (plugins.length === 0) return undefined;
if (plugins.length === 0) {
return {
plugins: [],
updatePackageScripts: false,
};
}
output.log({
title: `Recommended Plugins:`,
@@ -212,27 +226,30 @@ async function detectPlugins(): Promise<
},
]).then((r) => r.plugins);
if (pluginsToInstall?.length === 0) return undefined;
if (pluginsToInstall?.length === 0)
return {
plugins: [],
updatePackageScripts: false,
};
const updatePackageScripts = await prompt<{ updatePackageScripts: string }>([
{
name: 'updatePackageScripts',
type: 'autocomplete',
message: `Do you want to start using Nx in your package.json scripts?`,
choices: [
{
name: 'Yes',
},
{
name: 'No',
},
],
initial: 0,
},
]).then((r) => r.updatePackageScripts === 'Yes');
const updatePackageScripts =
existsSync('package.json') &&
(await prompt<{ updatePackageScripts: string }>([
{
name: 'updatePackageScripts',
type: 'autocomplete',
message: `Do you want to start using Nx in your package.json scripts?`,
choices: [
{
name: 'Yes',
},
{
name: 'No',
},
],
initial: 0,
},
]).then((r) => r.updatePackageScripts === 'Yes'));
return {
plugins: pluginsToInstall,
updatePackageScripts,
};
return { plugins: pluginsToInstall, updatePackageScripts };
}
@@ -436,7 +436,9 @@ function processEnv(color: boolean, cwd: string, env: Record<string, string>) {
...localEnv,
...env,
};
res.PATH = localEnv.PATH; // need to override PATH to make sure we are using the local node_modules
// need to override PATH to make sure we are using the local node_modules
if (localEnv.PATH) res.PATH = localEnv.PATH; // UNIX-like
if (localEnv.Path) res.Path = localEnv.Path; // Windows
if (color) {
res.FORCE_COLOR = `${color}`;
+6 -3
View File
@@ -276,17 +276,20 @@ export class Cache {
private tryAndRetry<T>(fn: () => Promise<T>): Promise<T> {
let attempts = 0;
const baseTimeout = 100;
const baseTimeout = 5;
// Generate a random number between 2 and 4 to raise to the power of attempts
const baseExponent = Math.random() * 2 + 2;
const _try = async () => {
try {
attempts++;
return await fn();
} catch (e) {
if (attempts === 10) {
// Max time is 5 * 4^3 = 20480ms
if (attempts === 6) {
// After enough attempts, throw the error
throw e;
}
await new Promise((res) => setTimeout(res, baseTimeout * attempts));
await new Promise((res) => setTimeout(res, baseExponent ** attempts));
return await _try();
}
};
+9 -1
View File
@@ -18,6 +18,10 @@ import { mergePlugins } from './merge-plugins';
import { withReact } from '../with-react';
import { existsSync } from 'fs';
// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`).
// TODO(v19): BREAKING: Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18.
const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY'];
// This is shamelessly taken from CRA and modified for NX use
// https://github.com/facebook/create-react-app/blob/4784997f0682e75eb32a897b4ffe34d735912e6c/packages/react-scripts/config/env.js#L71
function getClientEnvironment(mode) {
@@ -27,7 +31,11 @@ function getClientEnvironment(mode) {
const STORYBOOK_PREFIX = /^STORYBOOK_/i;
const raw = Object.keys(process.env)
.filter((key) => NX_PREFIX.test(key) || STORYBOOK_PREFIX.test(key))
.filter(
(key) =>
!excludedKeys.includes(key) &&
(NX_PREFIX.test(key) || STORYBOOK_PREFIX.test(key))
)
.reduce(
(env, key) => {
env[key] = process.env[key];
@@ -170,7 +170,10 @@ export function applyWebConfig(
use: [
...getCommonLoadersForCssModules(options, includePaths),
{
loader: path.join(__dirname, 'webpack/deprecated-stylus-loader.js'),
loader: path.join(
__dirname,
'../../../utils/webpack/deprecated-stylus-loader.js'
),
options: {
stylusOptions: {
include: includePaths,
@@ -230,7 +233,10 @@ export function applyWebConfig(
use: [
...getCommonLoadersForGlobalCss(options, includePaths),
{
loader: path.join(__dirname, 'webpack/deprecated-stylus-loader.js'),
loader: path.join(
__dirname,
'../../../utils/webpack/deprecated-stylus-loader.js'
),
options: {
sourceMap: !!options.sourceMap,
stylusOptions: {
@@ -291,7 +297,10 @@ export function applyWebConfig(
use: [
...getCommonLoadersForGlobalStyle(options, includePaths),
{
loader: require.resolve('stylus-loader'),
loader: path.join(
__dirname,
'../../../utils/webpack/deprecated-stylus-loader.js'
),
options: {
sourceMap: !!options.sourceMap,
stylusOptions: {
@@ -1,10 +1,14 @@
// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`).
// TODO(v19): Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18.
const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY'];
export function getClientEnvironment(mode?: string) {
// Grab NODE_ENV and NX_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const NX_APP = /^NX_/i;
const raw = Object.keys(process.env)
.filter((key) => NX_APP.test(key))
.filter((key) => !excludedKeys.includes(key) && NX_APP.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
@@ -8,8 +8,12 @@ export function interpolateEnvironmentVariablesToIndex(
const NX_PREFIX = /^NX_/i;
// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`).
// TODO(v19): Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18.
const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY'];
function isNxEnvironmentKey(x: string): boolean {
return NX_PREFIX.test(x);
return !excludedKeys.includes(x) && NX_PREFIX.test(x);
}
function getClientEnvironment(deployUrl: string) {
+1
View File
@@ -29,6 +29,7 @@ const scopes = [
{ value: 'vue', name: 'vue: anything Vue specific' },
{ value: 'web', name: 'web: anything Web specific' },
{ value: 'webpack', name: 'webpack: anything Webpack specific' },
{ value: 'gradle', name: 'gradle: anything Gradle specific'},
{value: 'module-federation', name: 'module-federation: anything Module Federation specific'},
];